@love-moon/app-sdk 0.3.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,135 @@
1
+ # @love-moon/app-sdk
2
+
3
+ ## 0.3.2
4
+
5
+ ### Changed
6
+
7
+ - `ChatEvent.task_failed.error` and `StreamReplyDelta.error` now include
8
+ optional `details?: unknown` and `cause?: unknown` fields. Additive,
9
+ non-breaking — existing consumers ignore the new fields. The default WS
10
+ → ChatEvent translation forwards both from `ConductorAppError`, so host
11
+ UIs can surface server payloads / request IDs / underlying causes without
12
+ depending on the SDK error class directly. The `<ChatView />` React store
13
+ (`useChat().state.error`) also carries these through.
14
+ - `examples/02_bff` BFF now hardcodes `role: 'user'` and strips
15
+ `metadata.audit` on `POST /messages`. **Integrators copying the example
16
+ must follow this pattern.** Without it, a malicious browser could forge
17
+ `role: 'system'|'assistant'` messages or stamp `audit.actor='app'` —
18
+ bypassing `streamReply`'s SDK-echo filter and impersonating server-side
19
+ app messages. The BFF is the only point in the pipeline that knows the
20
+ browser is untrusted; do not push role/audit choices down to the SDK.
21
+ - `client.tasks.streamReply()` now applies a default **120s idle timeout**
22
+ between consecutive `text` / status-transition deltas. If no progress is
23
+ observed in that window the iterator yields a terminal
24
+ `{ type: 'error', error: { code: 'stream_aborted', message: 'idle timeout' } }`
25
+ and closes. Long-running backends (e.g. tools that may legitimately go
26
+ silent for minutes) can opt out by passing `idleTimeoutMs: 0`:
27
+
28
+ ```ts
29
+ for await (const delta of client.tasks.streamReply(taskId, { idleTimeoutMs: 0 })) {
30
+ // never time out — caller is responsible for cancelling via signal
31
+ }
32
+ ```
33
+ - **Locked down the public `.d.ts` surface.** Internal transport types —
34
+ `Fetcher`, `FetcherOptions`, `RequestOptions`, `AppWebSocket`,
35
+ `AppWebSocketOptions`, `TasksRestApi` — are now tagged
36
+ `/** @internal */` and stripped from generated d.ts via tsup's
37
+ `dts.compilerOptions.stripInternal`. Previously these appeared as
38
+ `declare class …` in `dist/server/index.d.ts` (even though not
39
+ re-exported) and TypeScript surfaced them on hover / structural
40
+ reference. The `AppClient._internals` test-seam getter has been removed
41
+ (it was unused). The CI `bundle-smoke` script now also asserts none of
42
+ these symbols leak into any d.ts as a regression net.
43
+ - **`AppClient.close()` now safely terminates in-flight subscribe
44
+ iterators.** When a `for await` loop on `tasks.subscribe(taskId)` is
45
+ mid-stream and the caller invokes `client.close()`, the iterator now
46
+ yields a synthetic
47
+ `{ type: 'task_failed', taskId, error: { code: 'subscribe_failed', message: 'client closed' } }`
48
+ and returns instead of hanging silently. Same for `tasks.streamReply()`
49
+ — it surfaces an `{ type: 'error', error: { code: 'subscribe_failed' } }`
50
+ delta. `close()` is also idempotent (a second call is a no-op). After
51
+ close, calls to `tasks.subscribe()` / `tasks.streamReply()` / any
52
+ `tasks.*` REST method throw a synchronous
53
+ `ConductorAppError({ code: 'subscribe_failed', message: 'client is closed' })`
54
+ rather than returning a hanging iterator. Implemented internally by a
55
+ new `AppWebSocket.onClose(listener)` channel; the public API surface
56
+ is unchanged.
57
+
58
+ ## 0.1.0
59
+
60
+ Initial implementation, RFC 0027 milestones M0–M3.
61
+
62
+ ### Added — package layout (M0)
63
+
64
+ - Single npm package with three subpath entries:
65
+ - `@love-moon/app-sdk` (root, types-only + `SDK_VERSION`)
66
+ - `@love-moon/app-sdk/server` (Node)
67
+ - `@love-moon/app-sdk/react` (browser / React)
68
+ - `@love-moon/app-sdk/react/styles.css`
69
+ - tsup multi-entry build; per-entry tsconfig conditions.
70
+ - `react` + `react-dom` as optional peer dependencies — server-only consumers
71
+ don't get warnings.
72
+ - CI bundle smoke test (`npm run test:bundle`) statically asserts no Node
73
+ symbols leak into `/react` and no DOM symbols leak into `/server`.
74
+ - vitest with per-file `@vitest-environment` directives (node + jsdom).
75
+
76
+ ### Added — `/server` (M1)
77
+
78
+ - `connect()` + `AppClient` with `projects` and `tasks` sub-APIs.
79
+ - `projects.bind()` — idempotent find-or-create on (daemonHost, workspacePath);
80
+ stamps `metadata.audit.createdByApp` on creation (zero schema change).
81
+ - `projects.list()` / `projects.get()`.
82
+ - `tasks.create()` / `tasks.get()` / `tasks.list()`.
83
+ - `tasks.sendMessage()` with auto-generated `clientRequestId` (idempotent).
84
+ - `tasks.history()` with cursor-based pagination.
85
+ - `tasks.interrupt()` with `targetReplyTo`.
86
+ - `tasks.subscribe(taskId)` — `AsyncIterable<ChatEvent>` over `/ws/app` with
87
+ taskId-side filtering, capped backoff reconnect, and a per-iterator abort.
88
+ - `tasks.streamReply(taskId)` — `AsyncIterable<StreamReplyDelta>` built on
89
+ subscribe; yields `text` deltas from `reply_preview` plus a terminal `done`
90
+ on the assistant message (or `error` on `task_status_update=failed`).
91
+ - Unified `ConductorAppError` with named error codes mapped from HTTP status
92
+ + backend error strings.
93
+ - Custom fetch / WebSocket / bearerToken providers for SSR + test injection.
94
+
95
+ ### Added — `/react` (M2)
96
+
97
+ - `<ChatView />` — composed widget: runtime status bar + message list + input.
98
+ - `<MessageList />`, `<MessageInput />`, `<RuntimeStatusBar />` —
99
+ building-block components for compose-your-own layouts.
100
+ - `<ChatProvider />` + `useChat()` — React Context + useReducer chat state
101
+ (no zustand dep; per-instance store so multiple widgets on the same page
102
+ don't collide).
103
+ - `createRestAdapter()` — default `ChatAdapter` implementation that talks to
104
+ a host BFF exposing 4 routes (`messages` GET/POST, `interrupt`, `events`
105
+ SSE).
106
+ - Optimistic send → server confirm flow with pending-message replacement.
107
+ - Pre-compiled CSS at `@love-moon/app-sdk/react/styles.css`; CSS-variable
108
+ theming via the `theme` prop; mobile/desktop layout via responsive CSS
109
+ + an explicit `layout` prop override.
110
+ - jsdom integration tests covering hydration, live events, runtime status,
111
+ interrupt button visibility, and optimistic send.
112
+
113
+ ### Added — examples + docs (M3)
114
+
115
+ - `examples/01_example/` — minimal Node CLI demo: bind project → create task
116
+ → stream AI reply to stdout. ~35 lines of business code, no UI / BFF.
117
+ - `examples/02_bff/` — runnable Next.js demo: BFF wraps `/server`,
118
+ page mounts `<ChatView />`, ~120 lines of business code total.
119
+ - `lib/conductor.ts` — singleton AppClient.
120
+ - `app/api/conductor/bind/route.ts` — `projects.bind()` + `tasks.create()`.
121
+ - `app/api/conductor/[...path]/route.ts` — catch-all BFF translating to
122
+ the 4 widget-expected routes, including a 30-line SSE bridge over
123
+ `tasks.subscribe()`.
124
+ - README with quickstart, full integration recipe, and security model.
125
+
126
+ ### Known gaps
127
+
128
+ - v0.1 chat UI is functional but visually minimal. RFC §4 phase B (physical
129
+ extraction of `web/src/features/chat` polished components, including
130
+ Markdown / attachments / 1423-line regression test) is a follow-up PR.
131
+ - Streaming surface yields `text` cumulative previews, not token-level
132
+ deltas. Real token streaming requires a backend envelope addition; the
133
+ `StreamReplyDelta` shape is forward-compatible.
134
+ - Attachments not implemented in the default REST adapter or `<ChatView>`;
135
+ `Attachment` types are defined for forward compat.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # @love-moon/app-sdk
2
+
3
+ Embed Conductor AI tools into third-party apps. One npm package, three
4
+ subpath entries:
5
+
6
+ | Entry | Environment | Purpose |
7
+ | --- | --- | --- |
8
+ | `@love-moon/app-sdk` | any | Pure types + `SDK_VERSION`. Safe everywhere. |
9
+ | `@love-moon/app-sdk/server` | Node 18+ | SDK for third-party **backends** to talk to Conductor REST + `/ws/app`. |
10
+ | `@love-moon/app-sdk/react` | browser / React 18+ | Chat widget (`<ChatView />`) wired by a `ChatAdapter`. |
11
+
12
+ See [RFC 0027](../../claw/rfc/0027-feature-conductor-app-sdk.md) for the design.
13
+
14
+ ## Examples
15
+
16
+ Two runnable examples ship with the package, under [`examples/`](./examples/):
17
+
18
+ | Example | Lines | What it shows |
19
+ | --- | --- | --- |
20
+ | [`examples/01_example/`](./examples/01_example/) | ~35 LOC | **Smallest possible app.** Pure Node CLI: bind project → create task → stream the AI reply to stdout. Use this to learn the SDK in 60 seconds. |
21
+ | [`examples/02_bff/`](./examples/02_bff/) | ~120 LOC | Full-stack: Next.js BFF + React `<ChatView />` widget + SSE bridge over `/ws/app`. Use this as a template for real browser integrations. |
22
+
23
+ ## Quick start
24
+
25
+ A complete integration is ~120 lines of business code spread across a Next.js
26
+ BFF and a React page. See [`examples/02_bff/`](./examples/02_bff/) for the
27
+ full runnable demo, or [`examples/01_example/`](./examples/01_example/) for
28
+ a pure-Node CLI.
29
+
30
+ ### Backend (the only place your Conductor token lives)
31
+
32
+ ```ts
33
+ // lib/conductor.ts
34
+ import { connect } from '@love-moon/app-sdk/server';
35
+
36
+ export const client = await connect({
37
+ baseUrl: 'https://conductor.example.com',
38
+ bearerToken: process.env.CONDUCTOR_TOKEN!,
39
+ });
40
+
41
+ export async function bindProject() {
42
+ return client.projects.bind({
43
+ name: 'Acme Dashboard',
44
+ daemonHost: process.env.CONDUCTOR_DAEMON_HOST!,
45
+ workspacePath: process.env.CONDUCTOR_WORKSPACE_PATH!,
46
+ });
47
+ // Idempotent: matches by (daemonHost, workspacePath); creates only on miss.
48
+ }
49
+
50
+ // Create a task once you've got the project id. `initialMessage` is the
51
+ // kickoff prompt — the AI reply to that message starts streaming as soon
52
+ // as you subscribe below.
53
+ const task = await client.tasks.create({
54
+ projectId: project.id,
55
+ title: 'Investigate billing anomaly',
56
+ initialMessage: 'Look at the last 24h of charges.',
57
+ });
58
+
59
+ // Subscribe to receive the AI's reply (and any subsequent events on the
60
+ // task). Use `sendMessage` to add follow-up turns from your code; the demo
61
+ // here shows a single-turn flow, so we just consume events until the task
62
+ // finishes. To send a follow-up turn before exiting, call
63
+ // `client.tasks.sendMessage(task.id, 'drill into the top one')` and keep
64
+ // the loop running.
65
+ for await (const evt of client.tasks.subscribe(task.id)) {
66
+ if (evt.type === 'message_appended') console.log(evt.message.content);
67
+ if (evt.type === 'task_finished') break;
68
+ }
69
+
70
+ // streamReply: stream the AI's reply as it builds up. Defaults to a 120s
71
+ // idle timeout between deltas; pass `idleTimeoutMs: 0` to disable for
72
+ // long-running backends that may legitimately go silent for minutes.
73
+ for await (const delta of client.tasks.streamReply(task.id, { idleTimeoutMs: 0 })) {
74
+ if (delta.type === 'text') process.stdout.write(delta.text);
75
+ if (delta.type === 'done') break;
76
+ if (delta.type === 'error') throw new Error(delta.error.message);
77
+ }
78
+ ```
79
+
80
+ ### Frontend (chat widget)
81
+
82
+ ```tsx
83
+ import { ChatView, createRestAdapter } from '@love-moon/app-sdk/react';
84
+ import '@love-moon/app-sdk/react/styles.css';
85
+
86
+ const adapter = createRestAdapter({
87
+ baseUrl: '/api/conductor', // your BFF, not Conductor directly
88
+ });
89
+
90
+ export default function ChatPage({ taskId }: { taskId: string }) {
91
+ return <ChatView taskId={taskId} adapter={adapter} />;
92
+ }
93
+ ```
94
+
95
+ ### Backend-for-Frontend (the 4 routes the widget speaks)
96
+
97
+ The widget's default `createRestAdapter` expects:
98
+
99
+ | Route | Forward to |
100
+ | --- | --- |
101
+ | `GET /tasks/:id/messages?pagination=1&limit=N&before_id=...` | `client.tasks.history()` |
102
+ | `POST /tasks/:id/messages` | `client.tasks.sendMessage()` |
103
+ | `POST /tasks/:id/interrupt` | `client.tasks.interrupt()` |
104
+ | `GET /tasks/:id/events` (text/event-stream) | `client.tasks.subscribe()` via SSE bridge |
105
+
106
+ The SSE bridge over Conductor's `/ws/app` is the only non-trivial piece —
107
+ ~30 lines of code. See the
108
+ [example catch-all route](./examples/02_bff/app/api/conductor/[...path]/route.ts)
109
+ for the full pattern.
110
+
111
+ ## Why a single package, three entries
112
+
113
+ The widget and the BFF must agree on wire format and event shapes; coupling
114
+ them in a single package + shared types makes that contract impossible to
115
+ accidentally split. Subpath exports + `peerDependenciesMeta.optional = true`
116
+ ensure server-only consumers don't pay for React, and browsers never see Node
117
+ code. See [Option F in the RFC](../../claw/rfc/0027-feature-conductor-app-sdk.md#proposed-design).
118
+
119
+ ## Security model
120
+
121
+ The token used by `@love-moon/app-sdk/server` is equivalent to the user's
122
+ Conductor account. **Never** put it in a browser bundle or expose it to
123
+ untrusted code. The intended deployment is:
124
+
125
+ ```
126
+ [browser widget] ──▶ [your BFF, holds token] ──▶ [Conductor backend] ──▶ [daemon]
127
+ ```
128
+
129
+ The widget never receives the Conductor token — it talks only to your BFF.
130
+
131
+ ## Status
132
+
133
+ | Milestone | Status |
134
+ | --- | --- |
135
+ | M0 — package skeleton + exports + bundle smoke | ✓ |
136
+ | M1 — `/server` REST + WS subscribe + streamReply + tests | ✓ |
137
+ | M2 — `/react` widget + default REST adapter + integration tests | ✓ |
138
+ | M3 — `examples/01_example` CLI + `examples/02_bff` Next.js demo + this README | ✓ |
139
+
140
+ The v0.1 widget is intentionally minimal in visuals — the eventual physical
141
+ extraction of the polished UI from `web/src/features/chat` (RFC §4) is a
142
+ future PR. The widget's component API + adapter contract are stable now.
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ cd modules/app-sdk
148
+ npm install # via root npm workspaces
149
+ npm run build # tsup + Tailwind copy
150
+ npm test # vitest: unit + integration tests across node + jsdom
151
+ npm run typecheck
152
+ npm run test:bundle # static guard: no Node code in /react, no DOM in /server
153
+ ```
154
+
155
+ ### Project layout
156
+
157
+ ```
158
+ src/
159
+ types/ Pure types — Task / Message / RuntimeStatus / ChatEvent / ChatAdapter / errors
160
+ index.ts Root entry (re-exports types)
161
+ server/ Node-only: connect() / AppClient / projects.bind / tasks.* / ws subscribe / streamReply
162
+ react/ Browser-only: <ChatView />, components, default REST adapter, styles
163
+ test/
164
+ server/ node-env tests (fetcher, projects, tasks, subscribe, streamReply)
165
+ react/ jsdom-env tests (<ChatView /> integration)
166
+ ```
167
+
168
+ ### Versioning policy
169
+
170
+ - 1.x freezes `<ChatView>` props, `AppClient` methods, `ChatAdapter` interface,
171
+ and root type exports.
172
+ - Adding a new `ChatEvent` variant or a new SDK method is a minor bump.
173
+ - Removing or renaming any of the above is a major bump.
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Task: a single AI conversation within a project.
3
+ *
4
+ * Mirrors the shape returned by Conductor's `/api/tasks/[taskId]` REST endpoint
5
+ * (camelCase normalized). All fields are present after a successful read; the
6
+ * `null`-able fields reflect cases where the backend hasn't populated them yet
7
+ * (e.g. a task created via SDK before its daemon picks it up).
8
+ */
9
+ interface Task {
10
+ id: string;
11
+ projectId: string;
12
+ title: string;
13
+ status: TaskStatus;
14
+ /** Backend execution engine ("claude_code" / "codex" / etc.) or null when not selected yet. */
15
+ backendType: string | null;
16
+ /** AI session id assigned by the daemon; null until the daemon attaches. */
17
+ sessionId: string | null;
18
+ sessionFilePath: string | null;
19
+ /** ISO 8601 timestamps. Always present after the task is persisted. */
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ }
23
+ type TaskStatus = 'pending' | 'running' | 'finished' | 'failed' | 'cancelled' | string;
24
+ interface CreateTaskInput {
25
+ projectId: string;
26
+ title: string;
27
+ /**
28
+ * Optional first user message. When provided, the backend persists it as
29
+ * the task's initial message right after creation.
30
+ * Maps to the backend's `initialContent` field; the SDK renames it for
31
+ * symmetry with `sendMessage(taskId, content)`.
32
+ */
33
+ initialMessage?: string;
34
+ /** Backend type override (claude_code / codex / kimi-cli / etc.). */
35
+ backendType?: string;
36
+ /** Free-form metadata; merged with SDK audit fields server-side. */
37
+ metadata?: Record<string, unknown>;
38
+ }
39
+
40
+ /**
41
+ * Project: a workspace binding (daemon + filesystem path) inside which tasks
42
+ * are scoped. Maps to Conductor's Project model.
43
+ *
44
+ * `daemonHost` and `workspacePath` are the binding identity. App-SDK's
45
+ * `projects.bind()` is idempotent on this pair.
46
+ */
47
+ interface Project {
48
+ id: string;
49
+ name: string;
50
+ daemonHost: string | null;
51
+ workspacePath: string | null;
52
+ repoRoot: string | null;
53
+ worktreeBranch: string | null;
54
+ lastCommit: string | null;
55
+ lastCommitAt: string | null;
56
+ fileCount: number | null;
57
+ isDefault: boolean;
58
+ /**
59
+ * True when this project was created via the App SDK (audit hint, derived
60
+ * from `metadata.audit.createdByApp`). Read-only flag for UI affordances.
61
+ */
62
+ createdByApp: boolean;
63
+ createdAt: string;
64
+ updatedAt: string;
65
+ }
66
+ interface BindProjectInput {
67
+ name: string;
68
+ daemonHost: string;
69
+ workspacePath: string;
70
+ /**
71
+ * Optional override for the audit `createdByApp.name` field. Defaults to the
72
+ * `name` argument. Only used when the SDK has to create a new project (no
73
+ * existing binding found).
74
+ */
75
+ appLabel?: string;
76
+ }
77
+
78
+ /**
79
+ * Message: one entry in a task's chat transcript.
80
+ *
81
+ * Roles:
82
+ * - 'user' — message sent by the user (or the SDK on their behalf).
83
+ * - 'sdk' — message sent by an SDK / CLI / app integration (still
84
+ * appears in the chat as a user-side bubble).
85
+ * - 'assistant' — AI reply.
86
+ * - 'system' — system-emitted notice (rare; renders muted).
87
+ *
88
+ * `role` is intentionally open-vocabulary; new backend roles render as
89
+ * a generic bubble.
90
+ */
91
+ interface Message {
92
+ id: string;
93
+ taskId: string;
94
+ role: MessageRole | string;
95
+ content: string;
96
+ metadata: Record<string, unknown> | null;
97
+ attachments: Attachment[];
98
+ createdAt: string;
99
+ }
100
+ type MessageRole = 'user' | 'sdk' | 'assistant' | 'system';
101
+ interface Attachment {
102
+ id: string;
103
+ filename: string;
104
+ mimeType: string;
105
+ sizeBytes: number;
106
+ /** Resolvable URL or relative path the host must turn into a fetchable URL. */
107
+ url: string;
108
+ }
109
+ interface SendMessageInput {
110
+ content: string;
111
+ /** Idempotency key. When omitted, the SDK auto-generates a UUID. */
112
+ clientRequestId?: string;
113
+ metadata?: Record<string, unknown>;
114
+ /** Optional attachments to include with this message; must be pre-uploaded. */
115
+ attachmentIds?: string[];
116
+ /** Role override; defaults to 'sdk'. */
117
+ role?: MessageRole;
118
+ }
119
+
120
+ /**
121
+ * Runtime status pushed by the daemon while a task is in progress.
122
+ * Mirrors `task_runtime_status` envelope on /ws/app.
123
+ */
124
+ interface RuntimeStatus {
125
+ taskId: string;
126
+ /** High-level phase: 'idle' | 'thinking' | 'tool_call' | 'awaiting_user' | 'done'. */
127
+ state: RuntimeState;
128
+ phase?: string | null;
129
+ source?: string | null;
130
+ /** Short status line shown next to the AI avatar ("Reading file X..."). */
131
+ statusLine?: string | null;
132
+ /** Final status line shown when the reply finishes. */
133
+ statusDoneLine?: string | null;
134
+ replyPreview?: string | null;
135
+ replyTo?: string | null;
136
+ replyInProgress?: boolean;
137
+ backend?: string | null;
138
+ threadId?: string | null;
139
+ daemon?: string | null;
140
+ pid?: number | null;
141
+ sessionId?: string | null;
142
+ sessionFilePath?: string | null;
143
+ tokenUsagePercent?: number | null;
144
+ contextUsagePercent?: number | null;
145
+ createdAt?: string | null;
146
+ }
147
+ type RuntimeState = 'idle' | 'thinking' | 'tool_call' | 'awaiting_user' | 'done' | string;
148
+
149
+ /**
150
+ * ChatEvent: the minimal set of events the chat widget needs to render.
151
+ *
152
+ * This is intentionally a small union — the widget should not have to
153
+ * pattern-match dozens of envelope types. The default REST/WS adapter
154
+ * funnels raw `/ws/app` envelopes into ChatEvents; custom adapters can do the
155
+ * same translation against any wire format.
156
+ */
157
+ /**
158
+ * Error shape carried inside `task_failed` ChatEvents and `error`
159
+ * StreamReplyDeltas.
160
+ *
161
+ * `details` and `cause` are optional, free-form passthroughs of the original
162
+ * SDK error's structured fields — useful for surfacing request IDs / server
163
+ * payloads in host UIs without depending on the SDK error class directly.
164
+ */
165
+ interface ChatEventError {
166
+ code: string;
167
+ message: string;
168
+ details?: unknown;
169
+ cause?: unknown;
170
+ }
171
+ type ChatEvent = {
172
+ type: 'message_appended';
173
+ message: Message;
174
+ } | {
175
+ type: 'message_updated';
176
+ message: Message;
177
+ } | {
178
+ type: 'runtime_status';
179
+ status: RuntimeStatus;
180
+ } | {
181
+ type: 'task_finished';
182
+ taskId: string;
183
+ } | {
184
+ type: 'task_failed';
185
+ taskId: string;
186
+ error: ChatEventError;
187
+ } | {
188
+ type: 'connection_state';
189
+ state: 'connected' | 'reconnecting' | 'offline';
190
+ };
191
+ /**
192
+ * StreamReplyDelta: a streaming AI reply chunk yielded by
193
+ * `client.tasks.streamReply(id)`.
194
+ *
195
+ * v1 semantics: each `text` delta is a *cumulative* preview-style chunk
196
+ * (built from `task_runtime_status.reply_preview` rolling state). The final
197
+ * `done` delta carries the full reply Message. A future RFC may add real
198
+ * token-level streaming; the shape is forward-compatible.
199
+ */
200
+ type StreamReplyDelta = {
201
+ type: 'text';
202
+ text: string;
203
+ replyTo: string;
204
+ } | {
205
+ type: 'status';
206
+ status: RuntimeStatus;
207
+ } | {
208
+ type: 'done';
209
+ message: Message;
210
+ } | {
211
+ type: 'error';
212
+ error: ChatEventError;
213
+ };
214
+
215
+ /**
216
+ * ChatAdapter: the contract between the React widget and *something* that
217
+ * can talk to a Conductor-shaped backend. The widget calls these methods;
218
+ * implementations decide how to translate to HTTP / WS / GraphQL / in-process
219
+ * calls.
220
+ *
221
+ * The default implementation `createRestAdapter` (in `/react`) talks to a
222
+ * BFF that mirrors Conductor's REST shape. Hosts using a custom wire format
223
+ * just implement this interface directly.
224
+ */
225
+ interface ChatAdapter {
226
+ fetchHistory(taskId: string, opts?: {
227
+ beforeId?: string;
228
+ limit?: number;
229
+ signal?: AbortSignal;
230
+ }): Promise<{
231
+ messages: Message[];
232
+ hasMoreBefore: boolean;
233
+ oldestMessageId: string | null;
234
+ }>;
235
+ subscribe(taskId: string, handler: (event: ChatEvent) => void): {
236
+ unsubscribe(): void;
237
+ };
238
+ sendMessage(taskId: string, input: SendMessageInput): Promise<Message>;
239
+ interrupt(taskId: string, opts: {
240
+ targetReplyTo: string;
241
+ }): Promise<void>;
242
+ /** Optional. Adapters that don't support attachments may omit. */
243
+ uploadAttachment?(taskId: string, file: File | Blob, opts?: {
244
+ signal?: AbortSignal;
245
+ filename?: string;
246
+ }): Promise<{
247
+ id: string;
248
+ url: string;
249
+ }>;
250
+ }
251
+
252
+ /**
253
+ * ConductorAppError: every error thrown out of the SDK is one of these.
254
+ * Callers can `if (err instanceof ConductorAppError) switch (err.code)`.
255
+ *
256
+ * `code` is open-vocabulary for forward compat; the SDK promises to never
257
+ * remove existing codes, only add new ones, and to bump the minor version
258
+ * when a new code is introduced.
259
+ */
260
+ declare class ConductorAppError extends Error {
261
+ readonly name = "ConductorAppError";
262
+ readonly code: ConductorErrorCode | string;
263
+ readonly status?: number;
264
+ readonly details?: unknown;
265
+ readonly requestId?: string;
266
+ constructor(args: {
267
+ code: ConductorErrorCode | string;
268
+ message: string;
269
+ status?: number;
270
+ details?: unknown;
271
+ requestId?: string;
272
+ cause?: unknown;
273
+ });
274
+ }
275
+ type ConductorErrorCode = 'unauthorized' | 'forbidden' | 'token_revoked' | 'invalid_input' | 'project_not_found' | 'task_not_found' | 'message_not_found' | 'daemon_offline' | 'workspace_path_conflict' | 'binding_validation_failed' | 'task_not_running' | 'task_type_not_messageable' | 'task_type_not_interruptible' | 'task_fire_owner_offline' | 'network_error' | 'timeout' | 'rate_limited' | 'server_error' | 'subscribe_failed' | 'stream_aborted';
276
+ /** Helper: type-guard for callers that don't want `instanceof`. */
277
+ declare function isConductorAppError(error: unknown): error is ConductorAppError;
278
+
279
+ /**
280
+ * @conductor/app-sdk root entry.
281
+ *
282
+ * This entry is environment-agnostic: it re-exports pure types plus a few
283
+ * runtime constants. Safe to import from Node, browsers, Edge runtimes, and
284
+ * React Server Components.
285
+ *
286
+ * Use the subpath entries for runtime code:
287
+ * - `@love-moon/app-sdk/server` — Node SDK for talking to Conductor.
288
+ * - `@love-moon/app-sdk/react` — React chat widget.
289
+ */
290
+
291
+ declare const SDK_VERSION = "0.3.2";
292
+ declare const SDK_NAME = "@love-moon/app-sdk";
293
+ /**
294
+ * User-Agent fragment the SDK sets on every outbound HTTP request.
295
+ * Format: `conductor-app-sdk/<version>`.
296
+ */
297
+ declare const SDK_USER_AGENT = "conductor-app-sdk/0.3.2";
298
+
299
+ export { type Attachment, type BindProjectInput, type ChatAdapter, type ChatEvent, ConductorAppError, type ConductorErrorCode, type CreateTaskInput, type Message, type MessageRole, type Project, type RuntimeState, type RuntimeStatus, SDK_NAME, SDK_USER_AGENT, SDK_VERSION, type SendMessageInput, type StreamReplyDelta, type Task, type TaskStatus, isConductorAppError };
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ // src/types/errors.ts
2
+ var ConductorAppError = class extends Error {
3
+ name = "ConductorAppError";
4
+ code;
5
+ status;
6
+ details;
7
+ requestId;
8
+ constructor(args) {
9
+ super(args.message, args.cause ? { cause: args.cause } : void 0);
10
+ this.code = args.code;
11
+ this.status = args.status;
12
+ this.details = args.details;
13
+ this.requestId = args.requestId;
14
+ }
15
+ };
16
+ function isConductorAppError(error) {
17
+ return typeof error === "object" && error !== null && error.name === "ConductorAppError";
18
+ }
19
+
20
+ // src/index.ts
21
+ var SDK_VERSION = "0.3.2";
22
+ var SDK_NAME = "@love-moon/app-sdk";
23
+ var SDK_USER_AGENT = `conductor-app-sdk/${SDK_VERSION}`;
24
+ export {
25
+ ConductorAppError,
26
+ SDK_NAME,
27
+ SDK_USER_AGENT,
28
+ SDK_VERSION,
29
+ isConductorAppError
30
+ };