chamba 0.0.1 → 0.2.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 (74) hide show
  1. package/README.md +66 -1
  2. package/dist/cli.js +1691 -0
  3. package/dist/server.js +1919 -0
  4. package/inject/annotate.js +49 -0
  5. package/package.json +24 -3
  6. package/skill/README.md +12 -0
  7. package/skill/SKILL.md +285 -0
  8. package/web/assets/abnfDiagram-VRR7QNED-C5V2hrtE.js +1 -0
  9. package/web/assets/arc-d7OTmxZy.js +1 -0
  10. package/web/assets/architectureDiagram-ZJ3FMSHR--CztaeAS.js +36 -0
  11. package/web/assets/blockDiagram-677ZJIJ3-DkA5Z_uK.js +132 -0
  12. package/web/assets/c4Diagram-LMCZKHZV-C78dI0l-.js +10 -0
  13. package/web/assets/channel-D9AxU8kk.js +1 -0
  14. package/web/assets/chunk-2Q5K7J3B-LBNP12XV.js +1 -0
  15. package/web/assets/chunk-32BRIVSS-DZoXrhCs.js +1 -0
  16. package/web/assets/chunk-5VM5RSS4-CPjoRxF_.js +15 -0
  17. package/web/assets/chunk-EX3LRPZG-DSlL8FE9.js +231 -0
  18. package/web/assets/chunk-JWPE2WC7-BjbBLYge.js +1 -0
  19. package/web/assets/chunk-MOJQB5TN-DCKqdaDr.js +88 -0
  20. package/web/assets/chunk-RYQCIY6F-D5yAURhE.js +1 -0
  21. package/web/assets/chunk-V7JOEXUC-DKY2sn06.js +206 -0
  22. package/web/assets/chunk-VR4S4FIN-DGiVMHh6.js +1 -0
  23. package/web/assets/chunk-XXDRQBXY-CUI1Tkrr.js +1 -0
  24. package/web/assets/classDiagram-OUVF2IWQ-D7gWuzIF.js +1 -0
  25. package/web/assets/classDiagram-v2-EOCWNBFH-D7gWuzIF.js +1 -0
  26. package/web/assets/cose-bilkent-JH36ORCC-Ao3LzalV.js +1 -0
  27. package/web/assets/cynefin-VYW2F7L2-7JQwpRwH.js +178 -0
  28. package/web/assets/cynefinDiagram-TSTJHNR4-BfuANy_V.js +62 -0
  29. package/web/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  30. package/web/assets/dagre-VKFMJZFB-DHVnVJCL.js +4 -0
  31. package/web/assets/defaultLocale-DX6XiGOO.js +1 -0
  32. package/web/assets/diagram-FQU43EPY-OYrtwA2f.js +3 -0
  33. package/web/assets/diagram-G47NLZAW-CbkLZxYp.js +24 -0
  34. package/web/assets/diagram-NH7WQ7WH-B0z_jTdi.js +24 -0
  35. package/web/assets/diagram-OA4YK3LP-BRu_2nwx.js +30 -0
  36. package/web/assets/diagram-WEI45ONY-B9cpw86D.js +41 -0
  37. package/web/assets/ebnfDiagram-CCIWWBDH-DaBQ7T4R.js +1 -0
  38. package/web/assets/erDiagram-Q63AITRT-D6PQiFj3.js +85 -0
  39. package/web/assets/flowDiagram-23GEKE2U-p_8Wj7It.js +156 -0
  40. package/web/assets/ganttDiagram-NO4QXBWP-BZBwuqu3.js +292 -0
  41. package/web/assets/gitGraphDiagram-IHSO6WYX-DyPcQ-91.js +106 -0
  42. package/web/assets/graph-C9eacEi8.js +1 -0
  43. package/web/assets/highlighted-body-OFNGDK62-Cq7RJZcn.js +1 -0
  44. package/web/assets/index-BN-QUr5b.js +350 -0
  45. package/web/assets/index-D6-LVQ5m.css +1 -0
  46. package/web/assets/infoDiagram-FWYZ7A6U-sAgBAeWC.js +2 -0
  47. package/web/assets/init-Gi6I4Gst.js +1 -0
  48. package/web/assets/ishikawaDiagram-FXEZZL3T-COEn9wCs.js +70 -0
  49. package/web/assets/journeyDiagram-5HDEW3XC-BPddPRXW.js +139 -0
  50. package/web/assets/kanban-definition-HUTT4EX6-B8TW97Oy.js +89 -0
  51. package/web/assets/katex-C5jXJg4s.js +257 -0
  52. package/web/assets/layout-DEXfKzaS.js +1 -0
  53. package/web/assets/linear-CnW5uQ_k.js +1 -0
  54. package/web/assets/map-Czzmt4hB.js +1 -0
  55. package/web/assets/mermaid.core-bOV5iNX_.js +314 -0
  56. package/web/assets/mindmap-definition-LN4V7U3C-Dtr4xW_-.js +96 -0
  57. package/web/assets/ordinal-Cboi1Yqb.js +1 -0
  58. package/web/assets/pegDiagram-2B236MQR-DqBITK5N.js +1 -0
  59. package/web/assets/pieDiagram-ENE6RG2P-DXjxZgy3.js +39 -0
  60. package/web/assets/quadrantDiagram-ABIIQ3AL-D4MXx3U7.js +7 -0
  61. package/web/assets/railroadDiagram-RFXS5EU6-LPfpHg4_.js +1 -0
  62. package/web/assets/requirementDiagram-TGXJPOKE-CiDswbjt.js +84 -0
  63. package/web/assets/sankeyDiagram-HTMAVEWB-VfN_qTEI.js +40 -0
  64. package/web/assets/sequenceDiagram-DBY2YBRQ-kJRIx4zm.js +162 -0
  65. package/web/assets/sizeCapture-X5ZJPWSS-D8vJbAt-.js +1 -0
  66. package/web/assets/stateDiagram-2N3HPSRC-9qku-qAj.js +1 -0
  67. package/web/assets/stateDiagram-v2-6OUMAXLB-DbOQICyv.js +1 -0
  68. package/web/assets/swimlanes-5IMT3BWC-Cdb5FDUM.js +2 -0
  69. package/web/assets/swimlanesDiagram-G3AALYLV-EDpLmqQv.js +8 -0
  70. package/web/assets/timeline-definition-FHXFAJF6-D0hyJCMm.js +120 -0
  71. package/web/assets/vennDiagram-L72KCM5P-KsCkGMtr.js +34 -0
  72. package/web/assets/wardleyDiagram-EHGQE667-B4jT_1ak.js +78 -0
  73. package/web/assets/xychartDiagram-FW5EYKEG-Bmy0rt8j.js +7 -0
  74. package/web/index.html +28 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1691 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/cli/src/index.ts
4
+ import { realpathSync as realpathSync2 } from "node:fs";
5
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
6
+
7
+ // packages/server/src/index.ts
8
+ import { serve as honoServe } from "@hono/node-server";
9
+
10
+ // packages/shared/src/annotations.ts
11
+ import { z as z3 } from "zod";
12
+
13
+ // packages/shared/src/messages.ts
14
+ import { z as z2 } from "zod";
15
+
16
+ // packages/shared/src/session.ts
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()
27
+ });
28
+
29
+ // packages/shared/src/messages.ts
30
+ var attachmentSchema = z2.object({
31
+ id: z2.string().min(1),
32
+ name: z2.string().min(1),
33
+ relPath: z2.string().min(1),
34
+ contentType: z2.string().min(1),
35
+ size: z2.number().int().nonnegative()
36
+ });
37
+ var messagePayloadSchema = z2.object({
38
+ text: z2.string().min(1).optional(),
39
+ attachments: z2.array(attachmentSchema).optional()
40
+ }).refine((p) => (p.text?.length ?? 0) > 0 || (p.attachments?.length ?? 0) > 0, {
41
+ message: "a message needs text or at least one attachment"
42
+ });
43
+ var systemReasonSchema = z2.enum([
44
+ "session-created",
45
+ "user-moved-away",
46
+ "session-ended",
47
+ "kind-chosen"
48
+ ]);
49
+ var systemPayloadSchema = z2.object({
50
+ reason: systemReasonSchema,
51
+ to: z2.string().min(1).optional(),
52
+ kind: sessionKindSchema.optional()
53
+ });
54
+
55
+ // packages/shared/src/annotations.ts
56
+ var ANNOTATION_BATCH = "annotation-batch";
57
+ var annotationSourceSchema = z3.object({
58
+ file: z3.string().min(1),
59
+ line: z3.number().int().positive().optional(),
60
+ column: z3.number().int().positive().optional()
61
+ });
62
+ var annotationComponentSchema = z3.object({
63
+ name: z3.string().min(1),
64
+ source: annotationSourceSchema.optional()
65
+ });
66
+ var annotationTargetSchema = z3.object({
67
+ selector: z3.string().min(1),
68
+ pageUrl: z3.string().min(1),
69
+ viewport: z3.object({
70
+ width: z3.number().int().positive(),
71
+ height: z3.number().int().positive()
72
+ }),
73
+ label: z3.string().optional(),
74
+ component: annotationComponentSchema.optional()
75
+ });
76
+ var annotationItemSchema = z3.object({
77
+ note: z3.string().min(1).optional(),
78
+ target: annotationTargetSchema,
79
+ screenshot: attachmentSchema.optional()
80
+ });
81
+ var annotationBatchPayloadSchema = z3.object({
82
+ items: z3.array(annotationItemSchema).min(1)
83
+ });
84
+
85
+ // packages/shared/src/api.ts
86
+ import { z as z7 } from "zod";
87
+
88
+ // packages/shared/src/events.ts
89
+ 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
+ });
100
+
101
+ // packages/shared/src/presence.ts
102
+ 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
138
+ });
139
+
140
+ // 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
148
+ });
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"
154
+ });
155
+ var annotationItemReqSchema = z7.object({
156
+ note: z7.string().min(1).optional(),
157
+ target: annotationTargetSchema,
158
+ screenshotAttachmentId: z7.string().min(1).optional()
159
+ });
160
+ var postAnnotationsReqSchema = z7.object({
161
+ items: z7.array(annotationItemReqSchema).min(1)
162
+ });
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({
174
+ attachment: attachmentSchema
175
+ });
176
+ var eventResSchema = z7.object({
177
+ event: eventEnvelopeSchema
178
+ });
179
+ var pollResSchema = z7.object({
180
+ events: z7.array(eventEnvelopeSchema),
181
+ cursor: cursorSchema,
182
+ ended: z7.boolean().optional()
183
+ });
184
+ var fetchMoreSchema = z7.object({
185
+ hasMore: z7.boolean(),
186
+ earliestLoadedSeq: cursorSchema,
187
+ hint: z7.string()
188
+ });
189
+ var attachDigestSchema = z7.object({
190
+ meta: sessionMetaSchema,
191
+ presence: presenceSchema,
192
+ tail: z7.array(eventEnvelopeSchema),
193
+ cursor: cursorSchema,
194
+ fetchMore: fetchMoreSchema
195
+ });
196
+ var sessionSummarySchema = z7.object({
197
+ meta: sessionMetaSchema,
198
+ presence: presenceSchema
199
+ });
200
+ var listResSchema = z7.object({
201
+ sessions: z7.array(sessionSummarySchema)
202
+ });
203
+ var sessionResSchema = z7.object({
204
+ session: sessionSummarySchema
205
+ });
206
+ var healthResSchema = z7.object({
207
+ ok: z7.boolean(),
208
+ sessions: cursorSchema,
209
+ home: z7.string().min(1).optional(),
210
+ port: z7.number().int().positive().optional(),
211
+ version: z7.string().min(1).optional()
212
+ });
213
+
214
+ // packages/shared/src/issues.ts
215
+ import { z as z8 } from "zod";
216
+ var issueTypeSchema = z8.enum(["bug", "feature"]);
217
+ var postIssueReqSchema = z8.object({
218
+ type: issueTypeSchema,
219
+ text: z8.string().min(1),
220
+ screenshotAttachmentIds: z8.array(z8.string().min(1)).optional()
221
+ });
222
+ var issueResSchema = z8.object({
223
+ id: z8.string().min(1),
224
+ path: z8.string().min(1)
225
+ });
226
+
227
+ // packages/shared/src/ws.ts
228
+ import { z as z9 } from "zod";
229
+ var wsServerMessageSchema = z9.discriminatedUnion("t", [
230
+ z9.object({
231
+ t: z9.literal("hello"),
232
+ meta: sessionMetaSchema,
233
+ presence: presenceSchema
234
+ }),
235
+ z9.object({
236
+ t: z9.literal("append"),
237
+ event: eventEnvelopeSchema
238
+ }),
239
+ z9.object({
240
+ t: z9.literal("presence"),
241
+ state: presenceSchema
242
+ }),
243
+ z9.object({
244
+ t: z9.literal("kind"),
245
+ meta: sessionMetaSchema
246
+ }),
247
+ z9.object({
248
+ t: z9.literal("ended"),
249
+ meta: sessionMetaSchema
250
+ })
251
+ ]);
252
+
253
+ // packages/server/src/app.ts
254
+ import { createNodeWebSocket } from "@hono/node-ws";
255
+ import { Hono } from "hono";
256
+
257
+ // packages/server/src/inject.ts
258
+ import { existsSync } from "node:fs";
259
+ import path from "node:path";
260
+ 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
+
268
+ // packages/server/src/render.ts
269
+ import { marked } from "marked";
270
+
271
+ // 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
+ function resolveWebDist() {
276
+ 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;
280
+ }
281
+
282
+ // packages/server/src/app.ts
283
+ var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
284
+
285
+ // packages/server/src/lifecycle.ts
286
+ import { open, readFile as readFile2, rm } from "node:fs/promises";
287
+
288
+ // packages/server/src/sessions/paths.ts
289
+ import { existsSync as existsSync3, realpathSync } from "node:fs";
290
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
291
+ import path3 from "node:path";
292
+ function defaultContext() {
293
+ return { cwd: process.cwd(), env: process.env };
294
+ }
295
+ function resolveHome(ctx = defaultContext()) {
296
+ return findExistingHome(ctx) ?? plannedHome(ctx);
297
+ }
298
+ function findExistingHome(ctx = defaultContext()) {
299
+ const dir = ctx.env.CHAMBA_DIR;
300
+ if (dir && dir.length > 0) return canonicalize(dir);
301
+ const inside = enclosingChamba(ctx.cwd);
302
+ if (inside) return canonicalize(inside);
303
+ const root = gitRoot(ctx.cwd);
304
+ let cursor = path3.resolve(ctx.cwd);
305
+ while (true) {
306
+ if (existsSync3(path3.join(cursor, ".chamba"))) return canonicalize(path3.join(cursor, ".chamba"));
307
+ if (!root) break;
308
+ if (cursor === root) break;
309
+ const parent = path3.dirname(cursor);
310
+ if (parent === cursor) break;
311
+ cursor = parent;
312
+ }
313
+ return null;
314
+ }
315
+ function plannedHome(ctx = defaultContext()) {
316
+ const root = gitRoot(ctx.cwd) ?? path3.resolve(ctx.cwd);
317
+ return canonicalize(path3.join(root, ".chamba"));
318
+ }
319
+ function gitRoot(cwd) {
320
+ let cursor = path3.resolve(cwd);
321
+ while (true) {
322
+ if (existsSync3(path3.join(cursor, ".git"))) return cursor;
323
+ const parent = path3.dirname(cursor);
324
+ if (parent === cursor) return null;
325
+ cursor = parent;
326
+ }
327
+ }
328
+ function enclosingChamba(cwd) {
329
+ const parts = path3.resolve(cwd).split(path3.sep);
330
+ const index = parts.lastIndexOf(".chamba");
331
+ if (index < 0) return null;
332
+ return parts.slice(0, index + 1).join(path3.sep) || path3.sep;
333
+ }
334
+ function canonicalize(p) {
335
+ const resolved = path3.resolve(p);
336
+ let existing = resolved;
337
+ const tail = [];
338
+ while (!existsSync3(existing)) {
339
+ tail.unshift(path3.basename(existing));
340
+ const parent = path3.dirname(existing);
341
+ if (parent === existing) return resolved;
342
+ existing = parent;
343
+ }
344
+ return path3.join(realpathSync(existing), ...tail);
345
+ }
346
+ function sessionsDir(home) {
347
+ return path3.join(home, "sessions");
348
+ }
349
+ function sessionDir(home, id) {
350
+ return path3.join(sessionsDir(home), id);
351
+ }
352
+ function metaPath(home, id) {
353
+ return path3.join(sessionDir(home, id), "meta.json");
354
+ }
355
+ function pidfilePath(home) {
356
+ return path3.join(home, "server.json");
357
+ }
358
+ function agentStatePath(home) {
359
+ return path3.join(home, "agent-state.json");
360
+ }
361
+ async function ensureChambaHome(home) {
362
+ await mkdir(sessionsDir(home), { recursive: true });
363
+ await ensureGitignored(home);
364
+ }
365
+ async function ensureGitignored(home) {
366
+ if (path3.basename(home) !== ".chamba") return;
367
+ const gitignore = path3.join(path3.dirname(home), ".gitignore");
368
+ const entry = ".chamba/";
369
+ try {
370
+ let content = "";
371
+ try {
372
+ content = await readFile(gitignore, "utf8");
373
+ } catch (err) {
374
+ if (err.code !== "ENOENT") return;
375
+ }
376
+ const already = content.split(/\r?\n/).some((line) => {
377
+ const trimmed = line.trim();
378
+ return trimmed === entry || trimmed === ".chamba";
379
+ });
380
+ if (already) return;
381
+ const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
382
+ await appendFile(gitignore, `${prefix}${entry}
383
+ `, "utf8");
384
+ } catch {
385
+ }
386
+ }
387
+
388
+ // packages/server/src/lifecycle.ts
389
+ function readServerInfo(home) {
390
+ return readPidfile(pidfilePath(home));
391
+ }
392
+ async function readPidfile(file) {
393
+ try {
394
+ const raw = await readFile2(file, "utf8");
395
+ const parsed = JSON.parse(raw);
396
+ if (typeof parsed.pid === "number" && typeof parsed.port === "number") return parsed;
397
+ return null;
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+
403
+ // packages/server/src/port.ts
404
+ var DEFAULT_PORT = 4319;
405
+ var DEFAULT_BIND_HOST = "127.0.0.1";
406
+ function resolveBindHost(argv = process.argv.slice(2)) {
407
+ const flag = readFlag(argv, "--host");
408
+ if (flag !== void 0 && flag.length > 0) return flag;
409
+ const env = process.env.CHAMBA_HOST;
410
+ if (env !== void 0 && env.length > 0) return env;
411
+ return DEFAULT_BIND_HOST;
412
+ }
413
+ function resolvePort(argv = process.argv.slice(2), home) {
414
+ const flag = readFlag(argv, "--port");
415
+ if (flag !== void 0) return parsePort(flag, "--port");
416
+ const env = process.env.CHAMBA_PORT;
417
+ if (env !== void 0 && env.length > 0) return parsePort(env, "CHAMBA_PORT");
418
+ return home !== void 0 ? derivePort(home) : DEFAULT_PORT;
419
+ }
420
+ function derivePort(home) {
421
+ let hash = 2166136261;
422
+ for (let i = 0; i < home.length; i++) {
423
+ hash ^= home.charCodeAt(i);
424
+ hash = Math.imul(hash, 16777619);
425
+ }
426
+ return DEFAULT_PORT + Math.abs(hash) % 512;
427
+ }
428
+ function readFlag(argv, name) {
429
+ for (let i = 0; i < argv.length; i++) {
430
+ const arg = argv[i];
431
+ if (arg === name) return argv[i + 1];
432
+ if (arg?.startsWith(`${name}=`)) return arg.slice(name.length + 1);
433
+ }
434
+ return void 0;
435
+ }
436
+ function parsePort(value, source) {
437
+ const port = Number(value);
438
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
439
+ throw new Error(`invalid ${source}: ${value}`);
440
+ }
441
+ return port;
442
+ }
443
+
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
+ // packages/server/src/index.ts
487
+ var DEFAULT_IDLE_MS = 5 * 6e4;
488
+
489
+ // packages/cli/src/args.ts
490
+ function parseArgs(argv) {
491
+ const positionals = [];
492
+ const flags = {};
493
+ for (let i = 0; i < argv.length; i++) {
494
+ const arg = argv[i];
495
+ if (arg === void 0) continue;
496
+ if (!arg.startsWith("--")) {
497
+ positionals.push(arg);
498
+ continue;
499
+ }
500
+ const eq = arg.indexOf("=");
501
+ if (eq !== -1) {
502
+ flags[arg.slice(2, eq)] = arg.slice(eq + 1);
503
+ continue;
504
+ }
505
+ const next = argv[i + 1];
506
+ if (next !== void 0 && !next.startsWith("-")) {
507
+ flags[arg.slice(2)] = next;
508
+ i++;
509
+ } else {
510
+ flags[arg.slice(2)] = "true";
511
+ }
512
+ }
513
+ return { positionals, flags };
514
+ }
515
+
516
+ // packages/cli/src/client.ts
517
+ import { readFile as readFile4 } from "node:fs/promises";
518
+ import path5 from "node:path";
519
+ var ChannelError = class extends Error {
520
+ constructor(status2, message) {
521
+ super(message);
522
+ this.status = status2;
523
+ this.name = "ChannelError";
524
+ }
525
+ };
526
+ var Client = class {
527
+ constructor(baseUrl) {
528
+ this.baseUrl = baseUrl;
529
+ }
530
+ /** Liveness probe with a short deadline; never throws - `false` means "spawn it". */
531
+ async isUp(timeoutMs = 800) {
532
+ try {
533
+ const res = await this.fetch("/api/health", { method: "GET", timeoutMs });
534
+ return res.ok;
535
+ } catch {
536
+ return false;
537
+ }
538
+ }
539
+ async health() {
540
+ return this.json("/api/health", { method: "GET", timeoutMs: 2e3 }, healthResSchema);
541
+ }
542
+ async create(req) {
543
+ const res = await this.json(
544
+ "/api/sessions",
545
+ { method: "POST", body: req, timeoutMs: 5e3 },
546
+ sessionResSchema
547
+ );
548
+ return res.session;
549
+ }
550
+ async list() {
551
+ const res = await this.json("/api/sessions", { method: "GET", timeoutMs: 5e3 }, listResSchema);
552
+ return res.sessions;
553
+ }
554
+ async get(id) {
555
+ const res = await this.json(
556
+ `/api/sessions/${encodeURIComponent(id)}`,
557
+ { method: "GET", timeoutMs: 5e3 },
558
+ sessionResSchema
559
+ );
560
+ return res.session;
561
+ }
562
+ async postMessage(id, body) {
563
+ const res = await this.json(
564
+ `/api/sessions/${encodeURIComponent(id)}/messages`,
565
+ { method: "POST", body, timeoutMs: 5e3 },
566
+ eventResSchema
567
+ );
568
+ return res.event;
569
+ }
570
+ async reply(id, body) {
571
+ const res = await this.json(
572
+ `/api/sessions/${encodeURIComponent(id)}/reply`,
573
+ { method: "POST", body, timeoutMs: 5e3 },
574
+ eventResSchema
575
+ );
576
+ return res.event;
577
+ }
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) {
589
+ 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`,
600
+ { method: "POST", body, timeoutMs: 1e4 },
601
+ eventResSchema
602
+ );
603
+ return res.event;
604
+ }
605
+ /**
606
+ * Upload a local file into the session, returning its stored reference. The
607
+ * agent uses this to send an image back (`reply --image`); the returned id is
608
+ * then referenced from a `reply`. Multipart, so the server persists the raw
609
+ * bytes into the session directory rather than smuggling them through JSON.
610
+ */
611
+ async uploadAttachment(id, filePath) {
612
+ const bytes = await readFile4(filePath);
613
+ const form = new FormData();
614
+ form.append("file", new Blob([bytes]), path5.basename(filePath));
615
+ const res = await this.json(
616
+ `/api/sessions/${encodeURIComponent(id)}/attachments`,
617
+ { method: "POST", body: form, timeoutMs: 3e4 },
618
+ attachmentResSchema
619
+ );
620
+ return res.attachment;
621
+ }
622
+ /**
623
+ * The blocking long-poll. The server parks for up to `waitMs`; the client
624
+ * deadline is a little longer so a healthy server always answers before the
625
+ * client gives up (a client timeout would look like a lost message).
626
+ */
627
+ async poll(id, since, waitMs) {
628
+ return this.json(
629
+ `/api/sessions/${encodeURIComponent(id)}/poll?since=${since}&wait=${waitMs}`,
630
+ { method: "GET", timeoutMs: waitMs + 5e3 },
631
+ pollResSchema
632
+ );
633
+ }
634
+ async events(id, since, limit) {
635
+ const query = limit === void 0 ? `since=${since}` : `since=${since}&limit=${limit}`;
636
+ return this.json(
637
+ `/api/sessions/${encodeURIComponent(id)}/events?${query}`,
638
+ { method: "GET", timeoutMs: 5e3 },
639
+ pollResSchema
640
+ );
641
+ }
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
+ async end(id) {
650
+ const res = await this.json(
651
+ `/api/sessions/${encodeURIComponent(id)}/end`,
652
+ { method: "POST", timeoutMs: 5e3 },
653
+ sessionResSchema
654
+ );
655
+ return res.session;
656
+ }
657
+ async json(path12, options, schema) {
658
+ const res = await this.fetch(path12, options);
659
+ if (!res.ok) throw await toError(res);
660
+ return schema.parse(await res.json());
661
+ }
662
+ async fetch(path12, options) {
663
+ const { method = "GET", body, timeoutMs } = options;
664
+ const controller = timeoutMs !== void 0 ? new AbortController() : void 0;
665
+ const timer = controller && timeoutMs !== void 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
666
+ try {
667
+ return await fetch(new URL(path12, this.baseUrl), {
668
+ method,
669
+ signal: controller?.signal,
670
+ ...bodyInit(body)
671
+ });
672
+ } finally {
673
+ if (timer) clearTimeout(timer);
674
+ }
675
+ }
676
+ };
677
+ function bodyInit(body) {
678
+ if (body === void 0) return {};
679
+ if (body instanceof FormData) return { body };
680
+ return { headers: { "content-type": "application/json" }, body: JSON.stringify(body) };
681
+ }
682
+ 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
+ }
689
+ return new ChannelError(res.status, message);
690
+ }
691
+
692
+ // packages/cli/src/output.ts
693
+ import path6 from "node:path";
694
+ import { z as z10 } from "zod";
695
+ function out(message) {
696
+ process.stdout.write(`${message}
697
+ `);
698
+ }
699
+ function note(message) {
700
+ process.stderr.write(`${message}
701
+ `);
702
+ }
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
+ function formatSystemEvent(event) {
718
+ const parsed = systemPayloadSchema.safeParse(event.payload);
719
+ 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";
724
+ case "session-ended":
725
+ return "\xB7 Session ended";
726
+ case "session-created":
727
+ 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
+ }
733
+ }
734
+ function formatEvent(event) {
735
+ if (event.source === "system") return formatSystemEvent(event);
736
+ const who = event.source === "user" ? "you" : "agent";
737
+ const text = eventText(event);
738
+ return text ? `${who}: ${text}` : `${who}:`;
739
+ }
740
+ function annotationItems(event) {
741
+ const parsed = annotationBatchPayloadSchema.safeParse(event.payload);
742
+ return parsed.success ? parsed.data.items : [];
743
+ }
744
+ function formatAnnotationBatch(event, home) {
745
+ const items = annotationItems(event);
746
+ const who = event.source === "user" ? "you" : "agent";
747
+ const lines = [`${who} [${items.length} annotation${items.length === 1 ? "" : "s"}]:`];
748
+ items.forEach((item, index) => {
749
+ const { target } = item;
750
+ lines.push(` ${index + 1}. ${item.note ?? "(no comment)"}`);
751
+ lines.push(` element: ${target.label ?? target.selector}`);
752
+ lines.push(` selector: ${target.selector}`);
753
+ lines.push(
754
+ ` page: ${target.pageUrl} viewport: ${target.viewport.width}x${target.viewport.height}`
755
+ );
756
+ if (target.component) {
757
+ const source = target.component.source;
758
+ const at = source ? ` (${source.file}${source.line ? `:${source.line}` : ""})` : "";
759
+ lines.push(` component: ${target.component.name}${at}`);
760
+ }
761
+ if (item.screenshot) {
762
+ const abs = path6.join(sessionDir(home, event.sessionId), item.screenshot.relPath);
763
+ lines.push(` [shot] ${item.screenshot.name} -> ${abs}`);
764
+ }
765
+ });
766
+ return lines;
767
+ }
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
+ );
792
+ if (!parsed.success) return formatEvent(event);
793
+ const ref = parsed.data;
794
+ const abs = path6.join(sessionDir(home, event.sessionId), ref.relPath);
795
+ return `agent [artifact ${ref.kind} "${ref.name}"] -> ${abs}`;
796
+ }
797
+ function printEvents(events, home) {
798
+ for (const event of events) {
799
+ if (event.type === ANNOTATION_BATCH) {
800
+ for (const line of formatAnnotationBatch(event, home)) out(line);
801
+ continue;
802
+ }
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));
813
+ continue;
814
+ }
815
+ out(formatEvent(event));
816
+ for (const att of eventAttachments(event)) {
817
+ out(` [file] ${att.name} -> ${path6.join(sessionDir(home, event.sessionId), att.relPath)}`);
818
+ }
819
+ }
820
+ }
821
+ function formatSession(meta, presence, current) {
822
+ const marker = current ? "*" : " ";
823
+ const title = meta.title ? ` "${meta.title}"` : "";
824
+ const live = presence ? ` ${presence}` : "";
825
+ return `${marker} ${meta.id} ${meta.status}${live} ${meta.kind ?? "unset"}${title}`;
826
+ }
827
+
828
+ // packages/cli/src/commands/artifact.ts
829
+ import { readFile as readFile6 } from "node:fs/promises";
830
+
831
+ // 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";
834
+ 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({})
839
+ });
840
+ async function readState(home = resolveHome()) {
841
+ try {
842
+ const raw = await readFile5(agentStatePath(home), "utf8");
843
+ return agentStateSchema.parse(JSON.parse(raw));
844
+ } catch {
845
+ return { cursors: {} };
846
+ }
847
+ }
848
+ async function writeState(state, home = resolveHome()) {
849
+ const file = agentStatePath(home);
850
+ 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
+ }
860
+ }
861
+ function cursorFor(state, id) {
862
+ return state.cursors[id] ?? 0;
863
+ }
864
+ function withSession(state, id, cursor) {
865
+ return { currentSessionId: id, cursors: { ...state.cursors, [id]: cursor } };
866
+ }
867
+
868
+ // packages/cli/src/commands/context.ts
869
+ var UsageError = class extends Error {
870
+ constructor(message) {
871
+ super(message);
872
+ this.name = "UsageError";
873
+ }
874
+ };
875
+ async function resolveSessionId(ctx) {
876
+ const flag = ctx.flags.session;
877
+ if (flag === "true" || flag === "") {
878
+ throw new UsageError("`--session` needs a session id, e.g. --session <id>");
879
+ }
880
+ if (flag) return flag;
881
+ const state = await readState(ctx.home);
882
+ if (state.currentSessionId) return state.currentSessionId;
883
+ throw new UsageError("no current session - run `chamba open` or pass --session <id>");
884
+ }
885
+ function intFlag(value, fallback) {
886
+ if (value === void 0) return fallback;
887
+ const n = Number(value);
888
+ return Number.isInteger(n) && n >= 0 ? n : fallback;
889
+ }
890
+
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
+ // packages/cli/src/commands/end.ts
1002
+ async function end(ctx) {
1003
+ const id = await resolveSessionId(ctx);
1004
+ const summary = await ctx.client.end(id);
1005
+ note(`session ${summary.meta.id} ended`);
1006
+ return 0;
1007
+ }
1008
+
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
+ }
1066
+ }
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");
1093
+ }
1094
+
1095
+ // packages/cli/src/commands/help.ts
1096
+ var USAGE = `chamba - a browser companion for your terminal agent
1097
+
1098
+ Usage:
1099
+ chamba list sessions and show which is current
1100
+ chamba init [dir] create the .chamba workspace (at the git root, or
1101
+ [--here] <dir>, or the current dir with --here); prints its
1102
+ 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)
1112
+ chamba reply <text> send a line from the agent into the session
1113
+ [--file a,b] attach files by path (comma-separated); --image is
1114
+ [--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]
1117
+ chamba end end the current session
1118
+ chamba stop stop this project's server (it otherwise self-stops
1119
+ after an idle spell); the next open starts a fresh one
1120
+
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
+ chamba skill install install the chamba skill (/chamba) into
1136
+ [--global] ./.claude/skills, or ~/.claude/skills with --global
1137
+ chamba --version print the chamba version
1138
+ chamba help show this help
1139
+
1140
+ Global:
1141
+ --session <id> act on a specific session instead of the current one
1142
+ --port <n> server port (default: CHAMBA_PORT, else a per-project
1143
+ port derived from the workspace location)`;
1144
+ function help(_ctx) {
1145
+ note(USAGE);
1146
+ return Promise.resolve(0);
1147
+ }
1148
+
1149
+ // packages/cli/src/commands/init.ts
1150
+ import path9 from "node:path";
1151
+ async function init(ctx) {
1152
+ const target = resolveTarget(ctx);
1153
+ const existing = findExistingHome({ cwd: target, env: {} });
1154
+ if (existing && !ctx.flags.here) {
1155
+ report("chamba workspace already exists", existing);
1156
+ return 0;
1157
+ }
1158
+ if (existing && ctx.flags.here) {
1159
+ note(`note: shadowing an existing workspace at ${existing}`);
1160
+ }
1161
+ await ensureChambaHome(path9.join(target, ".chamba"));
1162
+ const home = findExistingHome({ cwd: target, env: {} }) ?? path9.join(target, ".chamba");
1163
+ report("created chamba workspace", home);
1164
+ if (path9.basename(home) !== ".chamba") {
1165
+ note("warning: home is not a .chamba directory, so it will not be auto-gitignored");
1166
+ }
1167
+ return 0;
1168
+ }
1169
+ function resolveTarget(ctx) {
1170
+ const explicit = ctx.positionals[0];
1171
+ if (explicit) return path9.resolve(explicit);
1172
+ const root = gitRoot(process.cwd());
1173
+ if (root) return root;
1174
+ if (ctx.flags.here) return path9.resolve(process.cwd());
1175
+ throw new UsageError(
1176
+ "no .git found here - pass a directory (`chamba init <dir>`) or `chamba init --here` to choose where .chamba lives"
1177
+ );
1178
+ }
1179
+ function report(headline, home) {
1180
+ note(headline);
1181
+ note(`home: ${home} port: ${derivePort(home)}`);
1182
+ note("each project gets its own server on its own port; other projects are unaffected");
1183
+ }
1184
+
1185
+ // packages/cli/src/launch.ts
1186
+ import { spawn, spawnSync } from "node:child_process";
1187
+ import { existsSync as existsSync5 } from "node:fs";
1188
+ import { createRequire } from "node:module";
1189
+ import path10 from "node:path";
1190
+ import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
1191
+ var require2 = createRequire(import.meta.url);
1192
+ async function ensureServer(client, target) {
1193
+ if (await client.isUp()) {
1194
+ await verifyHome(client, target);
1195
+ return { spawned: false };
1196
+ }
1197
+ ensureDevBuilds();
1198
+ spawnServer(target);
1199
+ await waitUntilUp(client, target);
1200
+ return { spawned: true };
1201
+ }
1202
+ async function verifyHome(client, target) {
1203
+ const health = await client.health();
1204
+ if (health.home === target.home) return;
1205
+ if (health.home === void 0) {
1206
+ throw new Error(
1207
+ `a chamba server already owns port ${target.port} but predates workspace isolation - stop that process and retry`
1208
+ );
1209
+ }
1210
+ throw new Error(
1211
+ `port ${target.port} is serving a different chamba workspace (${health.home}); run from that project, or set --port / CHAMBA_PORT to run a separate server`
1212
+ );
1213
+ }
1214
+ function spawnServer(target) {
1215
+ const child = spawn(process.execPath, serverLaunchArgs(), {
1216
+ detached: true,
1217
+ stdio: "ignore",
1218
+ env: { ...process.env, CHAMBA_DIR: target.home, CHAMBA_PORT: String(target.port) }
1219
+ });
1220
+ child.unref();
1221
+ }
1222
+ function serverLaunchArgs() {
1223
+ const here = path10.dirname(fileURLToPath4(import.meta.url));
1224
+ const packagedServer = path10.join(here, "server.js");
1225
+ if (existsSync5(packagedServer)) return [packagedServer];
1226
+ const serverBin = require2.resolve("@chamba/server/bin");
1227
+ const tsxLoader = pathToFileURL(require2.resolve("tsx")).href;
1228
+ return ["--import", tsxLoader, serverBin];
1229
+ }
1230
+ 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")), "../../..");
1234
+ const deps = [
1235
+ { label: "web UI", filter: "@chamba/web", dist: resolveWebDist(), marker: "index.html" },
1236
+ {
1237
+ label: "annotation client",
1238
+ filter: "@chamba/inject",
1239
+ dist: resolveInjectDist(),
1240
+ marker: "annotate.js"
1241
+ }
1242
+ ];
1243
+ for (const dep of deps) {
1244
+ if (existsSync5(path10.join(dep.dist, dep.marker))) continue;
1245
+ note(`building chamba ${dep.label} (first run in this environment)...`);
1246
+ const result = spawnSync("pnpm", ["--filter", dep.filter, "build"], {
1247
+ cwd: repoRoot,
1248
+ encoding: "utf8"
1249
+ });
1250
+ if (result.error || result.status !== 0) {
1251
+ const detail = `${result.stderr ?? ""}${result.stdout ?? ""}`.trim();
1252
+ throw new Error(
1253
+ `could not build ${dep.filter} (needed to serve the ${dep.label})${detail ? `:
1254
+ ${detail}` : ""}`
1255
+ );
1256
+ }
1257
+ }
1258
+ }
1259
+ async function waitUntilUp(client, target, timeoutMs = 1e4) {
1260
+ const deadline = Date.now() + timeoutMs;
1261
+ while (Date.now() < deadline) {
1262
+ if (await client.isUp()) {
1263
+ const health = await client.health();
1264
+ if (health.home === target.home) return;
1265
+ throw new Error(
1266
+ `port ${target.port} was taken by a different chamba workspace (${health.home ?? "unknown"})`
1267
+ );
1268
+ }
1269
+ await delay(150);
1270
+ }
1271
+ throw new Error("server did not become healthy in time");
1272
+ }
1273
+ function delay(ms) {
1274
+ return new Promise((resolve) => setTimeout(resolve, ms));
1275
+ }
1276
+
1277
+ // packages/cli/src/commands/open.ts
1278
+ async function open2(ctx) {
1279
+ if (!ctx.workspaceExists) {
1280
+ if (!gitRoot(process.cwd())) {
1281
+ throw new UsageError(
1282
+ "no chamba workspace, and no .git here to anchor one - run `chamba init <dir>` or `chamba init --here` to choose where .chamba lives"
1283
+ );
1284
+ }
1285
+ await ensureChambaHome(ctx.home);
1286
+ note(`created chamba workspace at ${ctx.home}`);
1287
+ }
1288
+ note(`home: ${ctx.home} port: ${ctx.port}`);
1289
+ const { spawned } = await ensureServer(ctx.client, { home: ctx.home, port: ctx.port });
1290
+ 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);
1298
+ const state = await readState(ctx.home);
1299
+ await writeState(withSession(state, id, digest.cursor), ctx.home);
1300
+ out(`http://localhost:${ctx.port}/?s=${id}`);
1301
+ note(" in a container? if this link is unreachable from your host:");
1302
+ note(
1303
+ ` - the server bound ${resolveBindHost([])}; set CHAMBA_HOST=0.0.0.0 so the host can reach it`
1304
+ );
1305
+ 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) {
1308
+ note(
1309
+ " no --kind given: the human picks spec or everyday in the browser; your next poll reports it as `\xB7 Session set to ...`"
1310
+ );
1311
+ }
1312
+ return 0;
1313
+ }
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;
1318
+ }
1319
+
1320
+ // packages/cli/src/commands/poll.ts
1321
+ var DEFAULT_WAIT_MS = 25e3;
1322
+ var MAX_WAIT_MS = 3e4;
1323
+ var DEFAULT_FOLLOW_MS = 4 * 6e4;
1324
+ var BACKOFF_START_MS = 500;
1325
+ var BACKOFF_MAX_MS = 5e3;
1326
+ var IDLE_GUARD_MS = 1e3;
1327
+ var realClock = {
1328
+ now: () => Date.now(),
1329
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
1330
+ };
1331
+ async function poll(ctx, clock = realClock) {
1332
+ const id = await resolveSessionId(ctx);
1333
+ const wait = Math.min(intFlag(ctx.flags.wait, DEFAULT_WAIT_MS), MAX_WAIT_MS);
1334
+ if (ctx.flags.once !== void 0) return pollOnce(ctx, id, wait);
1335
+ const followMs = intFlag(ctx.flags.for, DEFAULT_FOLLOW_MS);
1336
+ return follow(
1337
+ {
1338
+ now: clock.now,
1339
+ sleep: clock.sleep,
1340
+ cursor: async () => cursorFor(await readState(ctx.home), id),
1341
+ poll: (since) => ctx.client.poll(id, since, wait),
1342
+ deliver: async (res) => {
1343
+ printEvents(res.events, ctx.home);
1344
+ await writeState(withSession(await readState(ctx.home), id, res.cursor), ctx.home);
1345
+ },
1346
+ onError: (err, giveUp) => note(pollErrorMessage(err, giveUp)),
1347
+ onHeartbeat: () => note("still listening - re-run chamba poll to keep waiting"),
1348
+ onEnded: () => note("session has ended - nothing more to wait for"),
1349
+ isFatal
1350
+ },
1351
+ // The park floor never drops below the guard sleep, so a small (or zero) `--wait`
1352
+ // - which makes the server answer empty at once - cannot spin the loop hot.
1353
+ {
1354
+ deadline: clock.now() + followMs,
1355
+ parkFloorMs: Math.max(IDLE_GUARD_MS, Math.floor(wait / 2))
1356
+ }
1357
+ );
1358
+ }
1359
+ async function pollOnce(ctx, id, wait) {
1360
+ const state = await readState(ctx.home);
1361
+ const since = cursorFor(state, id);
1362
+ const res = await ctx.client.poll(id, since, wait);
1363
+ if (res.events.length === 0) {
1364
+ note("(no new messages)");
1365
+ return 0;
1366
+ }
1367
+ printEvents(res.events, ctx.home);
1368
+ await writeState(withSession(state, id, res.cursor), ctx.home);
1369
+ return 0;
1370
+ }
1371
+ async function follow(io, cfg) {
1372
+ let backoff = BACKOFF_START_MS;
1373
+ while (true) {
1374
+ const since = await io.cursor();
1375
+ const start = io.now();
1376
+ let res;
1377
+ try {
1378
+ res = await io.poll(since);
1379
+ } catch (err) {
1380
+ const giveUp = io.isFatal(err) || io.now() >= cfg.deadline;
1381
+ io.onError(err, giveUp);
1382
+ if (giveUp) return 1;
1383
+ await io.sleep(backoff);
1384
+ backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
1385
+ continue;
1386
+ }
1387
+ backoff = BACKOFF_START_MS;
1388
+ if (res.events.length > 0) {
1389
+ await io.deliver(res);
1390
+ return 0;
1391
+ }
1392
+ if (res.ended) {
1393
+ io.onEnded?.();
1394
+ return 0;
1395
+ }
1396
+ if (io.now() >= cfg.deadline) {
1397
+ io.onHeartbeat();
1398
+ return 0;
1399
+ }
1400
+ if (io.now() - start < cfg.parkFloorMs) await io.sleep(IDLE_GUARD_MS);
1401
+ }
1402
+ }
1403
+ function isFatal(err) {
1404
+ if (err instanceof ChannelError) return err.status === 404;
1405
+ if (err instanceof SyntaxError) return true;
1406
+ return err instanceof Error && err.name === "ZodError";
1407
+ }
1408
+ function pollErrorMessage(err, giveUp) {
1409
+ if (err instanceof ChannelError && err.status === 404) {
1410
+ return "session not found - it may have been removed";
1411
+ }
1412
+ const detail = err instanceof Error ? err.message : String(err);
1413
+ return giveUp ? `poll failed: ${detail}` : `poll failed, retrying: ${detail}`;
1414
+ }
1415
+
1416
+ // packages/cli/src/commands/reply.ts
1417
+ import { stat } from "node:fs/promises";
1418
+ async function reply(ctx) {
1419
+ const text = ctx.positionals.join(" ").trim();
1420
+ const files = filePaths(ctx.flags);
1421
+ if (!text && files.length === 0) {
1422
+ throw new UsageError("usage: chamba reply <text> [--file a,b] [--image a,b]");
1423
+ }
1424
+ const id = await resolveSessionId(ctx);
1425
+ for (const file of files) {
1426
+ try {
1427
+ const info = await stat(file);
1428
+ if (!info.isFile()) throw new UsageError(`not a file: ${file}`);
1429
+ } catch (err) {
1430
+ if (err instanceof UsageError) throw err;
1431
+ throw new UsageError(`file not found: ${file}`);
1432
+ }
1433
+ }
1434
+ const attachmentIds = [];
1435
+ for (const file of files) {
1436
+ const attachment = await ctx.client.uploadAttachment(id, file);
1437
+ attachmentIds.push(attachment.id);
1438
+ note(`uploaded ${attachment.name}`);
1439
+ }
1440
+ await ctx.client.reply(id, {
1441
+ ...text ? { text } : {},
1442
+ ...attachmentIds.length > 0 ? { attachmentIds } : {}
1443
+ });
1444
+ note(`sent to ${id}`);
1445
+ return 0;
1446
+ }
1447
+ function filePaths(flags) {
1448
+ 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
+ }
1450
+
1451
+ // packages/cli/src/commands/skill.ts
1452
+ import { cpSync, existsSync as existsSync6, rmSync } from "node:fs";
1453
+ import { homedir } from "node:os";
1454
+ import path11 from "node:path";
1455
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
1456
+ async function skill(ctx) {
1457
+ if (ctx.positionals[0] !== "install") {
1458
+ throw new UsageError("usage: chamba skill install [--global]");
1459
+ }
1460
+ const source = resolveSkillSource();
1461
+ if (!source) {
1462
+ note("bundled skill not found");
1463
+ return 1;
1464
+ }
1465
+ const base = ctx.flags.global ? homedir() : process.cwd();
1466
+ const target = path11.join(base, ".claude", "skills", "chamba");
1467
+ rmSync(target, { recursive: true, force: true });
1468
+ cpSync(source, target, { recursive: true });
1469
+ note(`installed the chamba skill to ${target}`);
1470
+ note("reload your agent's skills, then use /chamba");
1471
+ return 0;
1472
+ }
1473
+ function resolveSkillSource() {
1474
+ const here = path11.dirname(fileURLToPath5(import.meta.url));
1475
+ const candidates = [
1476
+ path11.resolve(here, "../skill"),
1477
+ // packaged: dist/cli.js -> <pkg>/skill
1478
+ path11.resolve(here, "../../../../skill")
1479
+ // dev: packages/cli/src/commands -> <repo>/skill
1480
+ ];
1481
+ return candidates.find((dir) => existsSync6(path11.join(dir, "SKILL.md"))) ?? null;
1482
+ }
1483
+
1484
+ // packages/cli/src/commands/status.ts
1485
+ async function status(ctx) {
1486
+ if (!ctx.workspaceExists) {
1487
+ note(
1488
+ "no chamba workspace here - run `chamba open` to start one (creates .chamba at the repo root), or `chamba init` to choose where"
1489
+ );
1490
+ return 0;
1491
+ }
1492
+ const state = await readState(ctx.home);
1493
+ const up = await serverOwnsHome(ctx);
1494
+ const rows = up ? await rowsFromServer(ctx) : await rowsFromDisk(ctx);
1495
+ note(`home: ${ctx.home}`);
1496
+ if (rows.length === 0) {
1497
+ note("no sessions yet - run `chamba open` to start one");
1498
+ return 0;
1499
+ }
1500
+ note(up ? `chamba server up on :${ctx.port}` : "chamba server not running (disk state)");
1501
+ for (const row of rows) {
1502
+ out(formatSession(row.meta, row.presence, row.meta.id === state.currentSessionId));
1503
+ }
1504
+ return 0;
1505
+ }
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
+ async function rowsFromServer(ctx) {
1515
+ const summaries = await ctx.client.list();
1516
+ return summaries.map((s) => ({ meta: s.meta, presence: s.presence }));
1517
+ }
1518
+ async function rowsFromDisk(ctx) {
1519
+ const ids = await listSessionIds(ctx.home);
1520
+ const metas = await Promise.all(ids.map((id) => readMeta(ctx.home, id)));
1521
+ return metas.filter((meta) => meta !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map((meta) => ({ meta }));
1522
+ }
1523
+
1524
+ // packages/cli/src/commands/stop.ts
1525
+ async function stop(ctx) {
1526
+ const info = await readServerInfo(ctx.home);
1527
+ if (!info) {
1528
+ note("no chamba server running here");
1529
+ return 0;
1530
+ }
1531
+ const client = new Client(`http://127.0.0.1:${info.port}/`);
1532
+ if (!await ownsHome(client, ctx.home)) {
1533
+ note("no chamba server running here");
1534
+ return 0;
1535
+ }
1536
+ try {
1537
+ process.kill(info.pid, "SIGTERM");
1538
+ } catch {
1539
+ note("no chamba server running here");
1540
+ return 0;
1541
+ }
1542
+ if (await waitUntilDown(client)) {
1543
+ note(`stopped chamba server on :${info.port}`);
1544
+ return 0;
1545
+ }
1546
+ note(`sent stop to the chamba server (pid ${info.pid}) but it did not exit in time`);
1547
+ return 1;
1548
+ }
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
+ async function waitUntilDown(client, timeoutMs = 5e3) {
1558
+ const deadline = Date.now() + timeoutMs;
1559
+ while (Date.now() < deadline) {
1560
+ if (!await client.isUp()) return true;
1561
+ await delay2(150);
1562
+ }
1563
+ return false;
1564
+ }
1565
+ function delay2(ms) {
1566
+ return new Promise((resolve) => setTimeout(resolve, ms));
1567
+ }
1568
+
1569
+ // packages/cli/src/commands/index.ts
1570
+ var COMMANDS = {
1571
+ init,
1572
+ open: open2,
1573
+ poll,
1574
+ reply,
1575
+ end,
1576
+ attach,
1577
+ status,
1578
+ stop,
1579
+ // Spec sessions: structured interview + live artifacts + export.
1580
+ ask,
1581
+ decision,
1582
+ artifact,
1583
+ export: exportCmd,
1584
+ // Distribution: install the bundled skill into a Claude skills directory.
1585
+ skill,
1586
+ help,
1587
+ "-h": help
1588
+ };
1589
+ function runCommand(name, ctx) {
1590
+ const handler = COMMANDS[name];
1591
+ if (!handler) {
1592
+ note(`unknown command: ${name}`);
1593
+ return help(ctx).then(() => 2);
1594
+ }
1595
+ return handler(ctx);
1596
+ }
1597
+
1598
+ // 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
+ ]);
1609
+ async function main(argv = process.argv.slice(2)) {
1610
+ const { positionals, flags } = parseArgs(argv);
1611
+ if (flags.version && positionals.length === 0) {
1612
+ out(chambaVersion());
1613
+ return 0;
1614
+ }
1615
+ const command = positionals[0] ?? (flags.help ? "help" : "status");
1616
+ const existing = findExistingHome();
1617
+ const home = existing ?? plannedHome();
1618
+ const port = resolvePort(argv, home);
1619
+ const ctx = {
1620
+ client: new Client(`http://127.0.0.1:${port}/`),
1621
+ home,
1622
+ port,
1623
+ flags,
1624
+ positionals: positionals.slice(1),
1625
+ workspaceExists: existing !== null
1626
+ };
1627
+ if (!ctx.workspaceExists && REQUIRES_WORKSPACE.has(command)) {
1628
+ note(
1629
+ "no chamba workspace here - run `chamba open` to start one (creates .chamba at the repo root), or `chamba init` to choose where"
1630
+ );
1631
+ return 2;
1632
+ }
1633
+ try {
1634
+ return await runCommand(command, ctx);
1635
+ } catch (err) {
1636
+ return reportError(err);
1637
+ }
1638
+ }
1639
+ function reportError(err) {
1640
+ if (err instanceof UsageError) {
1641
+ note(err.message);
1642
+ return 2;
1643
+ }
1644
+ if (err instanceof ChannelError) {
1645
+ note(`error: ${err.message}`);
1646
+ return 1;
1647
+ }
1648
+ if (errorCode(err) === "ECONNREFUSED") {
1649
+ note("cannot reach the chamba server - run `chamba open` first");
1650
+ return 1;
1651
+ }
1652
+ note(`error: ${err.message}`);
1653
+ return 1;
1654
+ }
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
+ function invokedAsEntry() {
1672
+ const entry = process.argv[1];
1673
+ if (entry === void 0) return false;
1674
+ try {
1675
+ return realpathSync2(entry) === realpathSync2(fileURLToPath6(import.meta.url));
1676
+ } catch {
1677
+ return false;
1678
+ }
1679
+ }
1680
+ if (invokedAsEntry()) {
1681
+ main().then(
1682
+ (code) => process.exit(code),
1683
+ (err) => {
1684
+ note(`fatal: ${err.message}`);
1685
+ process.exit(1);
1686
+ }
1687
+ );
1688
+ }
1689
+ export {
1690
+ main
1691
+ };