@astralform/js 9.2.0 → 9.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.cjs CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  VOICE_POLISH_MODES: () => VOICE_POLISH_MODES,
39
39
  generateId: () => generateId,
40
40
  isEmbeddedResource: () => isEmbeddedResource,
41
+ isToolImageStub: () => isToolImageStub,
41
42
  isToolOutputStub: () => isToolOutputStub,
42
43
  isVoiceLLMMode: () => isVoiceLLMMode,
43
44
  isVoicePolishMode: () => isVoicePolishMode,
@@ -371,6 +372,11 @@ function isToolOutputStub(value) {
371
372
  const v = value;
372
373
  return v.__stub === "tool_output" && typeof v.call_id === "string" && v.call_id.length > 0;
373
374
  }
375
+ function isToolImageStub(value) {
376
+ if (typeof value !== "object" || value === null) return false;
377
+ const v = value;
378
+ return v.__stub === "tool_image" && typeof v.call_id === "string" && v.call_id.length > 0 && typeof v.index === "number" && Number.isInteger(v.index) && v.index >= 0;
379
+ }
374
380
 
375
381
  // src/client.ts
376
382
  var DEFAULT_BASE_URL = "https://api.astralform.ai";
@@ -494,6 +500,79 @@ var AstralformClient = class {
494
500
  `/v1/code/projects/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`
495
501
  );
496
502
  }
503
+ },
504
+ /**
505
+ * Code groups: a user-named container under one repository, for a feature
506
+ * whose work outlives a single session. Its sessions share one plan and one
507
+ * set of notes, so what one writes the others read.
508
+ *
509
+ * A group is a LABEL, not a binding — unlike a task's project, which is
510
+ * written once on the first turn. A task may join, move between, or leave
511
+ * groups at any time, and a deleted group's sessions survive as ungrouped.
512
+ *
513
+ * The server enforces three things worth knowing before you render a
514
+ * failure: a group that is not this user's answers `404` (never `403`, so a
515
+ * group id cannot be probed); filing a task into a group under a DIFFERENT
516
+ * repository than the task's answers `409`; and the group's plan and notes
517
+ * are deleted with the group, while each session keeps its own.
518
+ */
519
+ groups: {
520
+ /**
521
+ * This user's groups, oldest first. Pass a repository to narrow to one
522
+ * repository's — the composer's picker does; the sidebar asks once and
523
+ * nests them itself.
524
+ */
525
+ list: async (repository) => {
526
+ const query = repository ? `?repository=${encodeURIComponent(repository)}` : "";
527
+ const raw = await this.get(`/v1/code/groups${query}`);
528
+ return raw.map((g) => camelizeKeys(g));
529
+ },
530
+ /**
531
+ * Create a group under a repository. The repository goes through the same
532
+ * normaliser the Projects surface uses, so a pasted GitHub URL is accepted
533
+ * and anything that is not `owner/repo` is refused.
534
+ */
535
+ create: async (repository, title) => {
536
+ const raw = await this.post("/v1/code/groups", { repository, title });
537
+ return camelizeKeys(raw);
538
+ },
539
+ /** Rename a group. A group that is not this user's answers `404`. */
540
+ rename: async (groupId, title) => {
541
+ const raw = await this.patch(`/v1/code/groups/${encodeURIComponent(groupId)}`, { title });
542
+ return camelizeKeys(raw);
543
+ },
544
+ /**
545
+ * Delete a group. Its tasks survive and become ungrouped, each keeping its
546
+ * own plan and notes; the group's SHARED documents are deleted with it.
547
+ */
548
+ remove: async (groupId) => {
549
+ await this.del(`/v1/code/groups/${encodeURIComponent(groupId)}`);
550
+ },
551
+ /**
552
+ * File a task into a group. Idempotent — filing an already-filed task
553
+ * again is the same state, not an error. Refused with `409` when the task
554
+ * belongs to a different repository than the group, because it would
555
+ * render nowhere in the sidebar.
556
+ */
557
+ assign: async (groupId, conversationId) => {
558
+ const raw = await this.post(
559
+ `/v1/code/groups/${encodeURIComponent(groupId)}/tasks/${encodeURIComponent(conversationId)}`,
560
+ void 0
561
+ );
562
+ return camelizeKeys(raw);
563
+ },
564
+ /**
565
+ * Take a task out of its group. The task and its documents are untouched.
566
+ *
567
+ * Deliberately not strict about WHICH group the task is in: a task that is
568
+ * in a different one (or in none) already satisfies "this task should not
569
+ * be grouped", so the call clears it rather than refusing.
570
+ */
571
+ unassign: async (groupId, conversationId) => {
572
+ await this.del(
573
+ `/v1/code/groups/${encodeURIComponent(groupId)}/tasks/${encodeURIComponent(conversationId)}`
574
+ );
575
+ }
497
576
  }
498
577
  };
499
578
  if (isApiKeyConfig(config)) {
@@ -694,6 +773,13 @@ var AstralformClient = class {
694
773
  return await response.json();
695
774
  });
696
775
  }
776
+ /** A GET whose body is binary. Read inside the race, for the reason above. */
777
+ async getBlob(path) {
778
+ return this.withDeadline(async (signal) => {
779
+ const response = await this.send("GET", path, void 0, signal);
780
+ return response.blob();
781
+ });
782
+ }
697
783
  async post(path, body) {
698
784
  return this.withDeadline(async (signal) => {
699
785
  const response = await this.send("POST", path, body, signal);
@@ -913,6 +999,7 @@ var AstralformClient = class {
913
999
  const params = new URLSearchParams();
914
1000
  if (jobId) params.set("job_id", jobId);
915
1001
  if (options?.toolOutputs === "stub") params.set("tool_outputs", "stub");
1002
+ if (options?.toolImages === "stub") params.set("tool_images", "stub");
916
1003
  const query = params.toString();
917
1004
  return this.get(
918
1005
  `/v1/conversations/${encodeURIComponent(conversationId)}/events${query ? `?${query}` : ""}`
@@ -927,6 +1014,25 @@ var AstralformClient = class {
927
1014
  );
928
1015
  return res.output;
929
1016
  }
1017
+ /**
1018
+ * The image behind a {@link ToolImageStub}, as a `Blob` of the preview's bytes.
1019
+ *
1020
+ * Takes the stub itself and nothing else — the `job_id` scoping that
1021
+ * `getToolOutput`'s id form warns about cannot be dropped from a value that
1022
+ * carries it. There is deliberately no by-ids form.
1023
+ *
1024
+ * A `Blob` rather than a URL because the route is authenticated: an `<img
1025
+ * src>` cannot send the bearer token, so a browser consumer turns this into
1026
+ * an object URL (`URL.createObjectURL`) and revokes it when the image
1027
+ * unmounts. The response is `private, immutable`, so a repeat fetch of the
1028
+ * same preview is served by the HTTP cache.
1029
+ */
1030
+ async getToolImage(conversationId, stub) {
1031
+ const query = stub.job_id ? `?job_id=${encodeURIComponent(stub.job_id)}` : "";
1032
+ return this.getBlob(
1033
+ `/v1/conversations/${encodeURIComponent(conversationId)}/tool-output/${encodeURIComponent(stub.call_id)}/images/${stub.index}${query}`
1034
+ );
1035
+ }
930
1036
  async submitToolResult(request) {
931
1037
  await this.post("/v1/tool-result", request);
932
1038
  }
@@ -2801,6 +2907,12 @@ var StreamManager = class _StreamManager {
2801
2907
  * Defaults to inline, so this is inert until a consumer opts in.
2802
2908
  */
2803
2909
  this.toolOutputs = "inline";
2910
+ /**
2911
+ * Whether restore asks for tool image previews inline or as fetch handles.
2912
+ * Separate from `toolOutputs` because the server keeps them separate — see
2913
+ * `ToolImageStub` — and on the same one-setting-both-waves rule.
2914
+ */
2915
+ this.toolImages = "inline";
2804
2916
  this._state = "idle";
2805
2917
  this._activeConversationId = null;
2806
2918
  this._backgroundJobs = /* @__PURE__ */ new Map();
@@ -2857,6 +2969,25 @@ var StreamManager = class _StreamManager {
2857
2969
  setToolOutputMode(mode) {
2858
2970
  this.toolOutputs = mode;
2859
2971
  }
2972
+ /**
2973
+ * Ask restore for stubbed image previews, fetched on demand.
2974
+ *
2975
+ * The same caution as `setToolOutputMode`, sharper: a consumer that cannot
2976
+ * resolve one (`isToolImageStub`, `client.getToolImage`) finds an `images`
2977
+ * entry with no `url` and draws a broken image where the preview was.
2978
+ */
2979
+ setToolImageMode(mode) {
2980
+ this.toolImages = mode;
2981
+ }
2982
+ /**
2983
+ * The ONE options object both event waves fetch with — the newest page and
2984
+ * every `loadEarlierTurns` page. Built in one place so an opt-in added here
2985
+ * reaches both by construction: two literals at two call sites is how one
2986
+ * wave would silently keep asking for inline payloads.
2987
+ */
2988
+ get eventFetchOptions() {
2989
+ return { toolOutputs: this.toolOutputs, toolImages: this.toolImages };
2990
+ }
2860
2991
  emit(event) {
2861
2992
  for (const handler of this.handlers) {
2862
2993
  try {
@@ -3357,9 +3488,7 @@ var StreamManager = class _StreamManager {
3357
3488
  const boundary = this.oldestDrawnMessageId;
3358
3489
  const eventLists = await Promise.all(
3359
3490
  page.jobs.map(
3360
- (job) => session.client.getConversationEvents(conversationId, job.job_id, {
3361
- toolOutputs: this.toolOutputs
3362
- }).catch(() => [])
3491
+ (job) => session.client.getConversationEvents(conversationId, job.job_id, this.eventFetchOptions).catch(() => [])
3363
3492
  )
3364
3493
  );
3365
3494
  if (stop()) return 0;
@@ -3480,9 +3609,7 @@ var StreamManager = class _StreamManager {
3480
3609
  });
3481
3610
  const eventLists = await Promise.all(
3482
3611
  replayableJobs.map(
3483
- (job) => this.session.client.getConversationEvents(conversationId, job.job_id, {
3484
- toolOutputs: this.toolOutputs
3485
- }).catch(() => [])
3612
+ (job) => this.session.client.getConversationEvents(conversationId, job.job_id, this.eventFetchOptions).catch(() => [])
3486
3613
  )
3487
3614
  );
3488
3615
  if (stopReplay()) return false;
@@ -3631,6 +3758,7 @@ function parseEmbeddedResource(value) {
3631
3758
  VOICE_POLISH_MODES,
3632
3759
  generateId,
3633
3760
  isEmbeddedResource,
3761
+ isToolImageStub,
3634
3762
  isToolOutputStub,
3635
3763
  isVoiceLLMMode,
3636
3764
  isVoicePolishMode,