@jxsuite/protocol 2.5.0 → 2.6.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Kevin Shenk
3
+ Copyright (c) 2026 Avunu LLC and Jx Suite contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/protocol",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "The Studio Backend Protocol — wire types and the canonical route table every Jx Studio backend implements",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,6 @@
27
27
  "test:coverage": "bun test --isolate --coverage"
28
28
  },
29
29
  "dependencies": {
30
- "@jxsuite/schema": "^2.3.0"
30
+ "@jxsuite/schema": "^3.0.0"
31
31
  }
32
32
  }
package/src/problems.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * declared once, exported as data, rendered into the docs by the same generator, and checked by the
7
7
  * same drift machinery.
8
8
  *
9
- * **What belongs here.** A type is a *class* of failure a client might handle differently — not a
9
+ * **What belongs here.** A type is a _class_ of failure a client might handle differently — not a
10
10
  * message. "The project root was refused" is a type; "root /x/y was refused" is a `detail`. If two
11
11
  * entries would only ever differ in their wording, they are one type.
12
12
  *
@@ -78,7 +78,18 @@ export const PROBLEM_TYPES = {
78
78
  403,
79
79
  "The path resolves outside the active project",
80
80
  ),
81
-
81
+ /*
82
+ * Refused for a reason the user can act on (desktop.md §10.4): a role, a policy, a plan, a quota.
83
+ * Its own type rather than a `forbidden` with a code, by the same test as
84
+ * `needsInstallationAccess`: the client does something different with it — it offers the
85
+ * `actions` the backend listed, under the backend's `heading`, and follows `retry` once one is
86
+ * done. Nothing here says WHY; that is the backend's, and so is every word the user reads.
87
+ */
88
+ actionRequired: problem("action-required", 403, "An action is required first", [
89
+ "actions",
90
+ "heading",
91
+ "retry",
92
+ ]),
82
93
  // ─── Backend state ────────────────────────────────────────────────────────
83
94
  noActiveProject: problem("no-active-project", 409, "No project is active on this backend"),
84
95
  capabilityUnavailable: problem(
package/src/routes.ts CHANGED
@@ -110,8 +110,8 @@ export const STUDIO_ROUTES = {
110
110
  files: route(
111
111
  "GET",
112
112
  "/__studio/files",
113
- "List a directory (DirEntry[]) in stable path order; with ?glob=<pattern>, search matching " +
114
- "files project-wide",
113
+ "List a directory (DirEntry[]) in stable path order; an entry may carry an opaque content " +
114
+ "`version` the backend vouches for; with ?glob=<pattern>, search matching files project-wide",
115
115
  ),
116
116
  fileRead: route("GET", "/__studio/file", "Read a file's text content"),
117
117
  /**
package/src/types.ts CHANGED
@@ -41,6 +41,13 @@ export interface DirEntry {
41
41
  type: "file" | "directory";
42
42
  size?: number;
43
43
  modified?: string;
44
+ /**
45
+ * An opaque version of the file's bytes: two equal versions mean identical bytes. ABSENT whenever
46
+ * the backend cannot vouch for the bytes — an uncommitted, dirty or just-uploaded file, and every
47
+ * directory. A client may append it to the file's URL (`?v=<version>`) so a host that recognizes
48
+ * it can answer with an immutable cache lifetime; a backend with no such notion never sets it.
49
+ */
50
+ version?: string;
44
51
  }
45
52
 
46
53
  /** A filesystem change pushed from the backend (project-relative, forward-slashed path). */
@@ -812,10 +819,47 @@ export interface AiModelsResponse {
812
819
  * true`: Cloudflare being briefly unreachable is not a reason to send someone round an OAuth flow
813
820
  * that fixes nothing. `cf_account_required` is a live grant with no account chosen (ai.md §2.1):
814
821
  * re-authorizing lands back in the same state, so it calls for the account picker instead.
822
+ *
823
+ * `action_required` is the fourth, and no connection fixes it: the backend requires something of
824
+ * the user before the assistant will run (desktop.md §10.4, ai.md §2.1). It arrives with
825
+ * {@link detail} and {@link actions}, the same members the `action-required` problem carries, so
826
+ * the gate can offer what the backend offers in the backend's own words.
815
827
  */
816
- code?: "cf_not_connected" | "cf_reconnect_required" | "cf_account_required" | "cf_upstream_error";
828
+ code?:
829
+ | "cf_not_connected"
830
+ | "cf_reconnect_required"
831
+ | "cf_account_required"
832
+ | "cf_upstream_error"
833
+ | "action_required";
834
+ /** Why the backend refuses, in its own words. Sent with `action_required`. */
835
+ detail?: string;
836
+ /** What the user can do about it. Sent with `action_required`. */
837
+ actions?: OfferedAction[];
838
+ }
839
+
840
+ // ─── Backend-directed access (desktop.md §10.4) ──────────────────────────────
841
+
842
+ /**
843
+ * Something the user can do, offered by the backend: on an `action-required` refusal, a notice, or
844
+ * an account row. The backend decides what it means; the platform adapter performs it.
845
+ */
846
+ export interface OfferedAction {
847
+ /** Stable within the offer — for the backend's bookkeeping and a test's selector. */
848
+ id: string;
849
+ /** The button's words. */
850
+ label: string;
851
+ /** A page the platform opens for the user. Absent: the platform performs it by `id`. */
852
+ href?: string;
853
+ /** The one action a surface leads with. */
854
+ primary?: boolean;
817
855
  }
818
856
 
857
+ /**
858
+ * What the client does once an offered action is done: `reload` the page (the refusal broke the
859
+ * session), `repeat` (the user runs what was refused again), or `none`.
860
+ */
861
+ export type RetryHint = "reload" | "repeat" | "none";
862
+
819
863
  // ─── Cloudflare publish surface ──────────────────────────────────────────────
820
864
 
821
865
  /**