@astralform/js 7.2.0 → 7.4.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/dist/index.js CHANGED
@@ -194,12 +194,13 @@ function createRateLimitErrorFromHttp(response, rawText) {
194
194
 
195
195
  // src/streaming.ts
196
196
  async function* streamJobSSE(options) {
197
- const { url, headers, signal, fetchFn } = options;
197
+ const { url, headers, signal, fetchFn, method = "GET", body } = options;
198
198
  let response;
199
199
  try {
200
200
  response = await fetchFn(url, {
201
- method: "GET",
201
+ method,
202
202
  headers,
203
+ body,
203
204
  signal
204
205
  });
205
206
  } catch (err) {
@@ -261,6 +262,57 @@ async function* streamJobSSE(options) {
261
262
  }
262
263
  }
263
264
 
265
+ // src/types.ts
266
+ var ChatEventType = {
267
+ // Connection lifecycle (SDK-local, not wire)
268
+ Connected: "connected",
269
+ Disconnected: "disconnected",
270
+ // Turn lifecycle
271
+ MessageStart: "message_start",
272
+ MessageStop: "message_stop",
273
+ // Block lifecycle
274
+ BlockStart: "block_start",
275
+ BlockDelta: "block_delta",
276
+ BlockStop: "block_stop",
277
+ // Reliability
278
+ Stall: "stall",
279
+ Retry: "retry",
280
+ Error: "error",
281
+ Keepalive: "keepalive",
282
+ // Conversation-level (typed custom events)
283
+ UserMessage: "user_message",
284
+ TitleGenerated: "title_generated",
285
+ TodoUpdate: "todo_update",
286
+ PlanUpdate: "plan_update",
287
+ NoteUpdate: "note_update",
288
+ ContextUpdate: "context_update",
289
+ SubagentStart: "subagent_start",
290
+ SubagentStop: "subagent_stop",
291
+ ContextWarning: "context_warning",
292
+ MemoryRecall: "memory_recall",
293
+ MemoryUpdate: "memory_update",
294
+ DesktopStream: "desktop_stream",
295
+ AttachmentStaged: "attachment_staged",
296
+ WorkspaceReady: "workspace_ready",
297
+ AssetCreated: "asset_created",
298
+ ToolApprovalRequested: "tool_approval_requested",
299
+ ToolApprovalGranted: "tool_approval_granted",
300
+ ToolPermissionDenied: "tool_permission_denied",
301
+ ToolHarnessWarning: "tool_harness_warning",
302
+ UserUnavailable: "user_unavailable",
303
+ PromptSuggestion: "prompt_suggestion",
304
+ StateChanged: "state_changed",
305
+ // Generic fallthrough for unknown custom events
306
+ Custom: "custom"
307
+ };
308
+ var VOICE_POLISH_MODES = ["raw", "light", "structured", "formal"];
309
+ function isVoicePolishMode(value) {
310
+ return typeof value === "string" && VOICE_POLISH_MODES.includes(value);
311
+ }
312
+ function isVoiceLLMMode(mode) {
313
+ return mode !== "raw";
314
+ }
315
+
264
316
  // src/client.ts
265
317
  var DEFAULT_BASE_URL = "https://api.astralform.ai";
266
318
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -286,6 +338,69 @@ function isApiKeyConfig(config) {
286
338
  }
287
339
  var AstralformClient = class {
288
340
  constructor(config) {
341
+ // --- Code mode: the app user's projects ---
342
+ /**
343
+ * The projects (GitHub repositories) this app user works with, and what they
344
+ * may add.
345
+ *
346
+ * A project list is per app user within a code-mode agent: the developer
347
+ * connects the workspace's GitHub account, and each user curates their own
348
+ * list from what that connection covers. Every method 404s on a chat-mode
349
+ * agent, so the surface is invisible rather than empty there.
350
+ */
351
+ this.code = {
352
+ projects: {
353
+ /** This user's projects on the active agent, oldest first. */
354
+ list: async () => {
355
+ const raw = await this.get(
356
+ "/v1/code/projects"
357
+ );
358
+ return raw.map((p) => camelizeKeys(p));
359
+ },
360
+ /**
361
+ * What the workspace's GitHub installations cover, minus what this user
362
+ * has already added. Read `state` before the list: an empty `repositories`
363
+ * means something different in each of its three values.
364
+ */
365
+ available: async () => {
366
+ const raw = await this.get("/v1/code/projects/available");
367
+ return {
368
+ state: raw.state,
369
+ repositories: (raw.repositories ?? []).map((r) => ({
370
+ fullName: r.full_name,
371
+ private: r.private
372
+ })),
373
+ // Defaulted like its neighbours: the `unavailable` branch has nothing
374
+ // to count, and an absent field behind a `number` type prints
375
+ // "undefined" in a picker rather than a number.
376
+ totalCount: raw.total_count ?? 0,
377
+ partial: raw.partial ?? false
378
+ };
379
+ },
380
+ /**
381
+ * Add a repository. The server checks it against the workspace's own
382
+ * installations and answers a repository it cannot reach the same way it
383
+ * answers one owned by someone else — deliberately, so this call cannot be
384
+ * used to discover which organisations use Astralform.
385
+ */
386
+ add: async (repoFullName) => {
387
+ const raw = await this.post(
388
+ "/v1/code/projects",
389
+ { repo_full_name: repoFullName }
390
+ );
391
+ return camelizeKeys(raw);
392
+ },
393
+ /**
394
+ * Remove a project. Tasks already bound to that repository keep their
395
+ * binding — they simply stop grouping under it.
396
+ */
397
+ remove: async (owner, repo) => {
398
+ await this.del(
399
+ `/v1/code/projects/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`
400
+ );
401
+ }
402
+ }
403
+ };
289
404
  if (isApiKeyConfig(config)) {
290
405
  if (!config.apiKey || typeof config.apiKey !== "string") {
291
406
  throw new Error("apiKey is required and must be a non-empty string");
@@ -526,10 +641,18 @@ var AstralformClient = class {
526
641
  }
527
642
  };
528
643
  }
529
- async getConversations(limit = 50, offset = 0) {
644
+ /**
645
+ * A page of conversations, newest-updated first.
646
+ *
647
+ * `options.repository` narrows to one project's tasks (`owner/repo`) on a
648
+ * code-mode agent — the same paging applies within the filter, so a client
649
+ * showing tasks per project pages each project separately.
650
+ */
651
+ async getConversations(limit = 50, offset = 0, options) {
530
652
  const safeLimit = Math.max(1, Math.min(200, Math.floor(Number(limit))));
531
653
  const safeOffset = Math.max(0, Math.floor(Number(offset)));
532
- const raw = await this.get(`/v1/conversations?limit=${safeLimit}&offset=${safeOffset}`);
654
+ const filter = options?.repository ? `&repository=${encodeURIComponent(options.repository)}` : "";
655
+ const raw = await this.get(`/v1/conversations?limit=${safeLimit}&offset=${safeOffset}${filter}`);
533
656
  return raw.map((c) => camelizeKeys(c));
534
657
  }
535
658
  async getMessages(conversationId) {
@@ -689,6 +812,94 @@ var AstralformClient = class {
689
812
  const raw = await response.json();
690
813
  return this.mapAsset(raw);
691
814
  }
815
+ // --- Voice input ---
816
+ /** The agent's voice-input defaults (`GET /v1/voice/config`). */
817
+ async getVoiceConfig() {
818
+ const raw = await this.get("/v1/voice/config");
819
+ return {
820
+ enabled: Boolean(raw.enabled),
821
+ modes: raw.modes ?? [...VOICE_POLISH_MODES],
822
+ // A mode this SDK does not know must not reach a `switch` typed as
823
+ // `VoicePolishMode`; `structured` is the server's own default.
824
+ defaultMode: isVoicePolishMode(raw.default_mode) ? raw.default_mode : "structured",
825
+ silenceAutoStopSeconds: raw.silence_auto_stop_seconds ?? 2,
826
+ autoSend: raw.auto_send ?? true,
827
+ maxRecordingSeconds: raw.max_recording_seconds ?? 300,
828
+ supportsStreaming: Boolean(raw.supports_streaming),
829
+ hotwords: raw.hotwords ?? []
830
+ };
831
+ }
832
+ /**
833
+ * Transcribe one recording with the agent's configured speech-to-text
834
+ * provider (`POST /v1/voice/transcriptions`). 16 kHz mono 16-bit WAV is the
835
+ * reference format; anything the provider accepts works.
836
+ *
837
+ * Deliberately outside `withDeadline`: a recording can run to
838
+ * `VoiceConfig.maxRecordingSeconds`, so the 30 s default would cut real
839
+ * uploads off. Pass `options.signal` to give up on a stalled one; the
840
+ * promise then rejects with the abort reason — the runtime's `AbortError`,
841
+ * or whatever was passed to `abort(reason)`.
842
+ */
843
+ async transcribeVoice(audio, options = {}) {
844
+ const formData = new FormData();
845
+ formData.append("file", audio, options.filename ?? "recording.wav");
846
+ if (options.hotwords?.length) {
847
+ formData.append("hotwords", options.hotwords.join(", "));
848
+ }
849
+ if (options.language) {
850
+ formData.append("language", options.language);
851
+ }
852
+ const response = await this.fetchFn(`${this.baseURL}/v1/voice/transcriptions`, {
853
+ method: "POST",
854
+ headers: this.authHeaders,
855
+ body: formData,
856
+ signal: options.signal
857
+ }).catch((err) => {
858
+ if (options.signal?.aborted) {
859
+ throw err;
860
+ }
861
+ throw new ConnectionError(
862
+ err instanceof Error ? err.message : "Failed to connect"
863
+ );
864
+ });
865
+ await this.handleError(response);
866
+ const raw = await response.json();
867
+ return {
868
+ text: raw.text ?? "",
869
+ language: raw.language ?? null,
870
+ durationMs: raw.duration_ms ?? null,
871
+ asrMs: raw.asr_ms ?? 0
872
+ };
873
+ }
874
+ /**
875
+ * Stream the LLM rewrite of a transcript (`POST /v1/voice/polish`) as typed
876
+ * frames.
877
+ *
878
+ * Failures the server reports mid-stream arrive as an `error` frame, but
879
+ * the iteration itself can reject: aborting `signal` closes the connection
880
+ * (which cancels the model call upstream) and rejects with
881
+ * `StreamAbortedError`; a non-2xx open rejects with `AuthenticationError`,
882
+ * `RateLimitError` or `ServerError`; a network failure with
883
+ * `ConnectionError`. Wrap the `for await` accordingly.
884
+ */
885
+ async *streamVoicePolish(request, options = {}) {
886
+ const frames = streamJobSSE({
887
+ url: `${this.baseURL}/v1/voice/polish`,
888
+ headers: { ...this.headers, Accept: "text/event-stream" },
889
+ method: "POST",
890
+ body: JSON.stringify({
891
+ text: request.text,
892
+ mode: request.mode,
893
+ hotwords: request.hotwords ?? []
894
+ }),
895
+ signal: options.signal,
896
+ fetchFn: this.fetchFn
897
+ });
898
+ for await (const frame of frames) {
899
+ const event = parseVoicePolishFrame(frame);
900
+ if (event) yield event;
901
+ }
902
+ }
692
903
  async listUploads(conversationId) {
693
904
  const raw = await this.get(
694
905
  `/v1/conversations/${encodeURIComponent(conversationId)}/uploads`
@@ -775,6 +986,35 @@ var AstralformClient = class {
775
986
  }));
776
987
  }
777
988
  };
989
+ function parseVoicePolishFrame(frame) {
990
+ let payload;
991
+ try {
992
+ const parsed = JSON.parse(frame.data);
993
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
994
+ payload = parsed;
995
+ } catch {
996
+ return null;
997
+ }
998
+ switch (frame.event) {
999
+ case "delta":
1000
+ return typeof payload.text === "string" ? { type: "delta", text: payload.text } : null;
1001
+ case "done":
1002
+ return typeof payload.text === "string" ? {
1003
+ type: "done",
1004
+ text: payload.text,
1005
+ polishMs: payload.polish_ms ?? 0
1006
+ } : null;
1007
+ case "error":
1008
+ return {
1009
+ type: "error",
1010
+ reason: payload.reason ?? "unknown",
1011
+ partial: payload.partial ?? "",
1012
+ ...typeof payload.detail === "string" ? { detail: payload.detail } : {}
1013
+ };
1014
+ default:
1015
+ return null;
1016
+ }
1017
+ }
778
1018
 
779
1019
  // src/storage.ts
780
1020
  var InMemoryStorage = class {
@@ -1507,6 +1747,9 @@ var ChatSession = class {
1507
1747
  image_mode: options?.imageMode,
1508
1748
  video_mode: options?.videoMode,
1509
1749
  goal: options?.goal,
1750
+ // The project this task belongs to, on a code-mode agent. Write-once
1751
+ // server-side: sent on every turn, honoured on the first.
1752
+ repository: options?.repository,
1510
1753
  // Per-request model choice (client-side model selection).
1511
1754
  provider: options?.provider,
1512
1755
  model: options?.model,
@@ -2259,50 +2502,6 @@ function planRestore(args) {
2259
2502
  return steps;
2260
2503
  }
2261
2504
 
2262
- // src/types.ts
2263
- var ChatEventType = {
2264
- // Connection lifecycle (SDK-local, not wire)
2265
- Connected: "connected",
2266
- Disconnected: "disconnected",
2267
- // Turn lifecycle
2268
- MessageStart: "message_start",
2269
- MessageStop: "message_stop",
2270
- // Block lifecycle
2271
- BlockStart: "block_start",
2272
- BlockDelta: "block_delta",
2273
- BlockStop: "block_stop",
2274
- // Reliability
2275
- Stall: "stall",
2276
- Retry: "retry",
2277
- Error: "error",
2278
- Keepalive: "keepalive",
2279
- // Conversation-level (typed custom events)
2280
- UserMessage: "user_message",
2281
- TitleGenerated: "title_generated",
2282
- TodoUpdate: "todo_update",
2283
- PlanUpdate: "plan_update",
2284
- NoteUpdate: "note_update",
2285
- ContextUpdate: "context_update",
2286
- SubagentStart: "subagent_start",
2287
- SubagentStop: "subagent_stop",
2288
- ContextWarning: "context_warning",
2289
- MemoryRecall: "memory_recall",
2290
- MemoryUpdate: "memory_update",
2291
- DesktopStream: "desktop_stream",
2292
- AttachmentStaged: "attachment_staged",
2293
- WorkspaceReady: "workspace_ready",
2294
- AssetCreated: "asset_created",
2295
- ToolApprovalRequested: "tool_approval_requested",
2296
- ToolApprovalGranted: "tool_approval_granted",
2297
- ToolPermissionDenied: "tool_permission_denied",
2298
- ToolHarnessWarning: "tool_harness_warning",
2299
- UserUnavailable: "user_unavailable",
2300
- PromptSuggestion: "prompt_suggestion",
2301
- StateChanged: "state_changed",
2302
- // Generic fallthrough for unknown custom events
2303
- Custom: "custom"
2304
- };
2305
-
2306
2505
  // src/stream-manager.ts
2307
2506
  var StreamManager = class {
2308
2507
  constructor(session) {
@@ -2956,10 +3155,14 @@ export {
2956
3155
  StreamAbortedError,
2957
3156
  StreamManager,
2958
3157
  ToolRegistry,
3158
+ VOICE_POLISH_MODES,
2959
3159
  generateId,
2960
3160
  isEmbeddedResource,
3161
+ isVoiceLLMMode,
3162
+ isVoicePolishMode,
2961
3163
  mapSseToChat,
2962
3164
  parseEmbeddedResource,
3165
+ parseVoicePolishFrame,
2963
3166
  replayEvents,
2964
3167
  streamJobSSE,
2965
3168
  translateDelta