@huynle/hub 0.7.3 → 0.8.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.
Files changed (221) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/Dockerfile +7 -2
  3. package/README.md +153 -24
  4. package/package.json +5 -3
  5. package/packages/claude-code/package.json +1 -1
  6. package/packages/claude-code/src/daemon.ts +8 -1
  7. package/packages/claude-code/src/launcher.ts +29 -9
  8. package/packages/claude-code/src/profile.ts +173 -0
  9. package/packages/claude-code/src/sessions.ts +22 -4
  10. package/packages/cli/dist/hub-cli.js +6474 -3470
  11. package/packages/cli/package.json +1 -1
  12. package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
  13. package/packages/cli/src/artifacts-mcp.ts +160 -0
  14. package/packages/cli/src/bootstrap/enrollment/enroll.ts +20 -8
  15. package/packages/cli/src/bootstrap/enrollment/policy.ts +125 -12
  16. package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
  17. package/packages/cli/src/cli.ts +95 -13
  18. package/packages/cli/src/format.ts +48 -10
  19. package/packages/cli/src/http.ts +1 -0
  20. package/packages/cli/src/intents.ts +23 -0
  21. package/packages/cli/src/login.ts +20 -6
  22. package/packages/cli/src/mcp.ts +380 -9
  23. package/packages/cli/src/messaging.ts +3 -1
  24. package/packages/cli/src/setup/commands.ts +3 -2
  25. package/packages/cli/src/setup/runtime.ts +3 -2
  26. package/packages/cli/src/team-intake-mcp.ts +158 -0
  27. package/packages/cli/src/teams-mcp.ts +403 -0
  28. package/packages/cli/src/watchlist.ts +135 -0
  29. package/packages/codex/package.json +1 -1
  30. package/packages/codex/src/profile.ts +163 -0
  31. package/packages/codex/src/sessions.ts +12 -0
  32. package/packages/codex/src/spoke.ts +19 -1
  33. package/packages/node/package.json +1 -1
  34. package/packages/node/src/config.ts +96 -2
  35. package/packages/node/src/opencode-warm.ts +325 -0
  36. package/packages/node/src/run.ts +27 -1
  37. package/packages/preset/package.json +1 -1
  38. package/packages/preset/src/profile.ts +34 -0
  39. package/packages/preset/src/spoke.ts +4 -0
  40. package/packages/protocol/package.json +1 -1
  41. package/packages/protocol/src/artifacts.ts +142 -0
  42. package/packages/protocol/src/concierge.ts +366 -0
  43. package/packages/protocol/src/contracts.ts +104 -0
  44. package/packages/protocol/src/graph.ts +70 -0
  45. package/packages/protocol/src/hub-workspace.ts +201 -0
  46. package/packages/protocol/src/index.ts +169 -6
  47. package/packages/protocol/src/launch.ts +22 -0
  48. package/packages/protocol/src/profiles.ts +301 -0
  49. package/packages/protocol/src/teams.ts +204 -0
  50. package/packages/runtime-openshell/package.json +1 -1
  51. package/packages/runtime-sdk/package.json +1 -1
  52. package/packages/sdk/package.json +1 -1
  53. package/packages/sdk/src/address.ts +50 -2
  54. package/packages/sdk/src/artifact-jobs.ts +194 -0
  55. package/packages/sdk/src/artifacts.ts +187 -0
  56. package/packages/sdk/src/client.ts +226 -5
  57. package/packages/sdk/src/concierge.ts +88 -0
  58. package/packages/sdk/src/errors.ts +3 -1
  59. package/packages/sdk/src/index.ts +5 -0
  60. package/packages/sdk/src/team-intake.ts +122 -0
  61. package/packages/sdk/src/teams.ts +346 -0
  62. package/packages/sdk/src/types.ts +548 -1
  63. package/packages/spoke-sdk/package.json +1 -1
  64. package/packages/spoke-sdk/src/index.ts +1 -0
  65. package/packages/spoke-sdk/src/profile-apply.ts +245 -0
  66. package/src/central/actions.ts +7 -0
  67. package/src/central/agent/acting-routes.ts +2 -1
  68. package/src/central/agent/acting-switch.ts +2 -1
  69. package/src/central/agent/acting.ts +215 -10
  70. package/src/central/agent/app-tokens.ts +3 -2
  71. package/src/central/agent/audit.ts +17 -0
  72. package/src/central/agent/body.ts +163 -0
  73. package/src/central/agent/decisions.ts +69 -2
  74. package/src/central/agent/delivery.ts +95 -15
  75. package/src/central/agent/directory-routes.ts +37 -7
  76. package/src/central/agent/directory.ts +70 -14
  77. package/src/central/agent/errors.ts +5 -0
  78. package/src/central/agent/events-routes.ts +21 -4
  79. package/src/central/agent/events.ts +11 -3
  80. package/src/central/agent/form-check.ts +85 -0
  81. package/src/central/agent/grant-requests.ts +2 -1
  82. package/src/central/agent/grant-routes.ts +4 -3
  83. package/src/central/agent/grants.ts +29 -2
  84. package/src/central/agent/idempotency.ts +133 -0
  85. package/src/central/agent/intent-routes.ts +116 -0
  86. package/src/central/agent/intents.ts +505 -0
  87. package/src/central/agent/interaction-graph-routes.ts +212 -0
  88. package/src/central/agent/interaction-graph.ts +865 -0
  89. package/src/central/agent/launch-engine.ts +16 -2
  90. package/src/central/agent/launch-routes.ts +6 -1
  91. package/src/central/agent/launch-spec.ts +14 -1
  92. package/src/central/agent/message-routes.ts +2 -1
  93. package/src/central/agent/messages.ts +27 -2
  94. package/src/central/agent/names.ts +12 -2
  95. package/src/central/agent/read-routes.ts +86 -19
  96. package/src/central/agent/routes.ts +176 -5
  97. package/src/central/agent/webhook-routes.ts +2 -1
  98. package/src/central/agent-data.ts +4 -0
  99. package/src/central/artifacts/access.ts +44 -0
  100. package/src/central/artifacts/blob-store.ts +113 -0
  101. package/src/central/artifacts/blob-support.ts +98 -0
  102. package/src/central/artifacts/events.ts +27 -0
  103. package/src/central/artifacts/host-caps.ts +162 -0
  104. package/src/central/artifacts/host.ts +315 -0
  105. package/src/central/artifacts/idempotency.ts +109 -0
  106. package/src/central/artifacts/jobs-routes.ts +773 -0
  107. package/src/central/artifacts/jobs.ts +344 -0
  108. package/src/central/artifacts/maintenance.ts +103 -0
  109. package/src/central/artifacts/routes.ts +360 -0
  110. package/src/central/artifacts/store.ts +776 -0
  111. package/src/central/artifacts/workspaces.ts +31 -0
  112. package/src/central/backend/file-blobs.ts +377 -0
  113. package/src/central/backend/index.ts +12 -3
  114. package/src/central/backend/memory-blobs.ts +116 -0
  115. package/src/central/backend/postgres-blobs.ts +513 -0
  116. package/src/central/backend/postgres-migrate.ts +33 -0
  117. package/src/central/backend/postgres-state.ts +19 -0
  118. package/src/central/backend/types.ts +8 -0
  119. package/src/central/concierge/activity.ts +142 -0
  120. package/src/central/concierge/arm.ts +63 -0
  121. package/src/central/concierge/board-render.ts +190 -0
  122. package/src/central/concierge/board-results.ts +169 -0
  123. package/src/central/concierge/board-routes.ts +339 -0
  124. package/src/central/concierge/board.ts +249 -0
  125. package/src/central/concierge/context.ts +221 -0
  126. package/src/central/concierge/entitlements.ts +13 -0
  127. package/src/central/concierge/guard.ts +120 -0
  128. package/src/central/concierge/inbound.ts +89 -0
  129. package/src/central/concierge/index.ts +84 -0
  130. package/src/central/concierge/notes.ts +41 -0
  131. package/src/central/concierge/notifier.ts +986 -0
  132. package/src/central/concierge/prompt.ts +130 -0
  133. package/src/central/concierge/proposals.ts +262 -0
  134. package/src/central/concierge/push-remap.ts +49 -0
  135. package/src/central/concierge/routes.ts +62 -0
  136. package/src/central/concierge/schedule.ts +113 -0
  137. package/src/central/concierge/service.ts +243 -0
  138. package/src/central/concierge/session-routes.ts +69 -0
  139. package/src/central/concierge/settings.ts +217 -0
  140. package/src/central/concierge/supervisor.ts +740 -0
  141. package/src/central/concierge/tiers.ts +260 -0
  142. package/src/central/concierge/watchlist-routes.ts +277 -0
  143. package/src/central/concierge/watchlist.ts +273 -0
  144. package/src/central/config.ts +7 -1
  145. package/src/central/http.ts +633 -50
  146. package/src/central/mcp-http.ts +10 -21
  147. package/src/central/profiles/apply.ts +333 -0
  148. package/src/central/profiles/content.ts +174 -0
  149. package/src/central/profiles/events.ts +40 -0
  150. package/src/central/profiles/idempotency.ts +14 -0
  151. package/src/central/profiles/import.ts +139 -0
  152. package/src/central/profiles/routes.ts +356 -0
  153. package/src/central/profiles/store.ts +597 -0
  154. package/src/central/profiles/usage-ports.ts +120 -0
  155. package/src/central/profiles/usage.ts +364 -0
  156. package/src/central/push.ts +7 -0
  157. package/src/central/registry.ts +43 -6
  158. package/src/central/store.ts +4 -0
  159. package/src/central/teams/collections.ts +34 -0
  160. package/src/central/teams/discovery.ts +346 -0
  161. package/src/central/teams/draft-edits.ts +46 -0
  162. package/src/central/teams/events.ts +46 -0
  163. package/src/central/teams/intake-routes.ts +100 -0
  164. package/src/central/teams/intake.ts +772 -0
  165. package/src/central/teams/policy.ts +231 -0
  166. package/src/central/teams/routes.ts +516 -0
  167. package/src/central/teams/seats.ts +272 -0
  168. package/src/central/teams/staffing.ts +888 -0
  169. package/src/central/teams/store.ts +667 -0
  170. package/src/central/teams/validation.ts +256 -0
  171. package/src/central/workspaces/board.ts +408 -0
  172. package/src/central/workspaces/checkpoints.ts +316 -0
  173. package/src/central/workspaces/coordinator.ts +841 -0
  174. package/src/central/workspaces/events.ts +85 -0
  175. package/src/central/workspaces/lifecycle.ts +93 -0
  176. package/src/central/workspaces/master.ts +490 -0
  177. package/src/central/workspaces/members.ts +632 -0
  178. package/src/central/workspaces/routes.ts +354 -0
  179. package/src/central/workspaces/sidechats-routes.ts +410 -0
  180. package/src/central/workspaces/sidechats.ts +482 -0
  181. package/src/central/workspaces/store.ts +571 -0
  182. package/src/central/workspaces/supervision.ts +355 -0
  183. package/src/index.ts +97 -15
  184. package/src/spoke/concierge-agent.ts +33 -0
  185. package/src/spoke/deliver.ts +22 -4
  186. package/src/spoke/fork-revert.ts +142 -0
  187. package/src/spoke/hub-rpc.ts +41 -5
  188. package/src/spoke/hub-tool.ts +383 -18
  189. package/src/spoke/listing.ts +47 -4
  190. package/src/spoke/profile-apply.ts +298 -0
  191. package/src/spoke/session-context.ts +176 -0
  192. package/src/spoke/spoke.ts +319 -20
  193. package/src/spoke/timeline.ts +6 -1
  194. package/src/spoke/tracker.ts +23 -1
  195. package/src/spoke/who-teams.ts +81 -0
  196. package/src/tui-messaging.ts +32 -18
  197. package/web/dist/assets/ArtifactPage-BS0gVkfu.js +1 -0
  198. package/web/dist/assets/ArtifactPane-PLYHNs6s.js +1 -0
  199. package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
  200. package/web/dist/assets/ArtifactWorkbench-LkDyJ5Dv.js +6 -0
  201. package/web/dist/assets/Library-CtxRqTjo.js +2 -0
  202. package/web/dist/assets/Library-DYsFj_5c.css +1 -0
  203. package/web/dist/assets/ProfilesDestination-BYiHKLmc.js +1 -0
  204. package/web/dist/assets/TeamsPage-BwjsAAUC.js +1 -0
  205. package/web/dist/assets/Watchtower-C4swrWVs.js +2 -0
  206. package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
  207. package/web/dist/assets/index-DaH5GUdV.js +31 -0
  208. package/web/dist/assets/index-WWuKfWxL.css +1 -0
  209. package/web/dist/assets/live-BUofsQpz.css +1 -0
  210. package/web/dist/assets/live-YyTjE47g.js +5 -0
  211. package/web/dist/assets/pane-layout-CKC4I00k.js +1 -0
  212. package/web/dist/assets/plural-Bspvoqfq.js +1 -0
  213. package/web/dist/assets/renderer2d-CX5j3NcD.js +1 -0
  214. package/web/dist/assets/renderer3d-BXIVOvfi.js +5393 -0
  215. package/web/dist/assets/safe-area-DfPsOdWR.js +1 -0
  216. package/web/dist/assets/source-CvUHTc3O.js +1 -0
  217. package/web/dist/assets/ui-C9Igxzjm.js +1 -0
  218. package/web/dist/index.html +5 -2
  219. package/web/dist/sw.js +22 -2
  220. package/web/dist/assets/index-Dm8OmkaO.css +0 -1
  221. package/web/dist/assets/index-FUJbj_Du.js +0 -20
@@ -0,0 +1,194 @@
1
+ // hub.artifacts.jobs (HWA-24): agent-backed AI jobs on an artifact revision over /api/agent/artifacts/:id/jobs.
2
+ // A job runs with the requester's own authority: its handler is an ordinary session the requester launches
3
+ // (handler.launch, through the launch API: scope sessions:launch and a launch grant for apps and sessions) or
4
+ // messages (handler.session). The handler reads a bounded, untrusted copy of the revision (context) and submits
5
+ // one result; a propose-edit result is a proposal the user (or the requesting app, with artifacts:write) applies
6
+ // as a CAS on the job's base revision. Needs scope artifacts:ai (+ artifacts:read) for an app token.
7
+ // The wire types mirror src/central/artifacts/jobs.ts (the SDK imports nothing outside its package).
8
+ import type { ArtifactPage, ArtifactRevision, ArtifactTransport, InlineFile, KeyOptions } from "./artifacts.ts"
9
+
10
+ interface Signal {
11
+ readonly signal?: AbortSignal
12
+ }
13
+
14
+ export type ArtifactJobKind = "ask" | "propose-edit" | "run-action"
15
+ export type ArtifactJobState = "queued" | "running" | "succeeded" | "failed" | "cancelled"
16
+ export type ArtifactJobHandlerStatus = "starting" | "waiting" | "offline" | "ready" | "ended" | "failed"
17
+
18
+ export interface ArtifactJobLaunchHandler {
19
+ /** The machine (ID or name) that runs the handler session; placement is required there. */
20
+ readonly machine: string
21
+ /** An absolute repository path on that machine. */
22
+ readonly repo: string
23
+ readonly harness?: string
24
+ readonly model?: string
25
+ readonly agent?: string
26
+ readonly mode?: "checkout" | "worktree"
27
+ }
28
+
29
+ export type ArtifactJobHandlerInput = { readonly launch: ArtifactJobLaunchHandler } | { readonly session: string }
30
+
31
+ export interface ArtifactJobRequest {
32
+ /** Default propose-edit. */
33
+ readonly kind?: ArtifactJobKind
34
+ readonly baseRevisionID: string
35
+ readonly instruction: string
36
+ /** run-action: one of the artifact's declared actions (metadata `ai-actions`). */
37
+ readonly action?: string
38
+ readonly handler: ArtifactJobHandlerInput
39
+ /** propose-edit by a user: "auto" applies the result at once (still a CAS on the base). */
40
+ readonly apply?: "manual" | "auto"
41
+ }
42
+
43
+ export type ArtifactJobHandler =
44
+ | {
45
+ readonly kind: "launch"
46
+ readonly status: ArtifactJobHandlerStatus
47
+ readonly machine: string
48
+ readonly repo: string
49
+ readonly harness: string
50
+ readonly mode: "checkout" | "worktree"
51
+ readonly model?: string
52
+ readonly agent?: string
53
+ readonly launchID?: string
54
+ readonly launchState?: string
55
+ readonly reason?: string
56
+ readonly outcome?: string
57
+ readonly sessionID?: string
58
+ }
59
+ | {
60
+ readonly kind: "session"
61
+ readonly status: ArtifactJobHandlerStatus
62
+ readonly address: string
63
+ readonly sessionID?: string
64
+ readonly messageID?: string
65
+ readonly delivery?: string
66
+ }
67
+
68
+ export interface ArtifactJobProposal {
69
+ readonly state: "pending" | "applied" | "conflict"
70
+ readonly files: readonly { readonly path?: string; readonly mediaType: string; readonly bytes: number; readonly hash: string }[]
71
+ readonly keep?: readonly string[]
72
+ readonly entry?: string
73
+ readonly appliedRevisionID?: string
74
+ }
75
+
76
+ export interface ArtifactJob {
77
+ readonly v: 1
78
+ readonly id: string
79
+ readonly revision: number
80
+ readonly artifactID: string
81
+ readonly baseRevisionID: string
82
+ readonly requestedBy: { readonly kind: "user" } | { readonly kind: "session"; readonly sessionID: string }
83
+ readonly state: ArtifactJobState
84
+ readonly launchID?: string
85
+ readonly sessionID?: string
86
+ readonly resultRevisionID?: string
87
+ readonly error?: string
88
+ readonly createdAt: number
89
+ readonly updatedAt: number
90
+ readonly kind: ArtifactJobKind
91
+ readonly action?: string
92
+ readonly instruction: string
93
+ /** `user`, `app:<token id>` or the requesting session's ID. */
94
+ readonly by: string
95
+ readonly handler: ArtifactJobHandler
96
+ readonly apply: "manual" | "auto"
97
+ readonly deadline: number
98
+ readonly startedAt?: number
99
+ readonly finishedAt?: number
100
+ readonly answer?: string
101
+ readonly proposal?: ArtifactJobProposal
102
+ }
103
+
104
+ /** The bounded copy of the job's revision its handler reads. The content is untrusted data. */
105
+ export interface ArtifactJobContext {
106
+ readonly untrusted: true
107
+ readonly notice: string
108
+ readonly job: { readonly id: string; readonly kind: ArtifactJobKind; readonly action?: string; readonly instruction: string; readonly state: ArtifactJobState }
109
+ readonly artifact: { readonly id: string; readonly kind: string; readonly title: string }
110
+ readonly revision: { readonly id: string; readonly number: number; readonly entry?: string }
111
+ readonly files: readonly {
112
+ readonly path?: string
113
+ readonly mediaType: string
114
+ readonly bytes: number
115
+ readonly hash: string
116
+ readonly encoding?: "utf8"
117
+ readonly text?: string
118
+ /** Not copied: a binary file, or past the context budget. */
119
+ readonly omitted?: "binary" | "budget"
120
+ }[]
121
+ readonly truncated: boolean
122
+ }
123
+
124
+ /** A propose-edit job's candidate files for review before apply (users and the requesting app). Untrusted data. */
125
+ export interface ArtifactJobProposalView {
126
+ readonly untrusted: true
127
+ readonly notice: string
128
+ readonly state: ArtifactJobProposal["state"]
129
+ readonly entry?: string
130
+ /** Base-revision paths carried over unchanged. */
131
+ readonly keep: readonly string[]
132
+ readonly files: readonly {
133
+ readonly path?: string
134
+ readonly mediaType: string
135
+ readonly bytes: number
136
+ readonly hash: string
137
+ readonly encoding?: "utf8"
138
+ readonly text?: string
139
+ /** Not copied: a binary file, or past the review budget. */
140
+ readonly omitted?: "binary" | "budget"
141
+ }[]
142
+ readonly truncated: boolean
143
+ }
144
+
145
+ /** A handler's result: an answer (ask, run-action), a proposal (propose-edit: inline files, kept base paths), or an error. */
146
+ export type ArtifactJobResult =
147
+ | { readonly answer: string }
148
+ | { readonly files?: readonly Omit<InlineFile, "hash">[]; readonly keep?: readonly string[]; readonly entry?: string; readonly answer?: string }
149
+ | { readonly error: string }
150
+
151
+ export interface ArtifactJobCreated {
152
+ readonly job: ArtifactJob
153
+ /** False when an Idempotency-Key repeat answered the original. */
154
+ readonly created: boolean
155
+ }
156
+
157
+ /** hub.artifacts.jobs. */
158
+ export interface ArtifactJobs {
159
+ create(artifactID: string, input: ArtifactJobRequest, options?: KeyOptions): Promise<ArtifactJobCreated>
160
+ list(artifactID: string, options?: Signal & { readonly limit?: number; readonly cursor?: string }): Promise<ArtifactPage<ArtifactJob>>
161
+ get(artifactID: string, jobID: string, options?: Signal): Promise<{ job: ArtifactJob }>
162
+ /** The handler (or the user): the job's bounded, untrusted revision copy. */
163
+ context(artifactID: string, jobID: string, options?: Signal): Promise<ArtifactJobContext>
164
+ /** Users and the requesting app: a proposal's files to review before apply (reading changes nothing). */
165
+ proposal(artifactID: string, jobID: string, options?: Signal): Promise<ArtifactJobProposalView>
166
+ /** The handler session only; a repeated identical result is tolerated. */
167
+ submit(artifactID: string, jobID: string, result: ArtifactJobResult, options?: Signal): Promise<{ job: ArtifactJob }>
168
+ /** Users (and the requesting app with artifacts:write): publishes the proposal on the job's base (stale: conflict). */
169
+ apply(artifactID: string, jobID: string, expectedRevision: number, options?: Signal): Promise<{ job: ArtifactJob; revision: ArtifactRevision }>
170
+ cancel(artifactID: string, jobID: string, expectedRevision: number, options?: Signal): Promise<{ job: ArtifactJob; handlerStopped: boolean }>
171
+ }
172
+
173
+ const PATH = "/api/agent/artifacts"
174
+ const seg = (value: string) => encodeURIComponent(value)
175
+ const sig = (options: Signal = {}) => (options.signal ? { signal: options.signal } : {})
176
+
177
+ export function artifactJobsApi(hub: ArtifactTransport): ArtifactJobs {
178
+ const jobs = (id: string) => `${PATH}/${seg(id)}/jobs`
179
+ const job = (id: string, jobID: string, tail = "") => `${jobs(id)}/${seg(jobID)}${tail}`
180
+ return {
181
+ create: async (artifactID, input, options = {}) => {
182
+ const response = await hub.request("POST", jobs(artifactID), { body: input, ...(options.idempotencyKey ? { headers: { "idempotency-key": options.idempotencyKey } } : {}), ...sig(options) })
183
+ const answer = (await response.json()) as { job: ArtifactJob }
184
+ return { job: answer.job, created: response.status === 201 }
185
+ },
186
+ list: (artifactID, options = {}) => hub.call("GET", jobs(artifactID), { query: { limit: options.limit, cursor: options.cursor }, ...sig(options) }),
187
+ get: (artifactID, jobID, options) => hub.call("GET", job(artifactID, jobID), sig(options)),
188
+ context: (artifactID, jobID, options) => hub.call("GET", job(artifactID, jobID, "/context"), sig(options)),
189
+ proposal: (artifactID, jobID, options) => hub.call("GET", job(artifactID, jobID, "/proposal"), sig(options)),
190
+ submit: (artifactID, jobID, result, options) => hub.call("POST", job(artifactID, jobID, "/result"), { body: result, ...sig(options) }),
191
+ apply: (artifactID, jobID, expectedRevision, options) => hub.call("POST", job(artifactID, jobID, "/apply"), { body: { expectedRevision }, ...sig(options) }),
192
+ cancel: (artifactID, jobID, expectedRevision, options) => hub.call("POST", job(artifactID, jobID, "/cancel"), { body: { expectedRevision }, ...sig(options) }),
193
+ }
194
+ }
@@ -0,0 +1,187 @@
1
+ // hub.artifacts (HWA-21): persistent artifacts over /api/agent/artifacts. Metadata travels as JSON; bytes go two
2
+ // ways so nobody embeds huge payloads: small files inline in a create/revise (`upload`, ≤ 1 MiB decoded per
3
+ // request), anything bigger staged first with uploadBlob (a raw streamed body, ≤ 25 MiB) and then referenced by its
4
+ // BlobRef in `blobs`. Downloads answer the raw Response (stream it; it is an attachment, never rendered).
5
+ // The wire types mirror packages/protocol/src/artifacts.ts (the SDK imports nothing outside its package; the hub's
6
+ // tests keep them in step).
7
+
8
+ import { type ArtifactJobs, artifactJobsApi } from "./artifact-jobs.ts"
9
+
10
+ /** Only an abort signal (as the client's CallOptions). */
11
+ interface Signal {
12
+ readonly signal?: AbortSignal
13
+ }
14
+
15
+ export type ArtifactKind = "markdown" | "code" | "image" | "table" | "chart" | "dashboard" | "html" | "app" | "file"
16
+ export type ArtifactVisibility = "private" | "workspace"
17
+ export type ArtifactState = "active" | "archived"
18
+
19
+ export interface Artifact {
20
+ readonly v: 1
21
+ readonly id: string
22
+ readonly revision: number
23
+ readonly title: string
24
+ readonly description?: string
25
+ readonly kind: ArtifactKind
26
+ readonly state: ArtifactState
27
+ readonly visibility: ArtifactVisibility
28
+ readonly workspaceID?: string
29
+ readonly createdBySessionID?: string
30
+ readonly latestRevisionID: string
31
+ readonly tags?: readonly string[]
32
+ readonly metadata?: Readonly<Record<string, string>>
33
+ readonly createdAt: number
34
+ readonly updatedAt: number
35
+ }
36
+
37
+ export interface BlobRef {
38
+ /** `sha256:<64 hex>`. */
39
+ readonly hash: string
40
+ readonly bytes: number
41
+ readonly mediaType: string
42
+ readonly path?: string
43
+ }
44
+
45
+ export interface ArtifactRevision {
46
+ readonly v: 1
47
+ readonly id: string
48
+ readonly artifactID: string
49
+ readonly number: number
50
+ readonly baseRevisionID?: string
51
+ readonly blobs: readonly BlobRef[]
52
+ readonly entry?: string
53
+ readonly author: { readonly kind: "user" } | { readonly kind: "session"; readonly sessionID: string } | { readonly kind: "ai_job"; readonly jobID: string }
54
+ readonly createdAt: number
55
+ }
56
+
57
+ /** A small file sent inline (utf8 text, or base64 bytes). */
58
+ export interface InlineFile {
59
+ readonly mediaType: string
60
+ readonly data: string
61
+ readonly encoding?: "utf8" | "base64"
62
+ readonly path?: string
63
+ /** `sha256:<hex>` the decoded bytes must hash to. */
64
+ readonly hash?: string
65
+ }
66
+
67
+ /** The files of a revision: references to stored bytes and/or inline files; `entry` names a bundle's entry path. */
68
+ export interface RevisionFiles {
69
+ readonly blobs?: readonly BlobRef[]
70
+ readonly upload?: readonly InlineFile[]
71
+ readonly entry?: string
72
+ }
73
+
74
+ export interface ArtifactCreate extends RevisionFiles {
75
+ readonly title: string
76
+ readonly kind: ArtifactKind
77
+ readonly description?: string
78
+ readonly visibility?: ArtifactVisibility
79
+ readonly workspaceID?: string
80
+ readonly tags?: readonly string[]
81
+ readonly metadata?: Readonly<Record<string, string>>
82
+ }
83
+
84
+ export interface ArtifactPage<T> {
85
+ readonly items: readonly T[]
86
+ readonly cursor?: string
87
+ }
88
+
89
+ export interface Published {
90
+ readonly artifact: Artifact
91
+ readonly revision: ArtifactRevision
92
+ /** False when an Idempotency-Key repeat answered the original. */
93
+ readonly created: boolean
94
+ }
95
+
96
+ export interface StagedBlob {
97
+ readonly blob: BlobRef
98
+ /** Reference it in a create/revise within this time, or recovery may collect the bytes. */
99
+ readonly referenceWithinMs: number
100
+ }
101
+
102
+ export interface ArtifactsFilter extends Signal {
103
+ readonly limit?: number
104
+ readonly cursor?: string
105
+ readonly state?: ArtifactState
106
+ readonly workspace?: string
107
+ }
108
+
109
+ export interface KeyOptions extends Signal {
110
+ /** Idempotency-Key: the same key and body again answer the original (created false). */
111
+ readonly idempotencyKey?: string
112
+ }
113
+
114
+ export interface ArtifactUpdate {
115
+ readonly expectedRevision: number
116
+ readonly title?: string
117
+ readonly description?: string
118
+ readonly tags?: readonly string[]
119
+ readonly metadata?: Readonly<Record<string, string>>
120
+ }
121
+
122
+ /** hub.artifacts (users; app tokens with artifacts:read / artifacts:write; sessions read workspace-shared ones and create). */
123
+ export interface Artifacts {
124
+ list(filter?: ArtifactsFilter): Promise<ArtifactPage<Artifact>>
125
+ get(id: string, options?: Signal): Promise<{ artifact: Artifact }>
126
+ create(input: ArtifactCreate, options?: KeyOptions): Promise<Published>
127
+ /** A new revision on `baseRevisionID` (the latest you read; a stale one is a conflict HubError). */
128
+ revise(id: string, input: RevisionFiles & { readonly baseRevisionID: string }, options?: KeyOptions): Promise<Published>
129
+ revisions(id: string, options?: Signal & { readonly limit?: number; readonly cursor?: string }): Promise<ArtifactPage<ArtifactRevision>>
130
+ revision(id: string, revisionID: string, options?: Signal): Promise<{ revision: ArtifactRevision }>
131
+ /** Stages raw bytes (streamed) under a declared media type; answers the reference to use in `blobs`. */
132
+ uploadBlob(body: BodyInit, mediaType: string, options?: Signal & { readonly hash?: string }): Promise<StagedBlob>
133
+ /** The bytes of one blob of a revision, as the raw Response (an attachment). */
134
+ download(id: string, revisionID: string, hash: string, options?: Signal): Promise<Response>
135
+ update(id: string, input: ArtifactUpdate, options?: Signal): Promise<{ artifact: Artifact }>
136
+ setVisibility(id: string, input: { readonly expectedRevision: number; readonly visibility: ArtifactVisibility; readonly workspaceID?: string }, options?: Signal): Promise<{ artifact: Artifact }>
137
+ archive(id: string, expectedRevision: number, options?: Signal): Promise<{ artifact: Artifact }>
138
+ unarchive(id: string, expectedRevision: number, options?: Signal): Promise<{ artifact: Artifact }>
139
+ delete(id: string, expectedRevision: number, options?: Signal): Promise<void>
140
+ /** AI jobs on an artifact revision (HWA-24; scope artifacts:ai). */
141
+ readonly jobs: ArtifactJobs
142
+ }
143
+
144
+ type Query = Readonly<Record<string, string | number | undefined>>
145
+
146
+ /** What hub.artifacts needs from the client (HubClient's request and call). */
147
+ export interface ArtifactTransport {
148
+ request(
149
+ method: string,
150
+ pathname: string,
151
+ options?: { readonly query?: Query; readonly body?: unknown; readonly raw?: { readonly body: BodyInit; readonly contentType: string }; readonly headers?: Readonly<Record<string, string>>; readonly signal?: AbortSignal },
152
+ ): Promise<Response>
153
+ call<T>(method: string, pathname: string, options?: { readonly query?: Query; readonly body?: unknown; readonly signal?: AbortSignal }): Promise<T>
154
+ }
155
+
156
+ const PATH = "/api/agent/artifacts"
157
+ const seg = (value: string) => encodeURIComponent(value)
158
+ const sig = (options: Signal = {}) => (options.signal ? { signal: options.signal } : {})
159
+
160
+ export function artifactsApi(hub: ArtifactTransport): Artifacts {
161
+ const publish = async (pathname: string, body: unknown, options: KeyOptions = {}): Promise<Published> => {
162
+ const response = await hub.request("POST", pathname, { body, ...(options.idempotencyKey ? { headers: { "idempotency-key": options.idempotencyKey } } : {}), ...sig(options) })
163
+ const answer = (await response.json()) as { artifact: Artifact; revision: ArtifactRevision }
164
+ return { artifact: answer.artifact, revision: answer.revision, created: response.status === 201 }
165
+ }
166
+ return {
167
+ list: (filter = {}) => hub.call("GET", PATH, { query: { limit: filter.limit, cursor: filter.cursor, state: filter.state, workspace: filter.workspace }, ...sig(filter) }),
168
+ get: (id, options) => hub.call("GET", `${PATH}/${seg(id)}`, sig(options)),
169
+ create: (input, options) => publish(PATH, input, options),
170
+ revise: (id, input, options) => publish(`${PATH}/${seg(id)}/revisions`, input, options),
171
+ revisions: (id, options = {}) => hub.call("GET", `${PATH}/${seg(id)}/revisions`, { query: { limit: options.limit, cursor: options.cursor }, ...sig(options) }),
172
+ revision: (id, revisionID, options) => hub.call("GET", `${PATH}/${seg(id)}/revisions/${seg(revisionID)}`, sig(options)),
173
+ uploadBlob: async (body, mediaType, options = {}) => {
174
+ const response = await hub.request("POST", `${PATH}/blobs`, { raw: { body, contentType: mediaType }, ...(options.hash ? { headers: { "x-blob-hash": options.hash } } : {}), ...sig(options) })
175
+ return (await response.json()) as StagedBlob
176
+ },
177
+ download: (id, revisionID, hash, options) => hub.request("GET", `${PATH}/${seg(id)}/revisions/${seg(revisionID)}/blobs/${seg(hash)}`, sig(options)),
178
+ update: (id, input, options) => hub.call("PATCH", `${PATH}/${seg(id)}`, { body: input, ...sig(options) }),
179
+ setVisibility: (id, input, options) => hub.call("PUT", `${PATH}/${seg(id)}/visibility`, { body: input, ...sig(options) }),
180
+ archive: (id, expectedRevision, options) => hub.call("POST", `${PATH}/${seg(id)}/archive`, { body: { expectedRevision }, ...sig(options) }),
181
+ unarchive: (id, expectedRevision, options) => hub.call("POST", `${PATH}/${seg(id)}/unarchive`, { body: { expectedRevision }, ...sig(options) }),
182
+ delete: async (id, expectedRevision, options) => {
183
+ await hub.call("DELETE", `${PATH}/${seg(id)}`, { body: { expectedRevision }, ...sig(options) })
184
+ },
185
+ jobs: artifactJobsApi(hub),
186
+ }
187
+ }