chamba 0.0.1 → 0.1.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 +17 -1
  2. package/dist/cli.js +1190 -0
  3. package/dist/server.js +1675 -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 +213 -0
  8. package/web/assets/abnfDiagram-VRR7QNED-aw1_rWFY.js +1 -0
  9. package/web/assets/arc-BMhSK0Sy.js +1 -0
  10. package/web/assets/architectureDiagram-ZJ3FMSHR-BZDcNKY3.js +36 -0
  11. package/web/assets/blockDiagram-677ZJIJ3-CLVR_gtH.js +132 -0
  12. package/web/assets/c4Diagram-LMCZKHZV-YjWWkNOJ.js +10 -0
  13. package/web/assets/channel-8ymR7Shm.js +1 -0
  14. package/web/assets/chunk-2Q5K7J3B-Bd9qqRl0.js +1 -0
  15. package/web/assets/chunk-32BRIVSS-Cq9EhooX.js +1 -0
  16. package/web/assets/chunk-5VM5RSS4-DYxyelz5.js +15 -0
  17. package/web/assets/chunk-EX3LRPZG-COaiqIlv.js +231 -0
  18. package/web/assets/chunk-JWPE2WC7-CMWXKnkF.js +1 -0
  19. package/web/assets/chunk-MOJQB5TN-BRzAoPL7.js +88 -0
  20. package/web/assets/chunk-RYQCIY6F-BRght6pl.js +1 -0
  21. package/web/assets/chunk-V7JOEXUC-BlbCYGxG.js +206 -0
  22. package/web/assets/chunk-VR4S4FIN-CPoKxaoM.js +1 -0
  23. package/web/assets/chunk-XXDRQBXY-DeNDfu5r.js +1 -0
  24. package/web/assets/classDiagram-OUVF2IWQ-g3gy3kDO.js +1 -0
  25. package/web/assets/classDiagram-v2-EOCWNBFH-g3gy3kDO.js +1 -0
  26. package/web/assets/cose-bilkent-JH36ORCC-C_V1sCw0.js +1 -0
  27. package/web/assets/cynefin-VYW2F7L2-CtWqChj_.js +178 -0
  28. package/web/assets/cynefinDiagram-TSTJHNR4-Q0g7rhQg.js +62 -0
  29. package/web/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  30. package/web/assets/dagre-VKFMJZFB-spMt8V5o.js +4 -0
  31. package/web/assets/defaultLocale-DX6XiGOO.js +1 -0
  32. package/web/assets/diagram-FQU43EPY-CF7kRW9U.js +3 -0
  33. package/web/assets/diagram-G47NLZAW-B-5ygaqt.js +24 -0
  34. package/web/assets/diagram-NH7WQ7WH-CdOGPV2-.js +24 -0
  35. package/web/assets/diagram-OA4YK3LP-D1_1pb2t.js +30 -0
  36. package/web/assets/diagram-WEI45ONY-gD_xjCgB.js +41 -0
  37. package/web/assets/ebnfDiagram-CCIWWBDH-aW4E-V1v.js +1 -0
  38. package/web/assets/erDiagram-Q63AITRT-BWndxFVg.js +85 -0
  39. package/web/assets/flowDiagram-23GEKE2U-CVc37qLo.js +156 -0
  40. package/web/assets/ganttDiagram-NO4QXBWP-DV-2BraZ.js +292 -0
  41. package/web/assets/gitGraphDiagram-IHSO6WYX-YCy7dADs.js +106 -0
  42. package/web/assets/graph-C9eacEi8.js +1 -0
  43. package/web/assets/highlighted-body-OFNGDK62-q_mVDG84.js +1 -0
  44. package/web/assets/index-BG6acTBk.css +1 -0
  45. package/web/assets/index-CBBSnvKL.js +335 -0
  46. package/web/assets/infoDiagram-FWYZ7A6U-ig4wRILz.js +2 -0
  47. package/web/assets/init-Gi6I4Gst.js +1 -0
  48. package/web/assets/ishikawaDiagram-FXEZZL3T-BE8nJNWr.js +70 -0
  49. package/web/assets/journeyDiagram-5HDEW3XC-_olL4LFF.js +139 -0
  50. package/web/assets/kanban-definition-HUTT4EX6-DTFmbw9d.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-CbmtQHdI.js +1 -0
  54. package/web/assets/map-Czzmt4hB.js +1 -0
  55. package/web/assets/mermaid.core-CyLpSBwh.js +314 -0
  56. package/web/assets/mindmap-definition-LN4V7U3C-BFXQ_blT.js +96 -0
  57. package/web/assets/ordinal-Cboi1Yqb.js +1 -0
  58. package/web/assets/pegDiagram-2B236MQR-BzFkomCY.js +1 -0
  59. package/web/assets/pieDiagram-ENE6RG2P-r6gWlwuV.js +39 -0
  60. package/web/assets/quadrantDiagram-ABIIQ3AL-DTFxtS0e.js +7 -0
  61. package/web/assets/railroadDiagram-RFXS5EU6-CbZAYLVe.js +1 -0
  62. package/web/assets/requirementDiagram-TGXJPOKE-DzGi5GvN.js +84 -0
  63. package/web/assets/sankeyDiagram-HTMAVEWB-CBWOwAUD.js +40 -0
  64. package/web/assets/sequenceDiagram-DBY2YBRQ-QX9rQ9ow.js +162 -0
  65. package/web/assets/sizeCapture-X5ZJPWSS-X8SU5JUW.js +1 -0
  66. package/web/assets/stateDiagram-2N3HPSRC-kQgWzKK8.js +1 -0
  67. package/web/assets/stateDiagram-v2-6OUMAXLB-2kXWc9Fv.js +1 -0
  68. package/web/assets/swimlanes-5IMT3BWC-eKT1_uaa.js +2 -0
  69. package/web/assets/swimlanesDiagram-G3AALYLV-B5i-k7ui.js +8 -0
  70. package/web/assets/timeline-definition-FHXFAJF6-BRKXt19C.js +120 -0
  71. package/web/assets/vennDiagram-L72KCM5P-C-mbeAXb.js +34 -0
  72. package/web/assets/wardleyDiagram-EHGQE667-Doojx4Uh.js +78 -0
  73. package/web/assets/xychartDiagram-FW5EYKEG-CQZh8Trp.js +7 -0
  74. package/web/index.html +13 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1190 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/cli/src/index.ts
4
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
5
+
6
+ // packages/server/src/index.ts
7
+ import { serve as honoServe } from "@hono/node-server";
8
+
9
+ // packages/shared/src/annotations.ts
10
+ import { z as z2 } from "zod";
11
+
12
+ // packages/shared/src/messages.ts
13
+ import { z } from "zod";
14
+ var attachmentSchema = z.object({
15
+ id: z.string().min(1),
16
+ name: z.string().min(1),
17
+ relPath: z.string().min(1),
18
+ contentType: z.string().min(1),
19
+ size: z.number().int().nonnegative()
20
+ });
21
+ var messagePayloadSchema = z.object({
22
+ text: z.string().min(1).optional(),
23
+ attachments: z.array(attachmentSchema).optional()
24
+ }).refine((p) => (p.text?.length ?? 0) > 0 || (p.attachments?.length ?? 0) > 0, {
25
+ message: "a message needs text or at least one attachment"
26
+ });
27
+ var systemReasonSchema = z.enum(["session-created", "user-moved-away", "session-ended"]);
28
+ var systemPayloadSchema = z.object({
29
+ reason: systemReasonSchema,
30
+ to: z.string().min(1).optional()
31
+ });
32
+
33
+ // packages/shared/src/annotations.ts
34
+ var ANNOTATION_BATCH = "annotation-batch";
35
+ var annotationSourceSchema = z2.object({
36
+ file: z2.string().min(1),
37
+ line: z2.number().int().positive().optional(),
38
+ column: z2.number().int().positive().optional()
39
+ });
40
+ var annotationComponentSchema = z2.object({
41
+ name: z2.string().min(1),
42
+ source: annotationSourceSchema.optional()
43
+ });
44
+ var annotationTargetSchema = z2.object({
45
+ selector: z2.string().min(1),
46
+ pageUrl: z2.string().min(1),
47
+ viewport: z2.object({
48
+ width: z2.number().int().positive(),
49
+ height: z2.number().int().positive()
50
+ }),
51
+ label: z2.string().optional(),
52
+ component: annotationComponentSchema.optional()
53
+ });
54
+ var annotationItemSchema = z2.object({
55
+ note: z2.string().min(1).optional(),
56
+ target: annotationTargetSchema,
57
+ screenshot: attachmentSchema.optional()
58
+ });
59
+ var annotationBatchPayloadSchema = z2.object({
60
+ items: z2.array(annotationItemSchema).min(1)
61
+ });
62
+
63
+ // packages/shared/src/api.ts
64
+ import { z as z7 } from "zod";
65
+
66
+ // packages/shared/src/events.ts
67
+ import { z as z3 } from "zod";
68
+ var eventSourceSchema = z3.enum(["user", "agent", "system"]);
69
+ var eventEnvelopeSchema = z3.object({
70
+ id: z3.string().uuid(),
71
+ seq: z3.number().int().nonnegative(),
72
+ sessionId: z3.string().min(1),
73
+ ts: z3.string().datetime(),
74
+ type: z3.string().min(1),
75
+ source: eventSourceSchema,
76
+ payload: z3.unknown()
77
+ });
78
+
79
+ // packages/shared/src/presence.ts
80
+ import { z as z4 } from "zod";
81
+ var presenceSchema = z4.enum(["listening", "away"]);
82
+
83
+ // packages/shared/src/session.ts
84
+ import { z as z5 } from "zod";
85
+ var sessionKindSchema = z5.enum(["everyday", "spec"]);
86
+ var sessionStatusSchema = z5.enum(["active", "ended"]);
87
+ var sessionMetaSchema = z5.object({
88
+ id: z5.string().min(1),
89
+ kind: sessionKindSchema,
90
+ title: z5.string().optional(),
91
+ status: sessionStatusSchema,
92
+ createdAt: z5.string().datetime(),
93
+ updatedAt: z5.string().datetime()
94
+ });
95
+
96
+ // packages/shared/src/spec.ts
97
+ import { z as z6 } from "zod";
98
+ var QUESTION_CARD = "question-card";
99
+ var DECISION = "decision";
100
+ var ARTIFACT = "artifact";
101
+ var questionOptionSchema = z6.object({
102
+ label: z6.string().min(1),
103
+ description: z6.string().min(1),
104
+ recommended: z6.boolean().optional()
105
+ });
106
+ var questionCardPayloadSchema = z6.object({
107
+ cardId: z6.string().uuid(),
108
+ question: z6.string().min(1),
109
+ options: z6.array(questionOptionSchema).min(2).max(6),
110
+ /** Optional custom wording for the stop-anytime affordance. */
111
+ stopHint: z6.string().min(1).optional()
112
+ });
113
+ var decisionStatusSchema = z6.enum(["confirmed", "assumed"]);
114
+ var decisionPayloadSchema = z6.object({
115
+ summary: z6.string().min(1),
116
+ status: decisionStatusSchema,
117
+ detail: z6.string().min(1).optional()
118
+ });
119
+ var artifactKindSchema = z6.enum(["draft", "mock", "diagram"]);
120
+ var artifactRefSchema = z6.object({
121
+ kind: artifactKindSchema,
122
+ name: z6.string().min(1),
123
+ relPath: z6.string().min(1),
124
+ contentType: z6.string().min(1),
125
+ size: z6.number().int().nonnegative()
126
+ });
127
+ var artifactPayloadSchema = z6.object({
128
+ artifact: artifactRefSchema
129
+ });
130
+
131
+ // packages/shared/src/api.ts
132
+ var cursorSchema = z7.number().int().nonnegative();
133
+ var createSessionReqSchema = z7.object({
134
+ kind: sessionKindSchema.optional(),
135
+ title: z7.string().min(1).optional()
136
+ });
137
+ var messageBodySchema = z7.object({
138
+ text: z7.string().min(1).optional(),
139
+ attachmentIds: z7.array(z7.string().min(1)).optional()
140
+ }).refine((r) => (r.text?.length ?? 0) > 0 || (r.attachmentIds?.length ?? 0) > 0, {
141
+ message: "a message needs text or at least one attachment"
142
+ });
143
+ var annotationItemReqSchema = z7.object({
144
+ note: z7.string().min(1).optional(),
145
+ target: annotationTargetSchema,
146
+ screenshotAttachmentId: z7.string().min(1).optional()
147
+ });
148
+ var postAnnotationsReqSchema = z7.object({
149
+ items: z7.array(annotationItemReqSchema).min(1)
150
+ });
151
+ var moveAwayReqSchema = z7.object({
152
+ to: z7.string().min(1).optional()
153
+ });
154
+ var postQuestionCardReqSchema = questionCardPayloadSchema;
155
+ var postDecisionReqSchema = decisionPayloadSchema;
156
+ var postArtifactReqSchema = z7.object({
157
+ kind: artifactKindSchema,
158
+ name: z7.string().min(1),
159
+ content: z7.string().min(1)
160
+ });
161
+ var attachmentResSchema = z7.object({
162
+ attachment: attachmentSchema
163
+ });
164
+ var eventResSchema = z7.object({
165
+ event: eventEnvelopeSchema
166
+ });
167
+ var pollResSchema = z7.object({
168
+ events: z7.array(eventEnvelopeSchema),
169
+ cursor: cursorSchema
170
+ });
171
+ var fetchMoreSchema = z7.object({
172
+ hasMore: z7.boolean(),
173
+ earliestLoadedSeq: cursorSchema,
174
+ hint: z7.string()
175
+ });
176
+ var attachDigestSchema = z7.object({
177
+ meta: sessionMetaSchema,
178
+ presence: presenceSchema,
179
+ tail: z7.array(eventEnvelopeSchema),
180
+ cursor: cursorSchema,
181
+ fetchMore: fetchMoreSchema
182
+ });
183
+ var sessionSummarySchema = z7.object({
184
+ meta: sessionMetaSchema,
185
+ presence: presenceSchema
186
+ });
187
+ var listResSchema = z7.object({
188
+ sessions: z7.array(sessionSummarySchema)
189
+ });
190
+ var sessionResSchema = z7.object({
191
+ session: sessionSummarySchema
192
+ });
193
+ var healthResSchema = z7.object({
194
+ ok: z7.boolean(),
195
+ sessions: cursorSchema
196
+ });
197
+
198
+ // packages/shared/src/issues.ts
199
+ import { z as z8 } from "zod";
200
+ var issueTypeSchema = z8.enum(["bug", "feature"]);
201
+ var postIssueReqSchema = z8.object({
202
+ type: issueTypeSchema,
203
+ text: z8.string().min(1),
204
+ screenshotAttachmentIds: z8.array(z8.string().min(1)).optional()
205
+ });
206
+ var issueResSchema = z8.object({
207
+ id: z8.string().min(1),
208
+ path: z8.string().min(1)
209
+ });
210
+
211
+ // packages/shared/src/ws.ts
212
+ import { z as z9 } from "zod";
213
+ var wsServerMessageSchema = z9.discriminatedUnion("t", [
214
+ z9.object({
215
+ t: z9.literal("hello"),
216
+ meta: sessionMetaSchema,
217
+ presence: presenceSchema
218
+ }),
219
+ z9.object({
220
+ t: z9.literal("append"),
221
+ event: eventEnvelopeSchema
222
+ }),
223
+ z9.object({
224
+ t: z9.literal("presence"),
225
+ state: presenceSchema
226
+ }),
227
+ z9.object({
228
+ t: z9.literal("ended"),
229
+ meta: sessionMetaSchema
230
+ })
231
+ ]);
232
+
233
+ // packages/server/src/app.ts
234
+ import { createNodeWebSocket } from "@hono/node-ws";
235
+ import { Hono } from "hono";
236
+
237
+ // packages/server/src/render.ts
238
+ import { marked } from "marked";
239
+
240
+ // packages/server/src/sessions/paths.ts
241
+ import path from "node:path";
242
+ function resolveHome() {
243
+ const override = process.env.CHAMBA_DIR;
244
+ return override && override.length > 0 ? path.resolve(override) : path.join(process.cwd(), ".chamba");
245
+ }
246
+ function sessionsDir(home) {
247
+ return path.join(home, "sessions");
248
+ }
249
+ function sessionDir(home, id) {
250
+ return path.join(sessionsDir(home), id);
251
+ }
252
+ function metaPath(home, id) {
253
+ return path.join(sessionDir(home, id), "meta.json");
254
+ }
255
+ function agentStatePath(home) {
256
+ return path.join(home, "agent-state.json");
257
+ }
258
+
259
+ // packages/server/src/port.ts
260
+ var DEFAULT_PORT = 4319;
261
+ function resolvePort(argv = process.argv.slice(2)) {
262
+ const flag = readFlag(argv, "--port");
263
+ if (flag !== void 0) return parsePort(flag, "--port");
264
+ const env = process.env.CHAMBA_PORT;
265
+ if (env !== void 0 && env.length > 0) return parsePort(env, "CHAMBA_PORT");
266
+ return DEFAULT_PORT;
267
+ }
268
+ function readFlag(argv, name) {
269
+ for (let i = 0; i < argv.length; i++) {
270
+ const arg = argv[i];
271
+ if (arg === name) return argv[i + 1];
272
+ if (arg?.startsWith(`${name}=`)) return arg.slice(name.length + 1);
273
+ }
274
+ return void 0;
275
+ }
276
+ function parsePort(value, source) {
277
+ const port = Number(value);
278
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
279
+ throw new Error(`invalid ${source}: ${value}`);
280
+ }
281
+ return port;
282
+ }
283
+
284
+ // packages/server/src/version.ts
285
+ import { existsSync, readFileSync } from "node:fs";
286
+ import path2 from "node:path";
287
+ import { fileURLToPath } from "node:url";
288
+ function chambaVersion() {
289
+ let dir = path2.dirname(fileURLToPath(import.meta.url));
290
+ for (; ; ) {
291
+ const manifest = path2.join(dir, "package.json");
292
+ if (existsSync(manifest)) {
293
+ try {
294
+ const { version } = JSON.parse(readFileSync(manifest, "utf8"));
295
+ if (typeof version === "string" && version) return version;
296
+ } catch {
297
+ }
298
+ }
299
+ const parent = path2.dirname(dir);
300
+ if (parent === dir) return "0.0.0-dev";
301
+ dir = parent;
302
+ }
303
+ }
304
+
305
+ // packages/server/src/sessions/meta.ts
306
+ import { readdir, readFile, rename, writeFile } from "node:fs/promises";
307
+ async function readMeta(home, id) {
308
+ try {
309
+ const raw = await readFile(metaPath(home, id), "utf8");
310
+ return sessionMetaSchema.parse(JSON.parse(raw));
311
+ } catch (err) {
312
+ if (err.code === "ENOENT") return null;
313
+ throw err;
314
+ }
315
+ }
316
+ async function listSessionIds(home) {
317
+ try {
318
+ const entries = await readdir(sessionsDir(home), { withFileTypes: true });
319
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
320
+ } catch (err) {
321
+ if (err.code === "ENOENT") return [];
322
+ throw err;
323
+ }
324
+ }
325
+
326
+ // packages/server/src/index.ts
327
+ var DEFAULT_IDLE_MS = 5 * 6e4;
328
+
329
+ // packages/cli/src/args.ts
330
+ function parseArgs(argv) {
331
+ const positionals = [];
332
+ const flags = {};
333
+ for (let i = 0; i < argv.length; i++) {
334
+ const arg = argv[i];
335
+ if (arg === void 0) continue;
336
+ if (!arg.startsWith("--")) {
337
+ positionals.push(arg);
338
+ continue;
339
+ }
340
+ const eq = arg.indexOf("=");
341
+ if (eq !== -1) {
342
+ flags[arg.slice(2, eq)] = arg.slice(eq + 1);
343
+ continue;
344
+ }
345
+ const next = argv[i + 1];
346
+ if (next !== void 0 && !next.startsWith("-")) {
347
+ flags[arg.slice(2)] = next;
348
+ i++;
349
+ } else {
350
+ flags[arg.slice(2)] = "true";
351
+ }
352
+ }
353
+ return { positionals, flags };
354
+ }
355
+
356
+ // packages/cli/src/client.ts
357
+ import { readFile as readFile2 } from "node:fs/promises";
358
+ import path3 from "node:path";
359
+ var ChannelError = class extends Error {
360
+ constructor(status2, message) {
361
+ super(message);
362
+ this.status = status2;
363
+ this.name = "ChannelError";
364
+ }
365
+ };
366
+ var Client = class {
367
+ constructor(baseUrl) {
368
+ this.baseUrl = baseUrl;
369
+ }
370
+ /** Liveness probe with a short deadline; never throws - `false` means "spawn it". */
371
+ async isUp(timeoutMs = 800) {
372
+ try {
373
+ const res = await this.fetch("/api/health", { method: "GET", timeoutMs });
374
+ return res.ok;
375
+ } catch {
376
+ return false;
377
+ }
378
+ }
379
+ async health() {
380
+ return this.json("/api/health", { method: "GET", timeoutMs: 2e3 }, healthResSchema);
381
+ }
382
+ async create(req) {
383
+ const res = await this.json(
384
+ "/api/sessions",
385
+ { method: "POST", body: req, timeoutMs: 5e3 },
386
+ sessionResSchema
387
+ );
388
+ return res.session;
389
+ }
390
+ async list() {
391
+ const res = await this.json("/api/sessions", { method: "GET", timeoutMs: 5e3 }, listResSchema);
392
+ return res.sessions;
393
+ }
394
+ async get(id) {
395
+ const res = await this.json(
396
+ `/api/sessions/${encodeURIComponent(id)}`,
397
+ { method: "GET", timeoutMs: 5e3 },
398
+ sessionResSchema
399
+ );
400
+ return res.session;
401
+ }
402
+ async postMessage(id, body) {
403
+ const res = await this.json(
404
+ `/api/sessions/${encodeURIComponent(id)}/messages`,
405
+ { method: "POST", body, timeoutMs: 5e3 },
406
+ eventResSchema
407
+ );
408
+ return res.event;
409
+ }
410
+ async reply(id, body) {
411
+ const res = await this.json(
412
+ `/api/sessions/${encodeURIComponent(id)}/reply`,
413
+ { method: "POST", body, timeoutMs: 5e3 },
414
+ eventResSchema
415
+ );
416
+ return res.event;
417
+ }
418
+ /** Post a structured interview question card (spec sessions). Agent-sourced. */
419
+ async postQuestionCard(id, body) {
420
+ const res = await this.json(
421
+ `/api/sessions/${encodeURIComponent(id)}/cards`,
422
+ { method: "POST", body, timeoutMs: 5e3 },
423
+ eventResSchema
424
+ );
425
+ return res.event;
426
+ }
427
+ /** Record a decision-log entry (confirmed vs. assumed). Agent-sourced. */
428
+ async postDecision(id, body) {
429
+ const res = await this.json(
430
+ `/api/sessions/${encodeURIComponent(id)}/decisions`,
431
+ { method: "POST", body, timeoutMs: 5e3 },
432
+ eventResSchema
433
+ );
434
+ return res.event;
435
+ }
436
+ /** Author or revise a draft/mock/diagram artifact by posting its source text. */
437
+ async postArtifact(id, body) {
438
+ const res = await this.json(
439
+ `/api/sessions/${encodeURIComponent(id)}/artifacts`,
440
+ { method: "POST", body, timeoutMs: 1e4 },
441
+ eventResSchema
442
+ );
443
+ return res.event;
444
+ }
445
+ /**
446
+ * Upload a local file into the session, returning its stored reference. The
447
+ * agent uses this to send an image back (`reply --image`); the returned id is
448
+ * then referenced from a `reply`. Multipart, so the server persists the raw
449
+ * bytes into the session directory rather than smuggling them through JSON.
450
+ */
451
+ async uploadAttachment(id, filePath) {
452
+ const bytes = await readFile2(filePath);
453
+ const form = new FormData();
454
+ form.append("file", new Blob([bytes]), path3.basename(filePath));
455
+ const res = await this.json(
456
+ `/api/sessions/${encodeURIComponent(id)}/attachments`,
457
+ { method: "POST", body: form, timeoutMs: 3e4 },
458
+ attachmentResSchema
459
+ );
460
+ return res.attachment;
461
+ }
462
+ /**
463
+ * The blocking long-poll. The server parks for up to `waitMs`; the client
464
+ * deadline is a little longer so a healthy server always answers before the
465
+ * client gives up (a client timeout would look like a lost message).
466
+ */
467
+ async poll(id, since, waitMs) {
468
+ return this.json(
469
+ `/api/sessions/${encodeURIComponent(id)}/poll?since=${since}&wait=${waitMs}`,
470
+ { method: "GET", timeoutMs: waitMs + 5e3 },
471
+ pollResSchema
472
+ );
473
+ }
474
+ async events(id, since, limit) {
475
+ const query = limit === void 0 ? `since=${since}` : `since=${since}&limit=${limit}`;
476
+ return this.json(
477
+ `/api/sessions/${encodeURIComponent(id)}/events?${query}`,
478
+ { method: "GET", timeoutMs: 5e3 },
479
+ pollResSchema
480
+ );
481
+ }
482
+ async attach(id, tail) {
483
+ return this.json(
484
+ `/api/sessions/${encodeURIComponent(id)}/attach?tail=${tail}`,
485
+ { method: "GET", timeoutMs: 5e3 },
486
+ attachDigestSchema
487
+ );
488
+ }
489
+ async end(id) {
490
+ const res = await this.json(
491
+ `/api/sessions/${encodeURIComponent(id)}/end`,
492
+ { method: "POST", timeoutMs: 5e3 },
493
+ sessionResSchema
494
+ );
495
+ return res.session;
496
+ }
497
+ async json(path9, options, schema) {
498
+ const res = await this.fetch(path9, options);
499
+ if (!res.ok) throw await toError(res);
500
+ return schema.parse(await res.json());
501
+ }
502
+ async fetch(path9, options) {
503
+ const { method = "GET", body, timeoutMs } = options;
504
+ const controller = timeoutMs !== void 0 ? new AbortController() : void 0;
505
+ const timer = controller && timeoutMs !== void 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
506
+ try {
507
+ return await fetch(new URL(path9, this.baseUrl), {
508
+ method,
509
+ signal: controller?.signal,
510
+ ...bodyInit(body)
511
+ });
512
+ } finally {
513
+ if (timer) clearTimeout(timer);
514
+ }
515
+ }
516
+ };
517
+ function bodyInit(body) {
518
+ if (body === void 0) return {};
519
+ if (body instanceof FormData) return { body };
520
+ return { headers: { "content-type": "application/json" }, body: JSON.stringify(body) };
521
+ }
522
+ async function toError(res) {
523
+ let message = `${res.status} ${res.statusText}`;
524
+ try {
525
+ const body = await res.json();
526
+ if (typeof body.error === "string") message = body.error;
527
+ } catch {
528
+ }
529
+ return new ChannelError(res.status, message);
530
+ }
531
+
532
+ // packages/cli/src/output.ts
533
+ import path4 from "node:path";
534
+ import { z as z10 } from "zod";
535
+ function out(message) {
536
+ process.stdout.write(`${message}
537
+ `);
538
+ }
539
+ function note(message) {
540
+ process.stderr.write(`${message}
541
+ `);
542
+ }
543
+ function eventText(event) {
544
+ const payload = event.payload;
545
+ if (payload && typeof payload === "object") {
546
+ const record = payload;
547
+ if (typeof record.text === "string") return record.text;
548
+ if (typeof record.reason === "string") return record.reason;
549
+ }
550
+ return "";
551
+ }
552
+ function eventAttachments(event) {
553
+ if (!event.payload || typeof event.payload !== "object") return [];
554
+ const parsed = z10.array(attachmentSchema).safeParse(event.payload.attachments);
555
+ return parsed.success ? parsed.data : [];
556
+ }
557
+ function formatEvent(event) {
558
+ if (event.source === "system") return `\xB7 ${eventText(event)}`;
559
+ const who = event.source === "user" ? "you" : "agent";
560
+ const text = eventText(event);
561
+ return text ? `${who}: ${text}` : `${who}:`;
562
+ }
563
+ function annotationItems(event) {
564
+ const parsed = annotationBatchPayloadSchema.safeParse(event.payload);
565
+ return parsed.success ? parsed.data.items : [];
566
+ }
567
+ function formatAnnotationBatch(event, home) {
568
+ const items = annotationItems(event);
569
+ const who = event.source === "user" ? "you" : "agent";
570
+ const lines = [`${who} [${items.length} annotation${items.length === 1 ? "" : "s"}]:`];
571
+ items.forEach((item, index) => {
572
+ const { target } = item;
573
+ lines.push(` ${index + 1}. ${item.note ?? "(no comment)"}`);
574
+ lines.push(` element: ${target.label ?? target.selector}`);
575
+ lines.push(` selector: ${target.selector}`);
576
+ lines.push(
577
+ ` page: ${target.pageUrl} viewport: ${target.viewport.width}x${target.viewport.height}`
578
+ );
579
+ if (target.component) {
580
+ const source = target.component.source;
581
+ const at = source ? ` (${source.file}${source.line ? `:${source.line}` : ""})` : "";
582
+ lines.push(` component: ${target.component.name}${at}`);
583
+ }
584
+ if (item.screenshot) {
585
+ const abs = path4.join(sessionDir(home, event.sessionId), item.screenshot.relPath);
586
+ lines.push(` [shot] ${item.screenshot.name} -> ${abs}`);
587
+ }
588
+ });
589
+ return lines;
590
+ }
591
+ function formatQuestionCard(event) {
592
+ const parsed = questionCardPayloadSchema.safeParse(event.payload);
593
+ if (!parsed.success) return [formatEvent(event)];
594
+ const card = parsed.data;
595
+ const lines = [`agent [question]: ${card.question}`];
596
+ card.options.forEach((option, index) => {
597
+ const rec = option.recommended ? " (recommended)" : "";
598
+ lines.push(` ${index + 1}. ${option.label}${rec} - ${option.description}`);
599
+ });
600
+ lines.push(
601
+ ` (the user can also type a custom answer${card.stopHint ? ` or ${card.stopHint}` : ", or stop anytime"})`
602
+ );
603
+ return lines;
604
+ }
605
+ function formatDecision(event) {
606
+ const parsed = decisionPayloadSchema.safeParse(event.payload);
607
+ if (!parsed.success) return formatEvent(event);
608
+ const { status: status2, summary, detail } = parsed.data;
609
+ return `agent [decision: ${status2}]: ${summary}${detail ? ` - ${detail}` : ""}`;
610
+ }
611
+ function formatArtifact(event, home) {
612
+ const parsed = artifactRefSchema.safeParse(
613
+ event.payload?.artifact
614
+ );
615
+ if (!parsed.success) return formatEvent(event);
616
+ const ref = parsed.data;
617
+ const abs = path4.join(sessionDir(home, event.sessionId), ref.relPath);
618
+ return `agent [artifact ${ref.kind} "${ref.name}"] -> ${abs}`;
619
+ }
620
+ function printEvents(events, home) {
621
+ for (const event of events) {
622
+ if (event.type === ANNOTATION_BATCH) {
623
+ for (const line of formatAnnotationBatch(event, home)) out(line);
624
+ continue;
625
+ }
626
+ if (event.type === QUESTION_CARD) {
627
+ for (const line of formatQuestionCard(event)) out(line);
628
+ continue;
629
+ }
630
+ if (event.type === DECISION) {
631
+ out(formatDecision(event));
632
+ continue;
633
+ }
634
+ if (event.type === ARTIFACT) {
635
+ out(formatArtifact(event, home));
636
+ continue;
637
+ }
638
+ out(formatEvent(event));
639
+ for (const att of eventAttachments(event)) {
640
+ out(` [file] ${att.name} -> ${path4.join(sessionDir(home, event.sessionId), att.relPath)}`);
641
+ }
642
+ }
643
+ }
644
+ function formatSession(meta, presence, current) {
645
+ const marker = current ? "*" : " ";
646
+ const title = meta.title ? ` "${meta.title}"` : "";
647
+ const live = presence ? ` ${presence}` : "";
648
+ return `${marker} ${meta.id} ${meta.status}${live} ${meta.kind}${title}`;
649
+ }
650
+
651
+ // packages/cli/src/commands/artifact.ts
652
+ import { readFile as readFile4 } from "node:fs/promises";
653
+
654
+ // packages/cli/src/state.ts
655
+ import { mkdir, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
656
+ import path5 from "node:path";
657
+ import { z as z11 } from "zod";
658
+ var agentStateSchema = z11.object({
659
+ currentSessionId: z11.string().optional(),
660
+ cursors: z11.record(z11.number().int().nonnegative()).default({})
661
+ });
662
+ async function readState(home = resolveHome()) {
663
+ try {
664
+ const raw = await readFile3(agentStatePath(home), "utf8");
665
+ return agentStateSchema.parse(JSON.parse(raw));
666
+ } catch {
667
+ return { cursors: {} };
668
+ }
669
+ }
670
+ async function writeState(state, home = resolveHome()) {
671
+ const file = agentStatePath(home);
672
+ await mkdir(path5.dirname(file), { recursive: true });
673
+ const tmp = `${file}.tmp`;
674
+ await writeFile2(tmp, `${JSON.stringify(state, null, 2)}
675
+ `, "utf8");
676
+ await rename2(tmp, file);
677
+ }
678
+ function cursorFor(state, id) {
679
+ return state.cursors[id] ?? 0;
680
+ }
681
+ function withSession(state, id, cursor) {
682
+ return { currentSessionId: id, cursors: { ...state.cursors, [id]: cursor } };
683
+ }
684
+
685
+ // packages/cli/src/commands/context.ts
686
+ var UsageError = class extends Error {
687
+ constructor(message) {
688
+ super(message);
689
+ this.name = "UsageError";
690
+ }
691
+ };
692
+ async function resolveSessionId(ctx) {
693
+ if (ctx.flags.session) return ctx.flags.session;
694
+ const state = await readState(ctx.home);
695
+ if (state.currentSessionId) return state.currentSessionId;
696
+ throw new UsageError("no current session - run `chamba open` or pass --session <id>");
697
+ }
698
+ function intFlag(value, fallback) {
699
+ if (value === void 0) return fallback;
700
+ const n = Number(value);
701
+ return Number.isInteger(n) && n >= 0 ? n : fallback;
702
+ }
703
+
704
+ // packages/cli/src/commands/artifact.ts
705
+ async function artifact(ctx) {
706
+ const kind = artifactKindSchema.safeParse(ctx.flags.kind);
707
+ if (!kind.success) {
708
+ throw new UsageError(
709
+ "usage: chamba artifact --kind draft|mock|diagram --name <name> --file <path>"
710
+ );
711
+ }
712
+ const name = ctx.flags.name?.trim();
713
+ if (!name || name === "true") throw new UsageError("chamba artifact needs --name <name>");
714
+ const file = ctx.flags.file;
715
+ if (!file || file === "true") throw new UsageError("chamba artifact needs --file <path>");
716
+ let content;
717
+ try {
718
+ content = await readFile4(file, "utf8");
719
+ } catch (err) {
720
+ throw new UsageError(`could not read ${file}: ${err.message}`);
721
+ }
722
+ const result = postArtifactReqSchema.safeParse({ kind: kind.data, name, content });
723
+ if (!result.success) {
724
+ throw new UsageError(
725
+ `invalid artifact: ${result.error.issues.map((i) => i.message).join("; ")}`
726
+ );
727
+ }
728
+ const id = await resolveSessionId(ctx);
729
+ const event = await ctx.client.postArtifact(id, result.data);
730
+ const ref = event.payload.artifact;
731
+ note(`posted ${kind.data} "${name}" to ${id}${ref?.relPath ? ` (${ref.relPath})` : ""}`);
732
+ return 0;
733
+ }
734
+
735
+ // packages/cli/src/commands/ask.ts
736
+ import { randomUUID } from "node:crypto";
737
+ import { readFile as readFile5 } from "node:fs/promises";
738
+ async function ask(ctx) {
739
+ const source = await readCardSource(ctx);
740
+ let parsed;
741
+ try {
742
+ parsed = JSON.parse(source);
743
+ } catch (err) {
744
+ throw new UsageError(`--json is not valid JSON: ${err.message}`);
745
+ }
746
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && !("cardId" in parsed)) {
747
+ parsed.cardId = randomUUID();
748
+ }
749
+ const result = postQuestionCardReqSchema.safeParse(parsed);
750
+ if (!result.success) {
751
+ throw new UsageError(
752
+ `invalid question card: ${result.error.issues.map((i) => `${i.path.join(".")} ${i.message}`).join("; ")}`
753
+ );
754
+ }
755
+ const id = await resolveSessionId(ctx);
756
+ await ctx.client.postQuestionCard(id, result.data);
757
+ note(`asked ${id}: "${result.data.question}" (${result.data.options.length} options)`);
758
+ return 0;
759
+ }
760
+ async function readCardSource(ctx) {
761
+ if (ctx.flags.json && ctx.flags.json !== "true") return ctx.flags.json;
762
+ const file = ctx.positionals[0];
763
+ if (file) {
764
+ try {
765
+ return await readFile5(file, "utf8");
766
+ } catch (err) {
767
+ throw new UsageError(`could not read card file ${file}: ${err.message}`);
768
+ }
769
+ }
770
+ throw new UsageError(
771
+ `usage: chamba ask --json '{"question":"...","options":[...]}' | <file.json>`
772
+ );
773
+ }
774
+
775
+ // packages/cli/src/commands/attach.ts
776
+ async function attach(ctx) {
777
+ const id = ctx.positionals[0] ?? ctx.flags.session;
778
+ if (!id) throw new UsageError("usage: chamba attach <session-id>");
779
+ const tail = intFlag(ctx.flags.tail, 20);
780
+ const digest = await ctx.client.attach(id, tail);
781
+ note(`session ${digest.meta.id} - ${digest.meta.status}, agent ${digest.presence}`);
782
+ if (digest.fetchMore.hasMore) note(`(${digest.fetchMore.hint})`);
783
+ printEvents(digest.tail, ctx.home);
784
+ const state = await readState(ctx.home);
785
+ await writeState(withSession(state, digest.meta.id, digest.cursor), ctx.home);
786
+ note(`now current - poll from cursor ${digest.cursor}`);
787
+ return 0;
788
+ }
789
+
790
+ // packages/cli/src/commands/decision.ts
791
+ async function decision(ctx) {
792
+ const summary = ctx.flags.summary?.trim();
793
+ if (!summary || summary === "true") {
794
+ throw new UsageError(
795
+ 'usage: chamba decision --status confirmed|assumed --summary "..." [--detail "..."]'
796
+ );
797
+ }
798
+ const detail = ctx.flags.detail && ctx.flags.detail !== "true" ? ctx.flags.detail : void 0;
799
+ const result = postDecisionReqSchema.safeParse({
800
+ summary,
801
+ status: ctx.flags.status,
802
+ ...detail ? { detail } : {}
803
+ });
804
+ if (!result.success) {
805
+ throw new UsageError("--status must be `confirmed` or `assumed`");
806
+ }
807
+ const id = await resolveSessionId(ctx);
808
+ await ctx.client.postDecision(id, result.data);
809
+ note(`recorded ${result.data.status} decision on ${id}`);
810
+ return 0;
811
+ }
812
+
813
+ // packages/cli/src/commands/end.ts
814
+ async function end(ctx) {
815
+ const id = await resolveSessionId(ctx);
816
+ const summary = await ctx.client.end(id);
817
+ note(`session ${summary.meta.id} ended`);
818
+ return 0;
819
+ }
820
+
821
+ // packages/cli/src/commands/export.ts
822
+ import { copyFile, mkdir as mkdir2, readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
823
+ import path6 from "node:path";
824
+ async function exportCmd(ctx) {
825
+ const dest = ctx.positionals[0];
826
+ if (!dest) throw new UsageError("usage: chamba export <dest> [--session <id>]");
827
+ const id = await resolveSessionId(ctx);
828
+ const { events } = await ctx.client.events(id, 0);
829
+ const summary = await assemblePackage({
830
+ events,
831
+ sessionDir: sessionDir(ctx.home, id),
832
+ dest: path6.resolve(dest)
833
+ });
834
+ note(
835
+ `exported ${id} -> ${path6.resolve(dest)} (spec.md${summary.hasDraft ? "" : " placeholder"}, ${summary.assets.length} asset${summary.assets.length === 1 ? "" : "s"}, ${summary.decisions} decision${summary.decisions === 1 ? "" : "s"})`
836
+ );
837
+ out(path6.resolve(dest));
838
+ return 0;
839
+ }
840
+ async function assemblePackage(input) {
841
+ const artifacts = /* @__PURE__ */ new Map();
842
+ let canonicalDraft;
843
+ const decisions = [];
844
+ for (const event of input.events) {
845
+ if (event.type === ARTIFACT) {
846
+ const parsed = artifactRefSchema.safeParse(
847
+ event.payload?.artifact
848
+ );
849
+ if (!parsed.success) continue;
850
+ const ref = parsed.data;
851
+ artifacts.set(`${ref.kind}:${ref.name}`, ref);
852
+ if (ref.kind === "draft") canonicalDraft = ref;
853
+ } else if (event.type === DECISION) {
854
+ const parsed = decisionPayloadSchema.safeParse(event.payload);
855
+ if (parsed.success) decisions.push(parsed.data);
856
+ }
857
+ }
858
+ await mkdir2(input.dest, { recursive: true });
859
+ const assetsDir = path6.join(input.dest, "assets");
860
+ const draftBody = canonicalDraft ? await readArtifact(input.sessionDir, canonicalDraft) : void 0;
861
+ await writeFile3(
862
+ path6.join(input.dest, "spec.md"),
863
+ draftBody ?? "# Spec\n\n_No draft was authored in this session._\n",
864
+ "utf8"
865
+ );
866
+ const assets = [];
867
+ for (const ref of artifacts.values()) {
868
+ if (ref === canonicalDraft) continue;
869
+ const src = path6.join(input.sessionDir, ref.relPath);
870
+ const base = path6.basename(ref.relPath);
871
+ try {
872
+ await mkdir2(assetsDir, { recursive: true });
873
+ await copyFile(src, path6.join(assetsDir, base));
874
+ assets.push(base);
875
+ } catch {
876
+ }
877
+ }
878
+ await writeFile3(path6.join(input.dest, "decisions.md"), renderDecisions(decisions), "utf8");
879
+ return { hasDraft: Boolean(draftBody), assets, decisions: decisions.length };
880
+ }
881
+ async function readArtifact(dir, ref) {
882
+ try {
883
+ return await readFile6(path6.join(dir, ref.relPath), "utf8");
884
+ } catch {
885
+ return void 0;
886
+ }
887
+ }
888
+ function renderDecisions(decisions) {
889
+ const confirmed = decisions.filter((d) => d.status === "confirmed");
890
+ const assumed = decisions.filter((d) => d.status === "assumed");
891
+ const section = (items) => items.length === 0 ? "_None._\n" : `${items.map((d) => `- ${d.summary}${d.detail ? `
892
+ ${d.detail}` : ""}`).join("\n")}
893
+ `;
894
+ return [
895
+ "# Decision log",
896
+ "",
897
+ "## Confirmed",
898
+ "",
899
+ section(confirmed),
900
+ "## Assumed / open questions",
901
+ "",
902
+ section(assumed)
903
+ ].join("\n");
904
+ }
905
+
906
+ // packages/cli/src/commands/help.ts
907
+ var USAGE = `chamba - a browser companion for your terminal agent
908
+
909
+ Usage:
910
+ chamba list sessions and show which is current
911
+ chamba open [--kind K] start a session (K: everyday | spec), print its URL
912
+ [--title T]
913
+ chamba poll [--wait ms] block for the next human/system events; a batch may
914
+ include "user moved away" (hand off with attach) or
915
+ "session ended" (stop polling, resume terminal work)
916
+ chamba reply <text> send a line from the agent into the session
917
+ [--file a,b] attach files by path (comma-separated); --image is
918
+ [--image a,b] an alias. text is optional when files are attached
919
+ chamba attach <id> load a session's recent tail and make it current
920
+ [--tail N]
921
+ chamba end end the current session
922
+
923
+ Spec sessions (open with --kind spec):
924
+ chamba ask --json '{...}' post a question card (question + 2-6 options with
925
+ | <file.json> descriptions, optional "recommended"); a cardId is
926
+ minted for you. The answer returns as a message
927
+ chamba decision record a decision: --status confirmed|assumed
928
+ --status S --summary "..." [--detail "..."]
929
+ --summary T
930
+ chamba artifact author/revise a draft (markdown), mock (HTML), or
931
+ --kind K diagram (mermaid): --kind draft|mock|diagram
932
+ --name N --name <name> --file <path>. Re-post to revise
933
+ --file P
934
+ chamba export <dest> copy the finalized package (spec.md + decisions.md
935
+ + assets/) to <dest> (e.g. docs/)
936
+
937
+ chamba skill install install the chamba skill (/chamba) into
938
+ [--global] ./.claude/skills, or ~/.claude/skills with --global
939
+ chamba --version print the chamba version
940
+ chamba help show this help
941
+
942
+ Global:
943
+ --session <id> act on a specific session instead of the current one
944
+ --port <n> server port (default: CHAMBA_PORT or 4319)`;
945
+ function help(_ctx) {
946
+ note(USAGE);
947
+ return Promise.resolve(0);
948
+ }
949
+
950
+ // packages/cli/src/launch.ts
951
+ import { spawn } from "node:child_process";
952
+ import { existsSync as existsSync2 } from "node:fs";
953
+ import { createRequire } from "node:module";
954
+ import path7 from "node:path";
955
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
956
+ var require2 = createRequire(import.meta.url);
957
+ async function ensureServer(client) {
958
+ if (await client.isUp()) return { spawned: false };
959
+ spawnServer();
960
+ await waitUntilUp(client);
961
+ return { spawned: true };
962
+ }
963
+ function spawnServer() {
964
+ const child = spawn(process.execPath, serverLaunchArgs(), {
965
+ detached: true,
966
+ stdio: "ignore",
967
+ env: process.env
968
+ });
969
+ child.unref();
970
+ }
971
+ function serverLaunchArgs() {
972
+ const here = path7.dirname(fileURLToPath2(import.meta.url));
973
+ const packagedServer = path7.join(here, "server.js");
974
+ if (existsSync2(packagedServer)) return [packagedServer];
975
+ const serverBin = require2.resolve("@chamba/server/bin");
976
+ const tsxLoader = pathToFileURL(require2.resolve("tsx")).href;
977
+ return ["--import", tsxLoader, serverBin];
978
+ }
979
+ async function waitUntilUp(client, timeoutMs = 1e4) {
980
+ const deadline = Date.now() + timeoutMs;
981
+ while (Date.now() < deadline) {
982
+ if (await client.isUp()) return;
983
+ await delay(150);
984
+ }
985
+ throw new Error("server did not become healthy in time");
986
+ }
987
+ function delay(ms) {
988
+ return new Promise((resolve) => setTimeout(resolve, ms));
989
+ }
990
+
991
+ // packages/cli/src/commands/open.ts
992
+ async function open(ctx) {
993
+ const { spawned } = await ensureServer(ctx.client);
994
+ if (spawned) note("started chamba server");
995
+ const req = {
996
+ ...ctx.flags.kind ? { kind: parseKind(ctx.flags.kind) } : {},
997
+ ...ctx.flags.title ? { title: ctx.flags.title } : {}
998
+ };
999
+ const summary = await ctx.client.create(req);
1000
+ const id = summary.meta.id;
1001
+ const digest = await ctx.client.attach(id, 0);
1002
+ const state = await readState(ctx.home);
1003
+ await writeState(withSession(state, id, digest.cursor), ctx.home);
1004
+ out(`http://localhost:${ctx.port}/?s=${id}`);
1005
+ note(`session ${id} - open the URL, then run \`chamba poll\` to wait for messages`);
1006
+ return 0;
1007
+ }
1008
+ function parseKind(value) {
1009
+ const parsed = sessionKindSchema.safeParse(value);
1010
+ if (!parsed.success) throw new UsageError(`invalid --kind: ${value} (expected everyday or spec)`);
1011
+ return parsed.data;
1012
+ }
1013
+
1014
+ // packages/cli/src/commands/poll.ts
1015
+ async function poll(ctx) {
1016
+ const id = await resolveSessionId(ctx);
1017
+ const wait = Math.min(intFlag(ctx.flags.wait, 25e3), 3e4);
1018
+ const state = await readState(ctx.home);
1019
+ const since = cursorFor(state, id);
1020
+ const res = await ctx.client.poll(id, since, wait);
1021
+ if (res.events.length === 0) {
1022
+ note("(no new messages)");
1023
+ return 0;
1024
+ }
1025
+ printEvents(res.events, ctx.home);
1026
+ await writeState(withSession(state, id, res.cursor), ctx.home);
1027
+ return 0;
1028
+ }
1029
+
1030
+ // packages/cli/src/commands/reply.ts
1031
+ async function reply(ctx) {
1032
+ const text = ctx.positionals.join(" ").trim();
1033
+ const files = filePaths(ctx.flags);
1034
+ if (!text && files.length === 0) {
1035
+ throw new UsageError("usage: chamba reply <text> [--file a,b] [--image a,b]");
1036
+ }
1037
+ const id = await resolveSessionId(ctx);
1038
+ const attachmentIds = [];
1039
+ for (const file of files) {
1040
+ const attachment = await ctx.client.uploadAttachment(id, file);
1041
+ attachmentIds.push(attachment.id);
1042
+ note(`uploaded ${attachment.name}`);
1043
+ }
1044
+ await ctx.client.reply(id, {
1045
+ ...text ? { text } : {},
1046
+ ...attachmentIds.length > 0 ? { attachmentIds } : {}
1047
+ });
1048
+ note(`sent to ${id}`);
1049
+ return 0;
1050
+ }
1051
+ function filePaths(flags) {
1052
+ 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);
1053
+ }
1054
+
1055
+ // packages/cli/src/commands/skill.ts
1056
+ import { cpSync, existsSync as existsSync3 } from "node:fs";
1057
+ import { homedir } from "node:os";
1058
+ import path8 from "node:path";
1059
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
1060
+ async function skill(ctx) {
1061
+ if (ctx.positionals[0] !== "install") {
1062
+ throw new UsageError("usage: chamba skill install [--global]");
1063
+ }
1064
+ const source = resolveSkillSource();
1065
+ if (!source) {
1066
+ note("bundled skill not found");
1067
+ return 1;
1068
+ }
1069
+ const base = ctx.flags.global ? homedir() : process.cwd();
1070
+ const target = path8.join(base, ".claude", "skills", "chamba");
1071
+ cpSync(source, target, { recursive: true });
1072
+ note(`installed the chamba skill to ${target}`);
1073
+ note("reload your agent's skills, then use /chamba");
1074
+ return 0;
1075
+ }
1076
+ function resolveSkillSource() {
1077
+ const here = path8.dirname(fileURLToPath3(import.meta.url));
1078
+ const candidates = [
1079
+ path8.resolve(here, "../skill"),
1080
+ // packaged: dist/cli.js -> <pkg>/skill
1081
+ path8.resolve(here, "../../../../skill")
1082
+ // dev: packages/cli/src/commands -> <repo>/skill
1083
+ ];
1084
+ return candidates.find((dir) => existsSync3(path8.join(dir, "SKILL.md"))) ?? null;
1085
+ }
1086
+
1087
+ // packages/cli/src/commands/status.ts
1088
+ async function status(ctx) {
1089
+ const state = await readState(ctx.home);
1090
+ const up = await ctx.client.isUp();
1091
+ const rows = up ? await rowsFromServer(ctx) : await rowsFromDisk(ctx);
1092
+ if (rows.length === 0) {
1093
+ note("no sessions yet - run `chamba open` to start one");
1094
+ return 0;
1095
+ }
1096
+ note(up ? `chamba server up on :${ctx.port}` : "chamba server not running (disk state)");
1097
+ for (const row of rows) {
1098
+ out(formatSession(row.meta, row.presence, row.meta.id === state.currentSessionId));
1099
+ }
1100
+ return 0;
1101
+ }
1102
+ async function rowsFromServer(ctx) {
1103
+ const summaries = await ctx.client.list();
1104
+ return summaries.map((s) => ({ meta: s.meta, presence: s.presence }));
1105
+ }
1106
+ async function rowsFromDisk(ctx) {
1107
+ const ids = await listSessionIds(ctx.home);
1108
+ const metas = await Promise.all(ids.map((id) => readMeta(ctx.home, id)));
1109
+ return metas.filter((meta) => meta !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map((meta) => ({ meta }));
1110
+ }
1111
+
1112
+ // packages/cli/src/commands/index.ts
1113
+ var COMMANDS = {
1114
+ open,
1115
+ poll,
1116
+ reply,
1117
+ end,
1118
+ attach,
1119
+ status,
1120
+ // Spec sessions: structured interview + live artifacts + export.
1121
+ ask,
1122
+ decision,
1123
+ artifact,
1124
+ export: exportCmd,
1125
+ // Distribution: install the bundled skill into a Claude skills directory.
1126
+ skill,
1127
+ help,
1128
+ "-h": help
1129
+ };
1130
+ function runCommand(name, ctx) {
1131
+ const handler = COMMANDS[name];
1132
+ if (!handler) {
1133
+ note(`unknown command: ${name}`);
1134
+ return help(ctx).then(() => 2);
1135
+ }
1136
+ return handler(ctx);
1137
+ }
1138
+
1139
+ // packages/cli/src/index.ts
1140
+ async function main(argv = process.argv.slice(2)) {
1141
+ const port = resolvePort(argv);
1142
+ const { positionals, flags } = parseArgs(argv);
1143
+ if (flags.version && positionals.length === 0) {
1144
+ out(chambaVersion());
1145
+ return 0;
1146
+ }
1147
+ const command = positionals[0] ?? (flags.help ? "help" : "status");
1148
+ const ctx = {
1149
+ client: new Client(`http://127.0.0.1:${port}/`),
1150
+ home: resolveHome(),
1151
+ port,
1152
+ flags,
1153
+ positionals: positionals.slice(1)
1154
+ };
1155
+ try {
1156
+ return await runCommand(command, ctx);
1157
+ } catch (err) {
1158
+ return reportError(err);
1159
+ }
1160
+ }
1161
+ function reportError(err) {
1162
+ if (err instanceof UsageError) {
1163
+ note(err.message);
1164
+ return 2;
1165
+ }
1166
+ if (err instanceof ChannelError) {
1167
+ note(`error: ${err.message}`);
1168
+ return 1;
1169
+ }
1170
+ const cause = err.cause;
1171
+ if (cause?.code === "ECONNREFUSED") {
1172
+ note("cannot reach the chamba server - run `chamba open` first");
1173
+ return 1;
1174
+ }
1175
+ note(`error: ${err.message}`);
1176
+ return 1;
1177
+ }
1178
+ var invokedDirectly = process.argv[1] !== void 0 && pathToFileURL2(process.argv[1]).href === import.meta.url;
1179
+ if (invokedDirectly) {
1180
+ main().then(
1181
+ (code) => process.exit(code),
1182
+ (err) => {
1183
+ note(`fatal: ${err.message}`);
1184
+ process.exit(1);
1185
+ }
1186
+ );
1187
+ }
1188
+ export {
1189
+ main
1190
+ };