chamba 0.2.0 → 0.3.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.
Files changed (77) hide show
  1. package/README.md +6 -7
  2. package/dist/cli.js +534 -643
  3. package/dist/server.js +1395 -1483
  4. package/inject/annotate.js +18 -49
  5. package/package.json +5 -7
  6. package/skill/SKILL.md +67 -259
  7. package/web/assets/highlighted-body-OFNGDK62-Bn4Eu7CG.js +1 -0
  8. package/web/assets/index-B9DI4F1Z.js +202 -0
  9. package/web/assets/index-DK_n6CTo.css +2 -0
  10. package/web/assets/mermaid-GHXKKRXX-CEMduc-U.js +1 -0
  11. package/web/index.html +2 -2
  12. package/web/assets/abnfDiagram-VRR7QNED-C5V2hrtE.js +0 -1
  13. package/web/assets/arc-d7OTmxZy.js +0 -1
  14. package/web/assets/architectureDiagram-ZJ3FMSHR--CztaeAS.js +0 -36
  15. package/web/assets/blockDiagram-677ZJIJ3-DkA5Z_uK.js +0 -132
  16. package/web/assets/c4Diagram-LMCZKHZV-C78dI0l-.js +0 -10
  17. package/web/assets/channel-D9AxU8kk.js +0 -1
  18. package/web/assets/chunk-2Q5K7J3B-LBNP12XV.js +0 -1
  19. package/web/assets/chunk-32BRIVSS-DZoXrhCs.js +0 -1
  20. package/web/assets/chunk-5VM5RSS4-CPjoRxF_.js +0 -15
  21. package/web/assets/chunk-EX3LRPZG-DSlL8FE9.js +0 -231
  22. package/web/assets/chunk-JWPE2WC7-BjbBLYge.js +0 -1
  23. package/web/assets/chunk-MOJQB5TN-DCKqdaDr.js +0 -88
  24. package/web/assets/chunk-RYQCIY6F-D5yAURhE.js +0 -1
  25. package/web/assets/chunk-V7JOEXUC-DKY2sn06.js +0 -206
  26. package/web/assets/chunk-VR4S4FIN-DGiVMHh6.js +0 -1
  27. package/web/assets/chunk-XXDRQBXY-CUI1Tkrr.js +0 -1
  28. package/web/assets/classDiagram-OUVF2IWQ-D7gWuzIF.js +0 -1
  29. package/web/assets/classDiagram-v2-EOCWNBFH-D7gWuzIF.js +0 -1
  30. package/web/assets/cose-bilkent-JH36ORCC-Ao3LzalV.js +0 -1
  31. package/web/assets/cynefin-VYW2F7L2-7JQwpRwH.js +0 -178
  32. package/web/assets/cynefinDiagram-TSTJHNR4-BfuANy_V.js +0 -62
  33. package/web/assets/cytoscape.esm-DTSO7Bv0.js +0 -331
  34. package/web/assets/dagre-VKFMJZFB-DHVnVJCL.js +0 -4
  35. package/web/assets/defaultLocale-DX6XiGOO.js +0 -1
  36. package/web/assets/diagram-FQU43EPY-OYrtwA2f.js +0 -3
  37. package/web/assets/diagram-G47NLZAW-CbkLZxYp.js +0 -24
  38. package/web/assets/diagram-NH7WQ7WH-B0z_jTdi.js +0 -24
  39. package/web/assets/diagram-OA4YK3LP-BRu_2nwx.js +0 -30
  40. package/web/assets/diagram-WEI45ONY-B9cpw86D.js +0 -41
  41. package/web/assets/ebnfDiagram-CCIWWBDH-DaBQ7T4R.js +0 -1
  42. package/web/assets/erDiagram-Q63AITRT-D6PQiFj3.js +0 -85
  43. package/web/assets/flowDiagram-23GEKE2U-p_8Wj7It.js +0 -156
  44. package/web/assets/ganttDiagram-NO4QXBWP-BZBwuqu3.js +0 -292
  45. package/web/assets/gitGraphDiagram-IHSO6WYX-DyPcQ-91.js +0 -106
  46. package/web/assets/graph-C9eacEi8.js +0 -1
  47. package/web/assets/highlighted-body-OFNGDK62-Cq7RJZcn.js +0 -1
  48. package/web/assets/index-BN-QUr5b.js +0 -350
  49. package/web/assets/index-D6-LVQ5m.css +0 -1
  50. package/web/assets/infoDiagram-FWYZ7A6U-sAgBAeWC.js +0 -2
  51. package/web/assets/init-Gi6I4Gst.js +0 -1
  52. package/web/assets/ishikawaDiagram-FXEZZL3T-COEn9wCs.js +0 -70
  53. package/web/assets/journeyDiagram-5HDEW3XC-BPddPRXW.js +0 -139
  54. package/web/assets/kanban-definition-HUTT4EX6-B8TW97Oy.js +0 -89
  55. package/web/assets/katex-C5jXJg4s.js +0 -257
  56. package/web/assets/layout-DEXfKzaS.js +0 -1
  57. package/web/assets/linear-CnW5uQ_k.js +0 -1
  58. package/web/assets/map-Czzmt4hB.js +0 -1
  59. package/web/assets/mermaid.core-bOV5iNX_.js +0 -314
  60. package/web/assets/mindmap-definition-LN4V7U3C-Dtr4xW_-.js +0 -96
  61. package/web/assets/ordinal-Cboi1Yqb.js +0 -1
  62. package/web/assets/pegDiagram-2B236MQR-DqBITK5N.js +0 -1
  63. package/web/assets/pieDiagram-ENE6RG2P-DXjxZgy3.js +0 -39
  64. package/web/assets/quadrantDiagram-ABIIQ3AL-D4MXx3U7.js +0 -7
  65. package/web/assets/railroadDiagram-RFXS5EU6-LPfpHg4_.js +0 -1
  66. package/web/assets/requirementDiagram-TGXJPOKE-CiDswbjt.js +0 -84
  67. package/web/assets/sankeyDiagram-HTMAVEWB-VfN_qTEI.js +0 -40
  68. package/web/assets/sequenceDiagram-DBY2YBRQ-kJRIx4zm.js +0 -162
  69. package/web/assets/sizeCapture-X5ZJPWSS-D8vJbAt-.js +0 -1
  70. package/web/assets/stateDiagram-2N3HPSRC-9qku-qAj.js +0 -1
  71. package/web/assets/stateDiagram-v2-6OUMAXLB-DbOQICyv.js +0 -1
  72. package/web/assets/swimlanes-5IMT3BWC-Cdb5FDUM.js +0 -2
  73. package/web/assets/swimlanesDiagram-G3AALYLV-EDpLmqQv.js +0 -8
  74. package/web/assets/timeline-definition-FHXFAJF6-D0hyJCMm.js +0 -120
  75. package/web/assets/vennDiagram-L72KCM5P-KsCkGMtr.js +0 -34
  76. package/web/assets/wardleyDiagram-EHGQE667-B4jT_1ak.js +0 -78
  77. package/web/assets/xychartDiagram-FW5EYKEG-Bmy0rt8j.js +0 -7
package/dist/cli.js CHANGED
@@ -13,17 +13,17 @@ import { z as z3 } from "zod";
13
13
  // packages/shared/src/messages.ts
14
14
  import { z as z2 } from "zod";
15
15
 
16
- // packages/shared/src/session.ts
16
+ // packages/shared/src/events.ts
17
17
  import { z } from "zod";
18
- var sessionKindSchema = z.enum(["everyday", "spec"]);
19
- var sessionStatusSchema = z.enum(["active", "ended"]);
20
- var sessionMetaSchema = z.object({
21
- id: z.string().min(1),
22
- kind: sessionKindSchema.optional(),
23
- title: z.string().optional(),
24
- status: sessionStatusSchema,
25
- createdAt: z.string().datetime(),
26
- updatedAt: z.string().datetime()
18
+ var eventSourceSchema = z.enum(["user", "agent", "system"]);
19
+ var eventEnvelopeSchema = z.object({
20
+ id: z.uuid(),
21
+ seq: z.number().int().nonnegative(),
22
+ sessionId: z.string().min(1),
23
+ ts: z.iso.datetime(),
24
+ type: z.string().min(1),
25
+ source: eventSourceSchema,
26
+ payload: z.unknown()
27
27
  });
28
28
 
29
29
  // packages/shared/src/messages.ts
@@ -34,22 +34,23 @@ var attachmentSchema = z2.object({
34
34
  contentType: z2.string().min(1),
35
35
  size: z2.number().int().nonnegative()
36
36
  });
37
+ var replyRefSchema = z2.object({
38
+ seq: z2.number().int().nonnegative(),
39
+ id: z2.string().min(1),
40
+ source: eventSourceSchema,
41
+ snippet: z2.string()
42
+ });
37
43
  var messagePayloadSchema = z2.object({
38
44
  text: z2.string().min(1).optional(),
39
- attachments: z2.array(attachmentSchema).optional()
45
+ attachments: z2.array(attachmentSchema).optional(),
46
+ replyTo: replyRefSchema.optional(),
47
+ answerIn: z2.literal("terminal").optional()
40
48
  }).refine((p) => (p.text?.length ?? 0) > 0 || (p.attachments?.length ?? 0) > 0, {
41
- message: "a message needs text or at least one attachment"
49
+ error: "a message needs text or at least one attachment"
42
50
  });
43
- var systemReasonSchema = z2.enum([
44
- "session-created",
45
- "user-moved-away",
46
- "session-ended",
47
- "kind-chosen"
48
- ]);
51
+ var systemReasonSchema = z2.enum(["session-created", "session-ended"]);
49
52
  var systemPayloadSchema = z2.object({
50
- reason: systemReasonSchema,
51
- to: z2.string().min(1).optional(),
52
- kind: sessionKindSchema.optional()
53
+ reason: systemReasonSchema
53
54
  });
54
55
 
55
56
  // packages/shared/src/annotations.ts
@@ -83,147 +84,169 @@ var annotationBatchPayloadSchema = z3.object({
83
84
  });
84
85
 
85
86
  // packages/shared/src/api.ts
86
- import { z as z7 } from "zod";
87
+ import { z as z6 } from "zod";
87
88
 
88
- // packages/shared/src/events.ts
89
+ // packages/shared/src/presence.ts
89
90
  import { z as z4 } from "zod";
90
- var eventSourceSchema = z4.enum(["user", "agent", "system"]);
91
- var eventEnvelopeSchema = z4.object({
92
- id: z4.string().uuid(),
93
- seq: z4.number().int().nonnegative(),
94
- sessionId: z4.string().min(1),
95
- ts: z4.string().datetime(),
96
- type: z4.string().min(1),
97
- source: eventSourceSchema,
98
- payload: z4.unknown()
99
- });
91
+ var presenceSchema = z4.enum(["listening", "working", "away"]);
100
92
 
101
- // packages/shared/src/presence.ts
93
+ // packages/shared/src/session.ts
102
94
  import { z as z5 } from "zod";
103
- var presenceSchema = z5.enum(["listening", "working", "away"]);
104
-
105
- // packages/shared/src/spec.ts
106
- import { z as z6 } from "zod";
107
- var QUESTION_CARD = "question-card";
108
- var DECISION = "decision";
109
- var ARTIFACT = "artifact";
110
- var questionOptionSchema = z6.object({
111
- label: z6.string().min(1),
112
- description: z6.string().min(1),
113
- recommended: z6.boolean().optional()
114
- });
115
- var questionCardPayloadSchema = z6.object({
116
- cardId: z6.string().uuid(),
117
- question: z6.string().min(1),
118
- options: z6.array(questionOptionSchema).min(2).max(6),
119
- /** Optional custom wording for the stop-anytime affordance. */
120
- stopHint: z6.string().min(1).optional()
121
- });
122
- var decisionStatusSchema = z6.enum(["confirmed", "assumed"]);
123
- var decisionPayloadSchema = z6.object({
124
- summary: z6.string().min(1),
125
- status: decisionStatusSchema,
126
- detail: z6.string().min(1).optional()
127
- });
128
- var artifactKindSchema = z6.enum(["draft", "mock", "diagram"]);
129
- var artifactRefSchema = z6.object({
130
- kind: artifactKindSchema,
131
- name: z6.string().min(1),
132
- relPath: z6.string().min(1),
133
- contentType: z6.string().min(1),
134
- size: z6.number().int().nonnegative()
135
- });
136
- var artifactPayloadSchema = z6.object({
137
- artifact: artifactRefSchema
95
+ var sessionStatusSchema = z5.enum(["active", "ended"]);
96
+ var sessionMetaSchema = z5.object({
97
+ id: z5.string().min(1),
98
+ /** The agent session that created this session (e.g. a Claude Code session id).
99
+ * `open` uses it to reconnect the same agent session to its existing workspace. */
100
+ agentKey: z5.string().min(1).optional(),
101
+ title: z5.string().optional(),
102
+ status: sessionStatusSchema,
103
+ createdAt: z5.string().datetime(),
104
+ updatedAt: z5.string().datetime()
138
105
  });
139
106
 
140
107
  // packages/shared/src/api.ts
141
- var cursorSchema = z7.number().int().nonnegative();
142
- var createSessionReqSchema = z7.object({
143
- kind: sessionKindSchema.optional(),
144
- title: z7.string().min(1).optional()
145
- });
146
- var setSessionKindReqSchema = z7.object({
147
- kind: sessionKindSchema
108
+ var cursorSchema = z6.number().int().nonnegative();
109
+ var POLL_DEFAULT_WAIT_MS = 25e3;
110
+ var POLL_MAX_WAIT_MS = 3e4;
111
+ var SESSION_QUERY_PARAM = "s";
112
+ function sessionPageUrl(baseUrl, id) {
113
+ return `${baseUrl.replace(/\/+$/, "")}/?${SESSION_QUERY_PARAM}=${encodeURIComponent(id)}`;
114
+ }
115
+ async function errorBodyMessage(res) {
116
+ try {
117
+ const body = await res.json();
118
+ return typeof body.error === "string" ? body.error : null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+ var createSessionReqSchema = z6.object({
124
+ title: z6.string().min(1).optional(),
125
+ agentKey: z6.string().min(1).optional()
148
126
  });
149
- var messageBodySchema = z7.object({
150
- text: z7.string().min(1).optional(),
151
- attachmentIds: z7.array(z7.string().min(1)).optional()
152
- }).refine((r) => (r.text?.length ?? 0) > 0 || (r.attachmentIds?.length ?? 0) > 0, {
153
- message: "a message needs text or at least one attachment"
127
+ var messageBodyBase = z6.object({
128
+ text: z6.string().min(1).optional(),
129
+ attachmentIds: z6.array(z6.string().min(1)).optional(),
130
+ replyTo: z6.number().int().nonnegative().optional()
154
131
  });
155
- var annotationItemReqSchema = z7.object({
156
- note: z7.string().min(1).optional(),
132
+ var needsContent = (r) => (r.text?.length ?? 0) > 0 || (r.attachmentIds?.length ?? 0) > 0;
133
+ var needsContentMessage = { error: "a message needs text or at least one attachment" };
134
+ var postMessageReqSchema = messageBodyBase.extend({ answerIn: z6.literal("terminal").optional() }).refine(needsContent, needsContentMessage);
135
+ var replyReqSchema = messageBodyBase.refine(needsContent, needsContentMessage);
136
+ var annotationItemReqSchema = z6.object({
137
+ note: z6.string().min(1).optional(),
157
138
  target: annotationTargetSchema,
158
- screenshotAttachmentId: z7.string().min(1).optional()
139
+ screenshotAttachmentId: z6.string().min(1).optional()
159
140
  });
160
- var postAnnotationsReqSchema = z7.object({
161
- items: z7.array(annotationItemReqSchema).min(1)
141
+ var postAnnotationsReqSchema = z6.object({
142
+ items: z6.array(annotationItemReqSchema).min(1)
162
143
  });
163
- var moveAwayReqSchema = z7.object({
164
- to: z7.string().min(1).optional()
165
- });
166
- var postQuestionCardReqSchema = questionCardPayloadSchema;
167
- var postDecisionReqSchema = decisionPayloadSchema;
168
- var postArtifactReqSchema = z7.object({
169
- kind: artifactKindSchema,
170
- name: z7.string().min(1),
171
- content: z7.string().min(1)
172
- });
173
- var attachmentResSchema = z7.object({
144
+ var attachmentResSchema = z6.object({
174
145
  attachment: attachmentSchema
175
146
  });
176
- var eventResSchema = z7.object({
147
+ var eventResSchema = z6.object({
177
148
  event: eventEnvelopeSchema
178
149
  });
179
- var pollResSchema = z7.object({
180
- events: z7.array(eventEnvelopeSchema),
150
+ var pollResSchema = z6.object({
151
+ events: z6.array(eventEnvelopeSchema),
181
152
  cursor: cursorSchema,
182
- ended: z7.boolean().optional()
153
+ ended: z6.boolean().optional(),
154
+ /** Set when the server is shutting down on purpose and woke this poll to say
155
+ * so; the caller should exit its loop cleanly instead of retrying. */
156
+ stopping: z6.boolean().optional()
183
157
  });
184
- var fetchMoreSchema = z7.object({
185
- hasMore: z7.boolean(),
158
+ var fetchMoreSchema = z6.object({
159
+ hasMore: z6.boolean(),
186
160
  earliestLoadedSeq: cursorSchema,
187
- hint: z7.string()
161
+ hint: z6.string()
188
162
  });
189
- var attachDigestSchema = z7.object({
163
+ var attachDigestSchema = z6.object({
190
164
  meta: sessionMetaSchema,
191
165
  presence: presenceSchema,
192
- tail: z7.array(eventEnvelopeSchema),
166
+ tail: z6.array(eventEnvelopeSchema),
193
167
  cursor: cursorSchema,
194
168
  fetchMore: fetchMoreSchema
195
169
  });
196
- var sessionSummarySchema = z7.object({
170
+ var sessionSummarySchema = z6.object({
197
171
  meta: sessionMetaSchema,
198
172
  presence: presenceSchema
199
173
  });
200
- var listResSchema = z7.object({
201
- sessions: z7.array(sessionSummarySchema)
174
+ var listResSchema = z6.object({
175
+ sessions: z6.array(sessionSummarySchema)
202
176
  });
203
- var sessionResSchema = z7.object({
177
+ var sessionResSchema = z6.object({
204
178
  session: sessionSummarySchema
205
179
  });
206
- var healthResSchema = z7.object({
207
- ok: z7.boolean(),
180
+ var healthResSchema = z6.object({
181
+ ok: z6.boolean(),
208
182
  sessions: cursorSchema,
209
- home: z7.string().min(1).optional(),
210
- port: z7.number().int().positive().optional(),
211
- version: z7.string().min(1).optional()
183
+ home: z6.string().min(1).optional(),
184
+ port: z6.number().int().positive().optional(),
185
+ version: z6.string().min(1).optional()
212
186
  });
213
187
 
214
188
  // packages/shared/src/issues.ts
215
- import { z as z8 } from "zod";
216
- var issueTypeSchema = z8.enum(["bug", "feature"]);
217
- var postIssueReqSchema = z8.object({
189
+ import { z as z7 } from "zod";
190
+ var issueTypeSchema = z7.enum(["bug", "feature"]);
191
+ var postIssueReqSchema = z7.object({
218
192
  type: issueTypeSchema,
219
- text: z8.string().min(1),
220
- screenshotAttachmentIds: z8.array(z8.string().min(1)).optional()
193
+ text: z7.string().min(1),
194
+ screenshotAttachmentIds: z7.array(z7.string().min(1)).optional()
195
+ });
196
+ var issueResSchema = z7.object({
197
+ id: z7.string().min(1),
198
+ path: z7.string().min(1)
199
+ });
200
+
201
+ // packages/shared/src/page.ts
202
+ import { z as z8 } from "zod";
203
+ var PAGE = "page";
204
+ var pageRefSchema = z8.object({
205
+ name: z8.string().min(1),
206
+ relPath: z8.string().min(1),
207
+ contentType: z8.string().min(1),
208
+ size: z8.number().int().nonnegative()
209
+ });
210
+ var pagePayloadSchema = z8.object({
211
+ page: pageRefSchema
221
212
  });
222
- var issueResSchema = z8.object({
223
- id: z8.string().min(1),
224
- path: z8.string().min(1)
213
+ var postPageReqSchema = z8.object({
214
+ name: z8.string().min(1).max(120),
215
+ content: z8.string().min(1)
225
216
  });
226
217
 
218
+ // packages/shared/src/payload.ts
219
+ var attachmentsSchema = attachmentSchema.array();
220
+ function eventText(event) {
221
+ const payload = event.payload;
222
+ if (payload && typeof payload === "object") {
223
+ const record = payload;
224
+ if (typeof record.text === "string") return record.text;
225
+ if (typeof record.reason === "string") return record.reason;
226
+ }
227
+ return "";
228
+ }
229
+ function eventAttachments(event) {
230
+ if (!event.payload || typeof event.payload !== "object") return [];
231
+ const parsed = attachmentsSchema.safeParse(
232
+ event.payload.attachments
233
+ );
234
+ return parsed.success ? parsed.data : [];
235
+ }
236
+
237
+ // packages/shared/src/reference.ts
238
+ function parseRef(input) {
239
+ let ref = input.trim();
240
+ const token = ref.match(/^\[?\s*chamba\s+([^\s\]]+)/i);
241
+ if (token?.[1]) ref = token[1];
242
+ const match = ref.match(/^(?:([A-Za-z0-9_-]+)#)?#?(\d+)$/);
243
+ if (!match?.[2]) return null;
244
+ return { ...match[1] ? { session: match[1] } : {}, seq: Number(match[2]) };
245
+ }
246
+ function sourceLabel(source) {
247
+ return source === "user" ? "you" : source;
248
+ }
249
+
227
250
  // packages/shared/src/ws.ts
228
251
  import { z as z9 } from "zod";
229
252
  var wsServerMessageSchema = z9.discriminatedUnion("t", [
@@ -240,10 +263,6 @@ var wsServerMessageSchema = z9.discriminatedUnion("t", [
240
263
  t: z9.literal("presence"),
241
264
  state: presenceSchema
242
265
  }),
243
- z9.object({
244
- t: z9.literal("kind"),
245
- meta: sessionMetaSchema
246
- }),
247
266
  z9.object({
248
267
  t: z9.literal("ended"),
249
268
  meta: sessionMetaSchema
@@ -255,40 +274,100 @@ import { createNodeWebSocket } from "@hono/node-ws";
255
274
  import { Hono } from "hono";
256
275
 
257
276
  // packages/server/src/inject.ts
277
+ import { existsSync as existsSync2 } from "node:fs";
278
+ import path2 from "node:path";
279
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
280
+
281
+ // packages/server/src/static.ts
258
282
  import { existsSync } from "node:fs";
259
283
  import path from "node:path";
260
284
  import { fileURLToPath } from "node:url";
261
- function resolveInjectDist() {
262
- const here = path.dirname(fileURLToPath(import.meta.url));
263
- const dev = path.resolve(here, "../../inject/dist");
264
- const packaged = path.resolve(here, "../inject");
265
- return [dev, packaged].find((dir) => existsSync(path.join(dir, "annotate.js"))) ?? dev;
266
- }
267
285
 
268
- // packages/server/src/render.ts
269
- import { marked } from "marked";
286
+ // packages/server/src/mime.ts
287
+ var CONTENT_TYPES = {
288
+ ".html": "text/html; charset=utf-8",
289
+ ".js": "text/javascript; charset=utf-8",
290
+ ".mjs": "text/javascript; charset=utf-8",
291
+ ".css": "text/css; charset=utf-8",
292
+ ".json": "application/json; charset=utf-8",
293
+ ".svg": "image/svg+xml",
294
+ ".png": "image/png",
295
+ ".jpg": "image/jpeg",
296
+ ".jpeg": "image/jpeg",
297
+ ".gif": "image/gif",
298
+ ".webp": "image/webp",
299
+ ".ico": "image/x-icon",
300
+ ".woff2": "font/woff2",
301
+ ".map": "application/json; charset=utf-8",
302
+ ".pdf": "application/pdf",
303
+ ".txt": "text/plain; charset=utf-8",
304
+ ".md": "text/markdown; charset=utf-8"
305
+ };
306
+ var EXTENSIONS = {};
307
+ for (const [ext, type] of Object.entries(CONTENT_TYPES)) {
308
+ const base = type.split(";")[0] ?? type;
309
+ if (!(base in EXTENSIONS)) EXTENSIONS[base] = ext;
310
+ }
270
311
 
271
312
  // packages/server/src/static.ts
272
- import { existsSync as existsSync2 } from "node:fs";
273
- import path2 from "node:path";
274
- import { fileURLToPath as fileURLToPath2 } from "node:url";
275
313
  function resolveWebDist() {
314
+ const here = path.dirname(fileURLToPath(import.meta.url));
315
+ const dev = path.resolve(here, "../../web/dist");
316
+ const packaged = path.resolve(here, "../web");
317
+ return [dev, packaged].find((dir) => existsSync(path.join(dir, "index.html"))) ?? dev;
318
+ }
319
+
320
+ // packages/server/src/inject.ts
321
+ function resolveInjectDist() {
276
322
  const here = path2.dirname(fileURLToPath2(import.meta.url));
277
- const dev = path2.resolve(here, "../../web/dist");
278
- const packaged = path2.resolve(here, "../web");
279
- return [dev, packaged].find((dir) => existsSync2(path2.join(dir, "index.html"))) ?? dev;
323
+ const dev = path2.resolve(here, "../../inject/dist");
324
+ const packaged = path2.resolve(here, "../inject");
325
+ return [dev, packaged].find((dir) => existsSync2(path2.join(dir, "annotate.js"))) ?? dev;
280
326
  }
281
327
 
282
- // packages/server/src/app.ts
283
- var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
328
+ // packages/server/src/version.ts
329
+ import { existsSync as existsSync3, readFileSync } from "node:fs";
330
+ import path3 from "node:path";
331
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
332
+ function chambaVersion() {
333
+ let dir = path3.dirname(fileURLToPath3(import.meta.url));
334
+ for (; ; ) {
335
+ const manifest = path3.join(dir, "package.json");
336
+ if (existsSync3(manifest)) {
337
+ try {
338
+ const { version } = JSON.parse(readFileSync(manifest, "utf8"));
339
+ if (typeof version === "string" && version) return version;
340
+ } catch {
341
+ }
342
+ }
343
+ const parent = path3.dirname(dir);
344
+ if (parent === dir) return "0.0.0-dev";
345
+ dir = parent;
346
+ }
347
+ }
284
348
 
285
- // packages/server/src/lifecycle.ts
286
- import { open, readFile as readFile2, rm } from "node:fs/promises";
349
+ // packages/server/src/sessions/meta.ts
350
+ import { readdir, readFile as readFile2 } from "node:fs/promises";
351
+
352
+ // packages/server/src/sessions/atomic.ts
353
+ import { randomUUID } from "node:crypto";
354
+ import { rename, rm, writeFile } from "node:fs/promises";
355
+ async function writeJsonAtomic(file, value) {
356
+ const tmp = `${file}.${randomUUID()}.tmp`;
357
+ try {
358
+ await writeFile(tmp, `${JSON.stringify(value, null, 2)}
359
+ `, "utf8");
360
+ await rename(tmp, file);
361
+ } catch (err) {
362
+ await rm(tmp, { force: true }).catch(() => void 0);
363
+ throw err;
364
+ }
365
+ }
287
366
 
288
367
  // packages/server/src/sessions/paths.ts
289
- import { existsSync as existsSync3, realpathSync } from "node:fs";
368
+ import { existsSync as existsSync4, realpathSync } from "node:fs";
290
369
  import { appendFile, mkdir, readFile } from "node:fs/promises";
291
- import path3 from "node:path";
370
+ import path4 from "node:path";
292
371
  function defaultContext() {
293
372
  return { cwd: process.cwd(), env: process.env };
294
373
  }
@@ -301,70 +380,70 @@ function findExistingHome(ctx = defaultContext()) {
301
380
  const inside = enclosingChamba(ctx.cwd);
302
381
  if (inside) return canonicalize(inside);
303
382
  const root = gitRoot(ctx.cwd);
304
- let cursor = path3.resolve(ctx.cwd);
383
+ let cursor = path4.resolve(ctx.cwd);
305
384
  while (true) {
306
- if (existsSync3(path3.join(cursor, ".chamba"))) return canonicalize(path3.join(cursor, ".chamba"));
385
+ if (existsSync4(path4.join(cursor, ".chamba"))) return canonicalize(path4.join(cursor, ".chamba"));
307
386
  if (!root) break;
308
387
  if (cursor === root) break;
309
- const parent = path3.dirname(cursor);
388
+ const parent = path4.dirname(cursor);
310
389
  if (parent === cursor) break;
311
390
  cursor = parent;
312
391
  }
313
392
  return null;
314
393
  }
315
394
  function plannedHome(ctx = defaultContext()) {
316
- const root = gitRoot(ctx.cwd) ?? path3.resolve(ctx.cwd);
317
- return canonicalize(path3.join(root, ".chamba"));
395
+ const root = gitRoot(ctx.cwd) ?? path4.resolve(ctx.cwd);
396
+ return canonicalize(path4.join(root, ".chamba"));
318
397
  }
319
398
  function gitRoot(cwd) {
320
- let cursor = path3.resolve(cwd);
399
+ let cursor = path4.resolve(cwd);
321
400
  while (true) {
322
- if (existsSync3(path3.join(cursor, ".git"))) return cursor;
323
- const parent = path3.dirname(cursor);
401
+ if (existsSync4(path4.join(cursor, ".git"))) return cursor;
402
+ const parent = path4.dirname(cursor);
324
403
  if (parent === cursor) return null;
325
404
  cursor = parent;
326
405
  }
327
406
  }
328
407
  function enclosingChamba(cwd) {
329
- const parts = path3.resolve(cwd).split(path3.sep);
408
+ const parts = path4.resolve(cwd).split(path4.sep);
330
409
  const index = parts.lastIndexOf(".chamba");
331
410
  if (index < 0) return null;
332
- return parts.slice(0, index + 1).join(path3.sep) || path3.sep;
411
+ return parts.slice(0, index + 1).join(path4.sep) || path4.sep;
333
412
  }
334
413
  function canonicalize(p) {
335
- const resolved = path3.resolve(p);
414
+ const resolved = path4.resolve(p);
336
415
  let existing = resolved;
337
416
  const tail = [];
338
- while (!existsSync3(existing)) {
339
- tail.unshift(path3.basename(existing));
340
- const parent = path3.dirname(existing);
417
+ while (!existsSync4(existing)) {
418
+ tail.unshift(path4.basename(existing));
419
+ const parent = path4.dirname(existing);
341
420
  if (parent === existing) return resolved;
342
421
  existing = parent;
343
422
  }
344
- return path3.join(realpathSync(existing), ...tail);
423
+ return path4.join(realpathSync(existing), ...tail);
345
424
  }
346
425
  function sessionsDir(home) {
347
- return path3.join(home, "sessions");
426
+ return path4.join(home, "sessions");
348
427
  }
349
428
  function sessionDir(home, id) {
350
- return path3.join(sessionsDir(home), id);
429
+ return path4.join(sessionsDir(home), id);
351
430
  }
352
431
  function metaPath(home, id) {
353
- return path3.join(sessionDir(home, id), "meta.json");
432
+ return path4.join(sessionDir(home, id), "meta.json");
354
433
  }
355
434
  function pidfilePath(home) {
356
- return path3.join(home, "server.json");
435
+ return path4.join(home, "server.json");
357
436
  }
358
437
  function agentStatePath(home) {
359
- return path3.join(home, "agent-state.json");
438
+ return path4.join(home, "agent-state.json");
360
439
  }
361
440
  async function ensureChambaHome(home) {
362
441
  await mkdir(sessionsDir(home), { recursive: true });
363
442
  await ensureGitignored(home);
364
443
  }
365
444
  async function ensureGitignored(home) {
366
- if (path3.basename(home) !== ".chamba") return;
367
- const gitignore = path3.join(path3.dirname(home), ".gitignore");
445
+ if (path4.basename(home) !== ".chamba") return;
446
+ const gitignore = path4.join(path4.dirname(home), ".gitignore");
368
447
  const entry = ".chamba/";
369
448
  try {
370
449
  let content = "";
@@ -385,13 +464,37 @@ async function ensureGitignored(home) {
385
464
  }
386
465
  }
387
466
 
467
+ // packages/server/src/sessions/meta.ts
468
+ async function readMeta(home, id) {
469
+ try {
470
+ const raw = await readFile2(metaPath(home, id), "utf8");
471
+ return sessionMetaSchema.parse(JSON.parse(raw));
472
+ } catch (err) {
473
+ if (err.code === "ENOENT") return null;
474
+ throw err;
475
+ }
476
+ }
477
+ async function listSessionIds(home) {
478
+ try {
479
+ const entries = await readdir(sessionsDir(home), { withFileTypes: true });
480
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
481
+ } catch (err) {
482
+ if (err.code === "ENOENT") return [];
483
+ throw err;
484
+ }
485
+ }
486
+
487
+ // packages/server/src/app.ts
488
+ var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
489
+
388
490
  // packages/server/src/lifecycle.ts
491
+ import { open, readFile as readFile3, rm as rm2 } from "node:fs/promises";
389
492
  function readServerInfo(home) {
390
493
  return readPidfile(pidfilePath(home));
391
494
  }
392
495
  async function readPidfile(file) {
393
496
  try {
394
- const raw = await readFile2(file, "utf8");
497
+ const raw = await readFile3(file, "utf8");
395
498
  const parsed = JSON.parse(raw);
396
499
  if (typeof parsed.pid === "number" && typeof parsed.port === "number") return parsed;
397
500
  return null;
@@ -441,48 +544,6 @@ function parsePort(value, source) {
441
544
  return port;
442
545
  }
443
546
 
444
- // packages/server/src/version.ts
445
- import { existsSync as existsSync4, readFileSync } from "node:fs";
446
- import path4 from "node:path";
447
- import { fileURLToPath as fileURLToPath3 } from "node:url";
448
- function chambaVersion() {
449
- let dir = path4.dirname(fileURLToPath3(import.meta.url));
450
- for (; ; ) {
451
- const manifest = path4.join(dir, "package.json");
452
- if (existsSync4(manifest)) {
453
- try {
454
- const { version } = JSON.parse(readFileSync(manifest, "utf8"));
455
- if (typeof version === "string" && version) return version;
456
- } catch {
457
- }
458
- }
459
- const parent = path4.dirname(dir);
460
- if (parent === dir) return "0.0.0-dev";
461
- dir = parent;
462
- }
463
- }
464
-
465
- // packages/server/src/sessions/meta.ts
466
- import { readdir, readFile as readFile3, rename, rm as rm2, writeFile } from "node:fs/promises";
467
- async function readMeta(home, id) {
468
- try {
469
- const raw = await readFile3(metaPath(home, id), "utf8");
470
- return sessionMetaSchema.parse(JSON.parse(raw));
471
- } catch (err) {
472
- if (err.code === "ENOENT") return null;
473
- throw err;
474
- }
475
- }
476
- async function listSessionIds(home) {
477
- try {
478
- const entries = await readdir(sessionsDir(home), { withFileTypes: true });
479
- return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
480
- } catch (err) {
481
- if (err.code === "ENOENT") return [];
482
- throw err;
483
- }
484
- }
485
-
486
547
  // packages/server/src/index.ts
487
548
  var DEFAULT_IDLE_MS = 5 * 6e4;
488
549
 
@@ -522,11 +583,13 @@ var ChannelError = class extends Error {
522
583
  this.status = status2;
523
584
  this.name = "ChannelError";
524
585
  }
586
+ status;
525
587
  };
526
588
  var Client = class {
527
589
  constructor(baseUrl) {
528
590
  this.baseUrl = baseUrl;
529
591
  }
592
+ baseUrl;
530
593
  /** Liveness probe with a short deadline; never throws - `false` means "spawn it". */
531
594
  async isUp(timeoutMs = 800) {
532
595
  try {
@@ -539,6 +602,17 @@ var Client = class {
539
602
  async health() {
540
603
  return this.json("/api/health", { method: "GET", timeoutMs: 2e3 }, healthResSchema);
541
604
  }
605
+ /** True when a server answers here *and* reports `home` as the workspace it
606
+ * owns - the identity check that keeps commands off a foreign server sharing
607
+ * the port (derived ports can collide across projects). Never throws. */
608
+ async ownsHome(home) {
609
+ if (!await this.isUp()) return false;
610
+ try {
611
+ return (await this.health()).home === home;
612
+ } catch {
613
+ return false;
614
+ }
615
+ }
542
616
  async create(req) {
543
617
  const res = await this.json(
544
618
  "/api/sessions",
@@ -575,28 +649,10 @@ var Client = class {
575
649
  );
576
650
  return res.event;
577
651
  }
578
- /** Post a structured interview question card (spec sessions). Agent-sourced. */
579
- async postQuestionCard(id, body) {
580
- const res = await this.json(
581
- `/api/sessions/${encodeURIComponent(id)}/cards`,
582
- { method: "POST", body, timeoutMs: 5e3 },
583
- eventResSchema
584
- );
585
- return res.event;
586
- }
587
- /** Record a decision-log entry (confirmed vs. assumed). Agent-sourced. */
588
- async postDecision(id, body) {
652
+ /** Show or revise a page (self-contained HTML for the Illustration tab). */
653
+ async showPage(id, body) {
589
654
  const res = await this.json(
590
- `/api/sessions/${encodeURIComponent(id)}/decisions`,
591
- { method: "POST", body, timeoutMs: 5e3 },
592
- eventResSchema
593
- );
594
- return res.event;
595
- }
596
- /** Author or revise a draft/mock/diagram artifact by posting its source text. */
597
- async postArtifact(id, body) {
598
- const res = await this.json(
599
- `/api/sessions/${encodeURIComponent(id)}/artifacts`,
655
+ `/api/sessions/${encodeURIComponent(id)}/pages`,
600
656
  { method: "POST", body, timeoutMs: 1e4 },
601
657
  eventResSchema
602
658
  );
@@ -639,13 +695,6 @@ var Client = class {
639
695
  pollResSchema
640
696
  );
641
697
  }
642
- async attach(id, tail) {
643
- return this.json(
644
- `/api/sessions/${encodeURIComponent(id)}/attach?tail=${tail}`,
645
- { method: "GET", timeoutMs: 5e3 },
646
- attachDigestSchema
647
- );
648
- }
649
698
  async end(id) {
650
699
  const res = await this.json(
651
700
  `/api/sessions/${encodeURIComponent(id)}/end`,
@@ -680,18 +729,12 @@ function bodyInit(body) {
680
729
  return { headers: { "content-type": "application/json" }, body: JSON.stringify(body) };
681
730
  }
682
731
  async function toError(res) {
683
- let message = `${res.status} ${res.statusText}`;
684
- try {
685
- const body = await res.json();
686
- if (typeof body.error === "string") message = body.error;
687
- } catch {
688
- }
732
+ const message = await errorBodyMessage(res) ?? `${res.status} ${res.statusText}`;
689
733
  return new ChannelError(res.status, message);
690
734
  }
691
735
 
692
736
  // packages/cli/src/output.ts
693
737
  import path6 from "node:path";
694
- import { z as z10 } from "zod";
695
738
  function out(message) {
696
739
  process.stdout.write(`${message}
697
740
  `);
@@ -700,42 +743,35 @@ function note(message) {
700
743
  process.stderr.write(`${message}
701
744
  `);
702
745
  }
703
- function eventText(event) {
704
- const payload = event.payload;
705
- if (payload && typeof payload === "object") {
706
- const record = payload;
707
- if (typeof record.text === "string") return record.text;
708
- if (typeof record.reason === "string") return record.reason;
709
- }
710
- return "";
711
- }
712
- function eventAttachments(event) {
713
- if (!event.payload || typeof event.payload !== "object") return [];
714
- const parsed = z10.array(attachmentSchema).safeParse(event.payload.attachments);
715
- return parsed.success ? parsed.data : [];
716
- }
717
746
  function formatSystemEvent(event) {
718
747
  const parsed = systemPayloadSchema.safeParse(event.payload);
719
748
  if (!parsed.success) return `\xB7 ${eventText(event)}`;
720
- const { reason, to, kind } = parsed.data;
721
- switch (reason) {
722
- case "user-moved-away":
723
- return to ? `\xB7 You moved to another session (${to})` : "\xB7 The user moved away";
749
+ switch (parsed.data.reason) {
724
750
  case "session-ended":
725
751
  return "\xB7 Session ended";
726
752
  case "session-created":
727
753
  return "\xB7 Session created";
728
- case "kind-chosen":
729
- return kind ? `\xB7 Session set to ${kind}` : "\xB7 Session kind chosen";
730
- default:
731
- return `\xB7 ${reason}`;
732
754
  }
733
755
  }
756
+ function eventMarkers(event) {
757
+ if (!event.payload || typeof event.payload !== "object") return "";
758
+ const record = event.payload;
759
+ const parts = [];
760
+ const ref = replyRefSchema.safeParse(record.replyTo);
761
+ if (ref.success) {
762
+ const who = sourceLabel(ref.data.source);
763
+ const snippet = ref.data.snippet ? `: "${ref.data.snippet}"` : "";
764
+ parts.push(`[re #${ref.data.seq} ${who}${snippet}]`);
765
+ }
766
+ if (record.answerIn === "terminal") parts.push("[answer in terminal]");
767
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
768
+ }
734
769
  function formatEvent(event) {
735
770
  if (event.source === "system") return formatSystemEvent(event);
736
- const who = event.source === "user" ? "you" : "agent";
771
+ const who = sourceLabel(event.source);
772
+ const markers = eventMarkers(event);
737
773
  const text = eventText(event);
738
- return text ? `${who}: ${text}` : `${who}:`;
774
+ return text ? `${who}${markers}: ${text}` : `${who}${markers}:`;
739
775
  }
740
776
  function annotationItems(event) {
741
777
  const parsed = annotationBatchPayloadSchema.safeParse(event.payload);
@@ -743,7 +779,7 @@ function annotationItems(event) {
743
779
  }
744
780
  function formatAnnotationBatch(event, home) {
745
781
  const items = annotationItems(event);
746
- const who = event.source === "user" ? "you" : "agent";
782
+ const who = sourceLabel(event.source);
747
783
  const lines = [`${who} [${items.length} annotation${items.length === 1 ? "" : "s"}]:`];
748
784
  items.forEach((item, index) => {
749
785
  const { target } = item;
@@ -765,34 +801,12 @@ function formatAnnotationBatch(event, home) {
765
801
  });
766
802
  return lines;
767
803
  }
768
- function formatQuestionCard(event) {
769
- const parsed = questionCardPayloadSchema.safeParse(event.payload);
770
- if (!parsed.success) return [formatEvent(event)];
771
- const card = parsed.data;
772
- const lines = [`agent [question]: ${card.question}`];
773
- card.options.forEach((option, index) => {
774
- const rec = option.recommended ? " (recommended)" : "";
775
- lines.push(` ${index + 1}. ${option.label}${rec} - ${option.description}`);
776
- });
777
- lines.push(
778
- ` (the user can also type a custom answer${card.stopHint ? ` or ${card.stopHint}` : ", or stop anytime"})`
779
- );
780
- return lines;
781
- }
782
- function formatDecision(event) {
783
- const parsed = decisionPayloadSchema.safeParse(event.payload);
784
- if (!parsed.success) return formatEvent(event);
785
- const { status: status2, summary, detail } = parsed.data;
786
- return `agent [decision: ${status2}]: ${summary}${detail ? ` - ${detail}` : ""}`;
787
- }
788
- function formatArtifact(event, home) {
789
- const parsed = artifactRefSchema.safeParse(
790
- event.payload?.artifact
791
- );
804
+ function formatPage(event, home) {
805
+ const parsed = pageRefSchema.safeParse(event.payload?.page);
792
806
  if (!parsed.success) return formatEvent(event);
793
807
  const ref = parsed.data;
794
808
  const abs = path6.join(sessionDir(home, event.sessionId), ref.relPath);
795
- return `agent [artifact ${ref.kind} "${ref.name}"] -> ${abs}`;
809
+ return `agent [page "${ref.name}"] -> ${abs}`;
796
810
  }
797
811
  function printEvents(events, home) {
798
812
  for (const event of events) {
@@ -800,16 +814,8 @@ function printEvents(events, home) {
800
814
  for (const line of formatAnnotationBatch(event, home)) out(line);
801
815
  continue;
802
816
  }
803
- if (event.type === QUESTION_CARD) {
804
- for (const line of formatQuestionCard(event)) out(line);
805
- continue;
806
- }
807
- if (event.type === DECISION) {
808
- out(formatDecision(event));
809
- continue;
810
- }
811
- if (event.type === ARTIFACT) {
812
- out(formatArtifact(event, home));
817
+ if (event.type === PAGE) {
818
+ out(formatPage(event, home));
813
819
  continue;
814
820
  }
815
821
  out(formatEvent(event));
@@ -822,47 +828,41 @@ function formatSession(meta, presence, current) {
822
828
  const marker = current ? "*" : " ";
823
829
  const title = meta.title ? ` "${meta.title}"` : "";
824
830
  const live = presence ? ` ${presence}` : "";
825
- return `${marker} ${meta.id} ${meta.status}${live} ${meta.kind ?? "unset"}${title}`;
831
+ return `${marker} ${meta.id} ${meta.status}${live}${title}`;
826
832
  }
827
833
 
828
- // packages/cli/src/commands/artifact.ts
829
- import { readFile as readFile6 } from "node:fs/promises";
830
-
831
834
  // packages/cli/src/state.ts
832
- import { randomUUID } from "node:crypto";
833
- import { mkdir as mkdir2, readFile as readFile5, rename as rename2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
835
+ import { mkdir as mkdir2, readFile as readFile5 } from "node:fs/promises";
834
836
  import path7 from "node:path";
835
- import { z as z11 } from "zod";
836
- var agentStateSchema = z11.object({
837
- currentSessionId: z11.string().optional(),
838
- cursors: z11.record(z11.number().int().nonnegative()).default({})
837
+ import { z as z10 } from "zod";
838
+ var agentStateSchema = z10.object({
839
+ currentSessionId: z10.string().optional(),
840
+ bindings: z10.record(z10.string(), z10.string()).default({}),
841
+ cursors: z10.record(z10.string(), z10.number().int().nonnegative()).default({})
839
842
  });
843
+ var EMPTY = { bindings: {}, cursors: {} };
840
844
  async function readState(home = resolveHome()) {
841
845
  try {
842
846
  const raw = await readFile5(agentStatePath(home), "utf8");
843
847
  return agentStateSchema.parse(JSON.parse(raw));
844
848
  } catch {
845
- return { cursors: {} };
849
+ return EMPTY;
846
850
  }
847
851
  }
848
852
  async function writeState(state, home = resolveHome()) {
849
853
  const file = agentStatePath(home);
850
854
  await mkdir2(path7.dirname(file), { recursive: true });
851
- const tmp = `${file}.${randomUUID()}.tmp`;
852
- try {
853
- await writeFile2(tmp, `${JSON.stringify(state, null, 2)}
854
- `, "utf8");
855
- await rename2(tmp, file);
856
- } catch (err) {
857
- await rm3(tmp, { force: true }).catch(() => void 0);
858
- throw err;
859
- }
855
+ await writeJsonAtomic(file, state);
860
856
  }
861
857
  function cursorFor(state, id) {
862
858
  return state.cursors[id] ?? 0;
863
859
  }
864
- function withSession(state, id, cursor) {
865
- return { currentSessionId: id, cursors: { ...state.cursors, [id]: cursor } };
860
+ function withSession(state, id, cursor, key) {
861
+ return {
862
+ currentSessionId: id,
863
+ bindings: key ? { ...state.bindings, [key]: id } : state.bindings,
864
+ cursors: { ...state.cursors, [id]: cursor }
865
+ };
866
866
  }
867
867
 
868
868
  // packages/cli/src/commands/context.ts
@@ -872,13 +872,22 @@ var UsageError = class extends Error {
872
872
  this.name = "UsageError";
873
873
  }
874
874
  };
875
- async function resolveSessionId(ctx) {
875
+ function agentKey(env = process.env) {
876
+ return env.CHAMBA_SESSION_KEY || env.CLAUDE_CODE_SESSION_ID || void 0;
877
+ }
878
+ async function resolveSessionId(ctx, env = process.env) {
876
879
  const flag = ctx.flags.session;
877
880
  if (flag === "true" || flag === "") {
878
881
  throw new UsageError("`--session` needs a session id, e.g. --session <id>");
879
882
  }
880
883
  if (flag) return flag;
881
884
  const state = await readState(ctx.home);
885
+ const key = agentKey(env);
886
+ if (key) {
887
+ const bound = state.bindings[key];
888
+ if (bound) return bound;
889
+ throw new UsageError("no session bound to this agent - run `chamba open`");
890
+ }
882
891
  if (state.currentSessionId) return state.currentSessionId;
883
892
  throw new UsageError("no current session - run `chamba open` or pass --session <id>");
884
893
  }
@@ -888,116 +897,6 @@ function intFlag(value, fallback) {
888
897
  return Number.isInteger(n) && n >= 0 ? n : fallback;
889
898
  }
890
899
 
891
- // packages/cli/src/commands/artifact.ts
892
- async function artifact(ctx) {
893
- const kind = artifactKindSchema.safeParse(ctx.flags.kind);
894
- if (!kind.success) {
895
- throw new UsageError(
896
- "usage: chamba artifact --kind draft|mock|diagram --name <name> --file <path>"
897
- );
898
- }
899
- const name = ctx.flags.name?.trim();
900
- if (!name || name === "true") throw new UsageError("chamba artifact needs --name <name>");
901
- const file = ctx.flags.file;
902
- if (!file || file === "true") throw new UsageError("chamba artifact needs --file <path>");
903
- let content;
904
- try {
905
- content = await readFile6(file, "utf8");
906
- } catch (err) {
907
- throw new UsageError(`could not read ${file}: ${err.message}`);
908
- }
909
- const result = postArtifactReqSchema.safeParse({ kind: kind.data, name, content });
910
- if (!result.success) {
911
- throw new UsageError(
912
- `invalid artifact: ${result.error.issues.map((i) => i.message).join("; ")}`
913
- );
914
- }
915
- const id = await resolveSessionId(ctx);
916
- const event = await ctx.client.postArtifact(id, result.data);
917
- const ref = event.payload.artifact;
918
- note(`posted ${kind.data} "${name}" to ${id}${ref?.relPath ? ` (${ref.relPath})` : ""}`);
919
- return 0;
920
- }
921
-
922
- // packages/cli/src/commands/ask.ts
923
- import { randomUUID as randomUUID2 } from "node:crypto";
924
- import { readFile as readFile7 } from "node:fs/promises";
925
- async function ask(ctx) {
926
- const source = await readCardSource(ctx);
927
- let parsed;
928
- try {
929
- parsed = JSON.parse(source);
930
- } catch (err) {
931
- throw new UsageError(`--json is not valid JSON: ${err.message}`);
932
- }
933
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && !("cardId" in parsed)) {
934
- parsed.cardId = randomUUID2();
935
- }
936
- const result = postQuestionCardReqSchema.safeParse(parsed);
937
- if (!result.success) {
938
- throw new UsageError(
939
- `invalid question card: ${result.error.issues.map((i) => `${i.path.join(".")} ${i.message}`).join("; ")}`
940
- );
941
- }
942
- const id = await resolveSessionId(ctx);
943
- await ctx.client.postQuestionCard(id, result.data);
944
- note(`asked ${id}: "${result.data.question}" (${result.data.options.length} options)`);
945
- return 0;
946
- }
947
- async function readCardSource(ctx) {
948
- if (ctx.flags.json && ctx.flags.json !== "true") return ctx.flags.json;
949
- const file = ctx.positionals[0];
950
- if (file) {
951
- try {
952
- return await readFile7(file, "utf8");
953
- } catch (err) {
954
- throw new UsageError(`could not read card file ${file}: ${err.message}`);
955
- }
956
- }
957
- throw new UsageError(
958
- `usage: chamba ask --json '{"question":"...","options":[...]}' | <file.json>`
959
- );
960
- }
961
-
962
- // packages/cli/src/commands/attach.ts
963
- async function attach(ctx) {
964
- const flagId = ctx.flags.session === "true" ? void 0 : ctx.flags.session || void 0;
965
- const id = ctx.positionals[0] ?? flagId;
966
- if (!id) throw new UsageError("usage: chamba attach <session-id>");
967
- const tail = intFlag(ctx.flags.tail, 20);
968
- const digest = await ctx.client.attach(id, tail);
969
- note(`session ${digest.meta.id} - ${digest.meta.status}, agent ${digest.presence}`);
970
- if (digest.fetchMore.hasMore) note(`(${digest.fetchMore.hint})`);
971
- printEvents(digest.tail, ctx.home);
972
- const state = await readState(ctx.home);
973
- await writeState(withSession(state, digest.meta.id, digest.cursor), ctx.home);
974
- note(`now current - poll from cursor ${digest.cursor}`);
975
- return 0;
976
- }
977
-
978
- // packages/cli/src/commands/decision.ts
979
- async function decision(ctx) {
980
- const summary = ctx.flags.summary?.trim();
981
- if (!summary || summary === "true") {
982
- throw new UsageError(
983
- 'usage: chamba decision --status confirmed|assumed --summary "..." [--detail "..."]'
984
- );
985
- }
986
- const detail = ctx.flags.detail && ctx.flags.detail !== "true" ? ctx.flags.detail : void 0;
987
- const result = postDecisionReqSchema.safeParse({
988
- summary,
989
- status: ctx.flags.status,
990
- ...detail ? { detail } : {}
991
- });
992
- if (!result.success) {
993
- throw new UsageError("--status must be `confirmed` or `assumed`");
994
- }
995
- const id = await resolveSessionId(ctx);
996
- await ctx.client.postDecision(id, result.data);
997
- note(`recorded ${result.data.status} decision on ${id}`);
998
- return 0;
999
- }
1000
-
1001
900
  // packages/cli/src/commands/end.ts
1002
901
  async function end(ctx) {
1003
902
  const id = await resolveSessionId(ctx);
@@ -1006,90 +905,23 @@ async function end(ctx) {
1006
905
  return 0;
1007
906
  }
1008
907
 
1009
- // packages/cli/src/commands/export.ts
1010
- import { copyFile, mkdir as mkdir3, readFile as readFile8, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
1011
- import path8 from "node:path";
1012
- async function exportCmd(ctx) {
1013
- const dest = ctx.positionals[0];
1014
- if (!dest) throw new UsageError("usage: chamba export <dest> [--session <id>]");
1015
- const id = await resolveSessionId(ctx);
1016
- const { events } = await ctx.client.events(id, 0);
1017
- const summary = await assemblePackage({
1018
- events,
1019
- sessionDir: sessionDir(ctx.home, id),
1020
- dest: path8.resolve(dest)
1021
- });
1022
- note(
1023
- `exported ${id} -> ${path8.resolve(dest)} (spec.md${summary.hasDraft ? "" : " placeholder"}, ${summary.assets.length} asset${summary.assets.length === 1 ? "" : "s"}, ${summary.decisions} decision${summary.decisions === 1 ? "" : "s"})`
1024
- );
1025
- out(path8.resolve(dest));
1026
- return 0;
1027
- }
1028
- async function assemblePackage(input) {
1029
- const artifacts = /* @__PURE__ */ new Map();
1030
- let canonicalDraft;
1031
- const decisions = [];
1032
- for (const event of input.events) {
1033
- if (event.type === ARTIFACT) {
1034
- const parsed = artifactRefSchema.safeParse(
1035
- event.payload?.artifact
1036
- );
1037
- if (!parsed.success) continue;
1038
- const ref = parsed.data;
1039
- artifacts.set(`${ref.kind}:${ref.name}`, ref);
1040
- if (ref.kind === "draft") canonicalDraft = ref;
1041
- } else if (event.type === DECISION) {
1042
- const parsed = decisionPayloadSchema.safeParse(event.payload);
1043
- if (parsed.success) decisions.push(parsed.data);
1044
- }
1045
- }
1046
- await mkdir3(input.dest, { recursive: true });
1047
- const assetsDir = path8.join(input.dest, "assets");
1048
- await rm4(assetsDir, { recursive: true, force: true });
1049
- const draftBody = canonicalDraft ? await readArtifact(input.sessionDir, canonicalDraft) : void 0;
1050
- await writeFile3(
1051
- path8.join(input.dest, "spec.md"),
1052
- draftBody ?? "# Spec\n\n_No draft was authored in this session._\n",
1053
- "utf8"
1054
- );
1055
- const assets = [];
1056
- for (const ref of artifacts.values()) {
1057
- if (ref === canonicalDraft) continue;
1058
- const src = path8.join(input.sessionDir, ref.relPath);
1059
- const base = path8.basename(ref.relPath);
1060
- try {
1061
- await mkdir3(assetsDir, { recursive: true });
1062
- await copyFile(src, path8.join(assetsDir, base));
1063
- assets.push(base);
1064
- } catch {
1065
- }
908
+ // packages/cli/src/commands/get.ts
909
+ async function get(ctx) {
910
+ const input = ctx.positionals.join(" ").trim();
911
+ if (!input) {
912
+ throw new UsageError("usage: chamba get <ref> (42, <session>#42, or a pasted token)");
913
+ }
914
+ const ref = parseRef(input);
915
+ if (!ref) throw new UsageError(`not a message reference: ${input}`);
916
+ const id = ref.session ?? await resolveSessionId(ctx);
917
+ const res = await ctx.client.events(id, ref.seq, 1);
918
+ const event = res.events[0];
919
+ if (!event || event.seq !== ref.seq) {
920
+ note(`no event #${ref.seq} in session ${id} on this server`);
921
+ return 1;
1066
922
  }
1067
- await writeFile3(path8.join(input.dest, "decisions.md"), renderDecisions(decisions), "utf8");
1068
- return { hasDraft: Boolean(draftBody), assets, decisions: decisions.length };
1069
- }
1070
- async function readArtifact(dir, ref) {
1071
- try {
1072
- return await readFile8(path8.join(dir, ref.relPath), "utf8");
1073
- } catch {
1074
- return void 0;
1075
- }
1076
- }
1077
- function renderDecisions(decisions) {
1078
- const confirmed = decisions.filter((d) => d.status === "confirmed");
1079
- const assumed = decisions.filter((d) => d.status === "assumed");
1080
- const section = (items) => items.length === 0 ? "_None._\n" : `${items.map((d) => `- ${d.summary}${d.detail ? `
1081
- ${d.detail}` : ""}`).join("\n")}
1082
- `;
1083
- return [
1084
- "# Decision log",
1085
- "",
1086
- "## Confirmed",
1087
- "",
1088
- section(confirmed),
1089
- "## Assumed / open questions",
1090
- "",
1091
- section(assumed)
1092
- ].join("\n");
923
+ printEvents([event], ctx.home);
924
+ return 0;
1093
925
  }
1094
926
 
1095
927
  // packages/cli/src/commands/help.ts
@@ -1100,45 +932,36 @@ Usage:
1100
932
  chamba init [dir] create the .chamba workspace (at the git root, or
1101
933
  [--here] <dir>, or the current dir with --here); prints its
1102
934
  location and per-project port
1103
- chamba open [--kind K] start a session (K: everyday | spec), print its URL
1104
- [--title T] (creates the workspace at the repo root if none yet)
1105
- chamba poll wait for the next human/system events, blocking across
1106
- [--wait ms] quiet windows on its own until a batch arrives (it may
1107
- [--for ms] include "user moved away" -> attach, or "session ended"
1108
- [--once] -> stop). After a quiet stretch (--for, default 4m) it
1109
- prints a heartbeat and exits 0 - just re-run. --once does
1110
- one bounded check instead (scripts/tests); --wait is the
1111
- per-request window (max 30s)
935
+ chamba open [--title T] open a session and print its URL (creates the
936
+ [--new] workspace at the repo root if none yet). Reconnects
937
+ [--last] to this agent's bound session when one is active;
938
+ --new always creates, --last rebinds to the newest
939
+ active session
940
+ chamba poll wait for the next human/system events, blocking
941
+ [--wait ms] indefinitely until a batch arrives (zero cost while
942
+ [--for ms] quiet). --for bounds the wait: after a quiet stretch
943
+ [--once] it prints a heartbeat and exits 0 - just re-run.
944
+ --once does one bounded check instead (scripts/
945
+ tests); --wait is the per-request window (max 30s)
1112
946
  chamba reply <text> send a line from the agent into the session
947
+ [--to <seq>] mark it as answering a specific earlier event
1113
948
  [--file a,b] attach files by path (comma-separated); --image is
1114
949
  [--image a,b] an alias. text is optional when files are attached
1115
- chamba attach <id> load a session's recent tail and make it current
1116
- [--tail N]
950
+ chamba get <ref> print one event by reference: 42, <session>#42, or
951
+ a pasted [chamba <session>#42 "..."] token
952
+ chamba show <file.html> render a self-contained HTML page in the browser's
953
+ [--name N] Illustration tab. Re-show the same name to revise
1117
954
  chamba end end the current session
1118
955
  chamba stop stop this project's server (it otherwise self-stops
1119
956
  after an idle spell); the next open starts a fresh one
1120
957
 
1121
- Spec sessions (open with --kind spec):
1122
- chamba ask --json '{...}' post a question card (question + 2-6 options with
1123
- | <file.json> descriptions, optional "recommended"); a cardId is
1124
- created for you. The answer returns as a message
1125
- chamba decision record a decision: --status confirmed|assumed
1126
- --status S --summary "..." [--detail "..."]
1127
- --summary T
1128
- chamba artifact author/revise a draft (markdown), mock (HTML), or
1129
- --kind K diagram (mermaid): --kind draft|mock|diagram
1130
- --name N --name <name> --file <path>. Re-post to revise
1131
- --file P
1132
- chamba export <dest> copy the finalized package (spec.md + decisions.md
1133
- + assets/) to <dest> (e.g. docs/)
1134
-
1135
958
  chamba skill install install the chamba skill (/chamba) into
1136
959
  [--global] ./.claude/skills, or ~/.claude/skills with --global
1137
960
  chamba --version print the chamba version
1138
961
  chamba help show this help
1139
962
 
1140
963
  Global:
1141
- --session <id> act on a specific session instead of the current one
964
+ --session <id> act on a specific session instead of the bound one
1142
965
  --port <n> server port (default: CHAMBA_PORT, else a per-project
1143
966
  port derived from the workspace location)`;
1144
967
  function help(_ctx) {
@@ -1147,7 +970,7 @@ function help(_ctx) {
1147
970
  }
1148
971
 
1149
972
  // packages/cli/src/commands/init.ts
1150
- import path9 from "node:path";
973
+ import path8 from "node:path";
1151
974
  async function init(ctx) {
1152
975
  const target = resolveTarget(ctx);
1153
976
  const existing = findExistingHome({ cwd: target, env: {} });
@@ -1158,20 +981,20 @@ async function init(ctx) {
1158
981
  if (existing && ctx.flags.here) {
1159
982
  note(`note: shadowing an existing workspace at ${existing}`);
1160
983
  }
1161
- await ensureChambaHome(path9.join(target, ".chamba"));
1162
- const home = findExistingHome({ cwd: target, env: {} }) ?? path9.join(target, ".chamba");
984
+ await ensureChambaHome(path8.join(target, ".chamba"));
985
+ const home = findExistingHome({ cwd: target, env: {} }) ?? path8.join(target, ".chamba");
1163
986
  report("created chamba workspace", home);
1164
- if (path9.basename(home) !== ".chamba") {
987
+ if (path8.basename(home) !== ".chamba") {
1165
988
  note("warning: home is not a .chamba directory, so it will not be auto-gitignored");
1166
989
  }
1167
990
  return 0;
1168
991
  }
1169
992
  function resolveTarget(ctx) {
1170
993
  const explicit = ctx.positionals[0];
1171
- if (explicit) return path9.resolve(explicit);
994
+ if (explicit) return path8.resolve(explicit);
1172
995
  const root = gitRoot(process.cwd());
1173
996
  if (root) return root;
1174
- if (ctx.flags.here) return path9.resolve(process.cwd());
997
+ if (ctx.flags.here) return path8.resolve(process.cwd());
1175
998
  throw new UsageError(
1176
999
  "no .git found here - pass a directory (`chamba init <dir>`) or `chamba init --here` to choose where .chamba lives"
1177
1000
  );
@@ -1186,7 +1009,7 @@ function report(headline, home) {
1186
1009
  import { spawn, spawnSync } from "node:child_process";
1187
1010
  import { existsSync as existsSync5 } from "node:fs";
1188
1011
  import { createRequire } from "node:module";
1189
- import path10 from "node:path";
1012
+ import path9 from "node:path";
1190
1013
  import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
1191
1014
  var require2 = createRequire(import.meta.url);
1192
1015
  async function ensureServer(client, target) {
@@ -1220,17 +1043,17 @@ function spawnServer(target) {
1220
1043
  child.unref();
1221
1044
  }
1222
1045
  function serverLaunchArgs() {
1223
- const here = path10.dirname(fileURLToPath4(import.meta.url));
1224
- const packagedServer = path10.join(here, "server.js");
1046
+ const here = path9.dirname(fileURLToPath4(import.meta.url));
1047
+ const packagedServer = path9.join(here, "server.js");
1225
1048
  if (existsSync5(packagedServer)) return [packagedServer];
1226
1049
  const serverBin = require2.resolve("@chamba/server/bin");
1227
1050
  const tsxLoader = pathToFileURL(require2.resolve("tsx")).href;
1228
1051
  return ["--import", tsxLoader, serverBin];
1229
1052
  }
1230
1053
  function ensureDevBuilds() {
1231
- const here = path10.dirname(fileURLToPath4(import.meta.url));
1232
- if (existsSync5(path10.join(here, "server.js"))) return;
1233
- const repoRoot = path10.resolve(path10.dirname(require2.resolve("@chamba/server/bin")), "../../..");
1054
+ const here = path9.dirname(fileURLToPath4(import.meta.url));
1055
+ if (existsSync5(path9.join(here, "server.js"))) return;
1056
+ const repoRoot = path9.resolve(path9.dirname(require2.resolve("@chamba/server/bin")), "../../..");
1234
1057
  const deps = [
1235
1058
  { label: "web UI", filter: "@chamba/web", dist: resolveWebDist(), marker: "index.html" },
1236
1059
  {
@@ -1241,7 +1064,7 @@ function ensureDevBuilds() {
1241
1064
  }
1242
1065
  ];
1243
1066
  for (const dep of deps) {
1244
- if (existsSync5(path10.join(dep.dist, dep.marker))) continue;
1067
+ if (existsSync5(path9.join(dep.dist, dep.marker))) continue;
1245
1068
  note(`building chamba ${dep.label} (first run in this environment)...`);
1246
1069
  const result = spawnSync("pnpm", ["--filter", dep.filter, "build"], {
1247
1070
  cwd: repoRoot,
@@ -1288,41 +1111,76 @@ async function open2(ctx) {
1288
1111
  note(`home: ${ctx.home} port: ${ctx.port}`);
1289
1112
  const { spawned } = await ensureServer(ctx.client, { home: ctx.home, port: ctx.port });
1290
1113
  if (spawned) note(`started chamba server on :${ctx.port}`);
1291
- const req = {
1292
- ...ctx.flags.kind ? { kind: parseKind(ctx.flags.kind) } : {},
1293
- ...ctx.flags.title ? { title: ctx.flags.title } : {}
1294
- };
1295
- const summary = await ctx.client.create(req);
1296
- const id = summary.meta.id;
1297
- const digest = await ctx.client.attach(id, 0);
1114
+ const key = agentKey();
1298
1115
  const state = await readState(ctx.home);
1299
- await writeState(withSession(state, id, digest.cursor), ctx.home);
1300
- out(`http://localhost:${ctx.port}/?s=${id}`);
1116
+ const existing = await resolveExisting(ctx, key, state);
1117
+ const meta = existing ?? await create(ctx, key);
1118
+ const cursor = state.cursors[meta.id] ?? (existing ? 0 : 1);
1119
+ await writeState(withSession(state, meta.id, cursor, key), ctx.home);
1120
+ out(sessionPageUrl(`http://localhost:${ctx.port}`, meta.id));
1301
1121
  note(" in a container? if this link is unreachable from your host:");
1302
1122
  note(
1303
1123
  ` - the server bound ${resolveBindHost([])}; set CHAMBA_HOST=0.0.0.0 so the host can reach it`
1304
1124
  );
1305
1125
  note(` - forward port ${ctx.port} to the host (1:1); pin it with CHAMBA_PORT to keep it stable`);
1306
- note(`session ${id} - open the URL, then run \`chamba poll\` to wait for messages`);
1307
- if (!ctx.flags.kind) {
1126
+ if (existing) {
1127
+ note(`reconnected to session ${meta.id} - open the URL, then run \`chamba poll\``);
1128
+ } else {
1308
1129
  note(
1309
- " no --kind given: the human picks spec or everyday in the browser; your next poll reports it as `\xB7 Session set to ...`"
1130
+ `session ${meta.id} created - open the URL, then run \`chamba poll\` to wait for messages`
1310
1131
  );
1311
1132
  }
1312
1133
  return 0;
1313
1134
  }
1314
- function parseKind(value) {
1315
- const parsed = sessionKindSchema.safeParse(value);
1316
- if (!parsed.success) throw new UsageError(`invalid --kind: ${value} (expected everyday or spec)`);
1317
- return parsed.data;
1135
+ async function resolveExisting(ctx, key, state) {
1136
+ if (ctx.flags.new !== void 0) return null;
1137
+ const wantLast = ctx.flags.last !== void 0;
1138
+ if (!wantLast && !key) return null;
1139
+ const active = (await ctx.client.list()).filter((s) => s.meta.status === "active").map((s) => s.meta);
1140
+ if (wantLast) {
1141
+ const found = active[0];
1142
+ if (!found)
1143
+ throw new UsageError("no active session to rebind - run `chamba open` to create one");
1144
+ return found;
1145
+ }
1146
+ const bound = key ? state.bindings[key] : void 0;
1147
+ return active.find((m) => m.id === bound) ?? active.find((m) => m.agentKey === key) ?? null;
1148
+ }
1149
+ async function create(ctx, key) {
1150
+ const req = {
1151
+ ...ctx.flags.title ? { title: ctx.flags.title } : {},
1152
+ ...key ? { agentKey: key } : {}
1153
+ };
1154
+ const summary = await ctx.client.create(req);
1155
+ return summary.meta;
1156
+ }
1157
+
1158
+ // packages/cli/src/errors.ts
1159
+ function errorCode(err) {
1160
+ const seen = /* @__PURE__ */ new Set();
1161
+ const queue = [err];
1162
+ while (queue.length > 0) {
1163
+ const current = queue.shift();
1164
+ if (!current || typeof current !== "object" || seen.has(current)) continue;
1165
+ seen.add(current);
1166
+ const code = current.code;
1167
+ if (typeof code === "string") return code;
1168
+ const cause = current.cause;
1169
+ if (cause) queue.push(cause);
1170
+ const errors = current.errors;
1171
+ if (Array.isArray(errors)) queue.push(...errors);
1172
+ }
1173
+ return void 0;
1174
+ }
1175
+ function isConnectionRefused(err) {
1176
+ return errorCode(err) === "ECONNREFUSED";
1318
1177
  }
1319
1178
 
1320
1179
  // packages/cli/src/commands/poll.ts
1321
- var DEFAULT_WAIT_MS = 25e3;
1322
- var MAX_WAIT_MS = 3e4;
1323
1180
  var DEFAULT_FOLLOW_MS = 4 * 6e4;
1324
1181
  var BACKOFF_START_MS = 500;
1325
1182
  var BACKOFF_MAX_MS = 5e3;
1183
+ var RESPAWN_BUDGET = 3;
1326
1184
  var IDLE_GUARD_MS = 1e3;
1327
1185
  var realClock = {
1328
1186
  now: () => Date.now(),
@@ -1330,28 +1188,48 @@ var realClock = {
1330
1188
  };
1331
1189
  async function poll(ctx, clock = realClock) {
1332
1190
  const id = await resolveSessionId(ctx);
1333
- const wait = Math.min(intFlag(ctx.flags.wait, DEFAULT_WAIT_MS), MAX_WAIT_MS);
1191
+ const wait = Math.min(intFlag(ctx.flags.wait, POLL_DEFAULT_WAIT_MS), POLL_MAX_WAIT_MS);
1334
1192
  if (ctx.flags.once !== void 0) return pollOnce(ctx, id, wait);
1335
- const followMs = intFlag(ctx.flags.for, DEFAULT_FOLLOW_MS);
1193
+ const followMs = ctx.flags.for !== void 0 ? intFlag(ctx.flags.for, DEFAULT_FOLLOW_MS) : null;
1194
+ let respawnsLeft = RESPAWN_BUDGET;
1336
1195
  return follow(
1337
1196
  {
1338
1197
  now: clock.now,
1339
1198
  sleep: clock.sleep,
1340
1199
  cursor: async () => cursorFor(await readState(ctx.home), id),
1341
- poll: (since) => ctx.client.poll(id, since, wait),
1200
+ poll: async (since) => {
1201
+ const res = await ctx.client.poll(id, since, wait);
1202
+ respawnsLeft = RESPAWN_BUDGET;
1203
+ return res;
1204
+ },
1342
1205
  deliver: async (res) => {
1343
1206
  printEvents(res.events, ctx.home);
1344
1207
  await writeState(withSession(await readState(ctx.home), id, res.cursor), ctx.home);
1345
1208
  },
1209
+ recover: async (err) => {
1210
+ if (!isConnectionRefused(err)) return "not-mine";
1211
+ if (!await readServerInfo(ctx.home)) return "server-stopped";
1212
+ if (respawnsLeft <= 0) return "unrecoverable";
1213
+ respawnsLeft -= 1;
1214
+ note("chamba server was down - restarting it");
1215
+ try {
1216
+ await ensureServer(ctx.client, { home: ctx.home, port: ctx.port });
1217
+ return "respawned";
1218
+ } catch (e) {
1219
+ note(`could not restart the server: ${e.message}`);
1220
+ return "unrecoverable";
1221
+ }
1222
+ },
1346
1223
  onError: (err, giveUp) => note(pollErrorMessage(err, giveUp)),
1347
1224
  onHeartbeat: () => note("still listening - re-run chamba poll to keep waiting"),
1348
1225
  onEnded: () => note("session has ended - nothing more to wait for"),
1226
+ onServerStopped: () => note("chamba server stopped - run `chamba open` to continue"),
1349
1227
  isFatal
1350
1228
  },
1351
1229
  // The park floor never drops below the guard sleep, so a small (or zero) `--wait`
1352
1230
  // - which makes the server answer empty at once - cannot spin the loop hot.
1353
1231
  {
1354
- deadline: clock.now() + followMs,
1232
+ deadline: followMs === null ? null : clock.now() + followMs,
1355
1233
  parkFloorMs: Math.max(IDLE_GUARD_MS, Math.floor(wait / 2))
1356
1234
  }
1357
1235
  );
@@ -1377,7 +1255,24 @@ async function follow(io, cfg) {
1377
1255
  try {
1378
1256
  res = await io.poll(since);
1379
1257
  } catch (err) {
1380
- const giveUp = io.isFatal(err) || io.now() >= cfg.deadline;
1258
+ if (io.isFatal(err)) {
1259
+ io.onError(err, true);
1260
+ return 1;
1261
+ }
1262
+ const outcome = await io.recover?.(err) ?? "not-mine";
1263
+ if (outcome === "respawned") {
1264
+ backoff = BACKOFF_START_MS;
1265
+ continue;
1266
+ }
1267
+ if (outcome === "server-stopped") {
1268
+ io.onServerStopped?.();
1269
+ return 0;
1270
+ }
1271
+ if (outcome === "unrecoverable") {
1272
+ io.onError(err, true);
1273
+ return 1;
1274
+ }
1275
+ const giveUp = cfg.deadline !== null && io.now() >= cfg.deadline;
1381
1276
  io.onError(err, giveUp);
1382
1277
  if (giveUp) return 1;
1383
1278
  await io.sleep(backoff);
@@ -1393,7 +1288,11 @@ async function follow(io, cfg) {
1393
1288
  io.onEnded?.();
1394
1289
  return 0;
1395
1290
  }
1396
- if (io.now() >= cfg.deadline) {
1291
+ if (res.stopping) {
1292
+ io.onServerStopped?.();
1293
+ return 0;
1294
+ }
1295
+ if (cfg.deadline !== null && io.now() >= cfg.deadline) {
1397
1296
  io.onHeartbeat();
1398
1297
  return 0;
1399
1298
  }
@@ -1419,8 +1318,9 @@ async function reply(ctx) {
1419
1318
  const text = ctx.positionals.join(" ").trim();
1420
1319
  const files = filePaths(ctx.flags);
1421
1320
  if (!text && files.length === 0) {
1422
- throw new UsageError("usage: chamba reply <text> [--file a,b] [--image a,b]");
1321
+ throw new UsageError("usage: chamba reply <text> [--to <seq>] [--file a,b] [--image a,b]");
1423
1322
  }
1323
+ const replyTo = replyToSeq(ctx.flags);
1424
1324
  const id = await resolveSessionId(ctx);
1425
1325
  for (const file of files) {
1426
1326
  try {
@@ -1439,15 +1339,54 @@ async function reply(ctx) {
1439
1339
  }
1440
1340
  await ctx.client.reply(id, {
1441
1341
  ...text ? { text } : {},
1442
- ...attachmentIds.length > 0 ? { attachmentIds } : {}
1342
+ ...attachmentIds.length > 0 ? { attachmentIds } : {},
1343
+ ...replyTo !== void 0 ? { replyTo } : {}
1443
1344
  });
1444
1345
  note(`sent to ${id}`);
1445
1346
  return 0;
1446
1347
  }
1348
+ function replyToSeq(flags) {
1349
+ const raw = flags.to;
1350
+ if (raw === void 0) return void 0;
1351
+ const n = Number(raw);
1352
+ if (raw === "true" || !Number.isInteger(n) || n < 0) {
1353
+ throw new UsageError("`--to` needs an event seq, e.g. --to 42");
1354
+ }
1355
+ return n;
1356
+ }
1447
1357
  function filePaths(flags) {
1448
1358
  return [flags.file, flags.image].filter((value) => typeof value === "string" && value !== "true").flatMap((value) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
1449
1359
  }
1450
1360
 
1361
+ // packages/cli/src/commands/show.ts
1362
+ import { readFile as readFile6 } from "node:fs/promises";
1363
+ import path10 from "node:path";
1364
+ async function show(ctx) {
1365
+ const file = ctx.positionals[0];
1366
+ if (!file) throw new UsageError("usage: chamba show <file.html> [--name <name>]");
1367
+ if (!/\.html?$/i.test(file)) {
1368
+ throw new UsageError(`chamba show takes a self-contained HTML file, got: ${file}`);
1369
+ }
1370
+ let content;
1371
+ try {
1372
+ content = await readFile6(file, "utf8");
1373
+ } catch (err) {
1374
+ throw new UsageError(`could not read ${file}: ${err.message}`);
1375
+ }
1376
+ const flagName = ctx.flags.name?.trim();
1377
+ const name = flagName && flagName !== "true" ? flagName : path10.basename(file).replace(/\.html?$/i, "");
1378
+ const result = postPageReqSchema.safeParse({ name, content });
1379
+ if (!result.success) {
1380
+ throw new UsageError(`invalid page: ${result.error.issues.map((i) => i.message).join("; ")}`);
1381
+ }
1382
+ const id = await resolveSessionId(ctx);
1383
+ const event = await ctx.client.showPage(id, result.data);
1384
+ const ref = pageRefSchema.safeParse(event.payload?.page);
1385
+ const shownAs = ref.success ? ref.data.name : name;
1386
+ note(`showing "${shownAs}" in session ${id}${ref.success ? ` (${ref.data.relPath})` : ""}`);
1387
+ return 0;
1388
+ }
1389
+
1451
1390
  // packages/cli/src/commands/skill.ts
1452
1391
  import { cpSync, existsSync as existsSync6, rmSync } from "node:fs";
1453
1392
  import { homedir } from "node:os";
@@ -1490,7 +1429,7 @@ async function status(ctx) {
1490
1429
  return 0;
1491
1430
  }
1492
1431
  const state = await readState(ctx.home);
1493
- const up = await serverOwnsHome(ctx);
1432
+ const up = await ctx.client.ownsHome(ctx.home);
1494
1433
  const rows = up ? await rowsFromServer(ctx) : await rowsFromDisk(ctx);
1495
1434
  note(`home: ${ctx.home}`);
1496
1435
  if (rows.length === 0) {
@@ -1503,14 +1442,6 @@ async function status(ctx) {
1503
1442
  }
1504
1443
  return 0;
1505
1444
  }
1506
- async function serverOwnsHome(ctx) {
1507
- if (!await ctx.client.isUp()) return false;
1508
- try {
1509
- return (await ctx.client.health()).home === ctx.home;
1510
- } catch {
1511
- return false;
1512
- }
1513
- }
1514
1445
  async function rowsFromServer(ctx) {
1515
1446
  const summaries = await ctx.client.list();
1516
1447
  return summaries.map((s) => ({ meta: s.meta, presence: s.presence }));
@@ -1529,7 +1460,7 @@ async function stop(ctx) {
1529
1460
  return 0;
1530
1461
  }
1531
1462
  const client = new Client(`http://127.0.0.1:${info.port}/`);
1532
- if (!await ownsHome(client, ctx.home)) {
1463
+ if (!await client.ownsHome(ctx.home)) {
1533
1464
  note("no chamba server running here");
1534
1465
  return 0;
1535
1466
  }
@@ -1546,25 +1477,14 @@ async function stop(ctx) {
1546
1477
  note(`sent stop to the chamba server (pid ${info.pid}) but it did not exit in time`);
1547
1478
  return 1;
1548
1479
  }
1549
- async function ownsHome(client, home) {
1550
- if (!await client.isUp()) return false;
1551
- try {
1552
- return (await client.health()).home === home;
1553
- } catch {
1554
- return false;
1555
- }
1556
- }
1557
1480
  async function waitUntilDown(client, timeoutMs = 5e3) {
1558
1481
  const deadline = Date.now() + timeoutMs;
1559
1482
  while (Date.now() < deadline) {
1560
1483
  if (!await client.isUp()) return true;
1561
- await delay2(150);
1484
+ await delay(150);
1562
1485
  }
1563
1486
  return false;
1564
1487
  }
1565
- function delay2(ms) {
1566
- return new Promise((resolve) => setTimeout(resolve, ms));
1567
- }
1568
1488
 
1569
1489
  // packages/cli/src/commands/index.ts
1570
1490
  var COMMANDS = {
@@ -1572,15 +1492,11 @@ var COMMANDS = {
1572
1492
  open: open2,
1573
1493
  poll,
1574
1494
  reply,
1495
+ get,
1496
+ show,
1575
1497
  end,
1576
- attach,
1577
1498
  status,
1578
1499
  stop,
1579
- // Spec sessions: structured interview + live artifacts + export.
1580
- ask,
1581
- decision,
1582
- artifact,
1583
- export: exportCmd,
1584
1500
  // Distribution: install the bundled skill into a Claude skills directory.
1585
1501
  skill,
1586
1502
  help,
@@ -1596,16 +1512,7 @@ function runCommand(name, ctx) {
1596
1512
  }
1597
1513
 
1598
1514
  // packages/cli/src/index.ts
1599
- var REQUIRES_WORKSPACE = /* @__PURE__ */ new Set([
1600
- "poll",
1601
- "reply",
1602
- "end",
1603
- "attach",
1604
- "ask",
1605
- "decision",
1606
- "artifact",
1607
- "export"
1608
- ]);
1515
+ var REQUIRES_WORKSPACE = /* @__PURE__ */ new Set(["poll", "reply", "get", "show", "end"]);
1609
1516
  async function main(argv = process.argv.slice(2)) {
1610
1517
  const { positionals, flags } = parseArgs(argv);
1611
1518
  if (flags.version && positionals.length === 0) {
@@ -1645,29 +1552,13 @@ function reportError(err) {
1645
1552
  note(`error: ${err.message}`);
1646
1553
  return 1;
1647
1554
  }
1648
- if (errorCode(err) === "ECONNREFUSED") {
1555
+ if (isConnectionRefused(err)) {
1649
1556
  note("cannot reach the chamba server - run `chamba open` first");
1650
1557
  return 1;
1651
1558
  }
1652
1559
  note(`error: ${err.message}`);
1653
1560
  return 1;
1654
1561
  }
1655
- function errorCode(err) {
1656
- const seen = /* @__PURE__ */ new Set();
1657
- const queue = [err];
1658
- while (queue.length > 0) {
1659
- const current = queue.shift();
1660
- if (!current || typeof current !== "object" || seen.has(current)) continue;
1661
- seen.add(current);
1662
- const code = current.code;
1663
- if (typeof code === "string") return code;
1664
- const cause = current.cause;
1665
- if (cause) queue.push(cause);
1666
- const errors = current.errors;
1667
- if (Array.isArray(errors)) queue.push(...errors);
1668
- }
1669
- return void 0;
1670
- }
1671
1562
  function invokedAsEntry() {
1672
1563
  const entry = process.argv[1];
1673
1564
  if (entry === void 0) return false;