chamba 0.1.0 → 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 (72) hide show
  1. package/README.md +49 -0
  2. package/dist/cli.js +662 -161
  3. package/dist/server.js +355 -111
  4. package/inject/annotate.js +1 -1
  5. package/package.json +1 -1
  6. package/skill/SKILL.md +91 -19
  7. package/web/assets/{abnfDiagram-VRR7QNED-aw1_rWFY.js → abnfDiagram-VRR7QNED-C5V2hrtE.js} +1 -1
  8. package/web/assets/{arc-BMhSK0Sy.js → arc-d7OTmxZy.js} +1 -1
  9. package/web/assets/{architectureDiagram-ZJ3FMSHR-BZDcNKY3.js → architectureDiagram-ZJ3FMSHR--CztaeAS.js} +1 -1
  10. package/web/assets/{blockDiagram-677ZJIJ3-CLVR_gtH.js → blockDiagram-677ZJIJ3-DkA5Z_uK.js} +1 -1
  11. package/web/assets/{c4Diagram-LMCZKHZV-YjWWkNOJ.js → c4Diagram-LMCZKHZV-C78dI0l-.js} +1 -1
  12. package/web/assets/channel-D9AxU8kk.js +1 -0
  13. package/web/assets/{chunk-2Q5K7J3B-Bd9qqRl0.js → chunk-2Q5K7J3B-LBNP12XV.js} +1 -1
  14. package/web/assets/{chunk-32BRIVSS-Cq9EhooX.js → chunk-32BRIVSS-DZoXrhCs.js} +1 -1
  15. package/web/assets/{chunk-5VM5RSS4-DYxyelz5.js → chunk-5VM5RSS4-CPjoRxF_.js} +1 -1
  16. package/web/assets/{chunk-EX3LRPZG-COaiqIlv.js → chunk-EX3LRPZG-DSlL8FE9.js} +1 -1
  17. package/web/assets/{chunk-JWPE2WC7-CMWXKnkF.js → chunk-JWPE2WC7-BjbBLYge.js} +1 -1
  18. package/web/assets/{chunk-MOJQB5TN-BRzAoPL7.js → chunk-MOJQB5TN-DCKqdaDr.js} +1 -1
  19. package/web/assets/{chunk-RYQCIY6F-BRght6pl.js → chunk-RYQCIY6F-D5yAURhE.js} +1 -1
  20. package/web/assets/{chunk-V7JOEXUC-BlbCYGxG.js → chunk-V7JOEXUC-DKY2sn06.js} +1 -1
  21. package/web/assets/{chunk-VR4S4FIN-CPoKxaoM.js → chunk-VR4S4FIN-DGiVMHh6.js} +1 -1
  22. package/web/assets/{chunk-XXDRQBXY-DeNDfu5r.js → chunk-XXDRQBXY-CUI1Tkrr.js} +1 -1
  23. package/web/assets/classDiagram-OUVF2IWQ-D7gWuzIF.js +1 -0
  24. package/web/assets/classDiagram-v2-EOCWNBFH-D7gWuzIF.js +1 -0
  25. package/web/assets/{cose-bilkent-JH36ORCC-C_V1sCw0.js → cose-bilkent-JH36ORCC-Ao3LzalV.js} +1 -1
  26. package/web/assets/{cynefin-VYW2F7L2-CtWqChj_.js → cynefin-VYW2F7L2-7JQwpRwH.js} +1 -1
  27. package/web/assets/{cynefinDiagram-TSTJHNR4-Q0g7rhQg.js → cynefinDiagram-TSTJHNR4-BfuANy_V.js} +1 -1
  28. package/web/assets/{dagre-VKFMJZFB-spMt8V5o.js → dagre-VKFMJZFB-DHVnVJCL.js} +1 -1
  29. package/web/assets/{diagram-FQU43EPY-CF7kRW9U.js → diagram-FQU43EPY-OYrtwA2f.js} +1 -1
  30. package/web/assets/{diagram-G47NLZAW-B-5ygaqt.js → diagram-G47NLZAW-CbkLZxYp.js} +1 -1
  31. package/web/assets/{diagram-NH7WQ7WH-CdOGPV2-.js → diagram-NH7WQ7WH-B0z_jTdi.js} +1 -1
  32. package/web/assets/{diagram-OA4YK3LP-D1_1pb2t.js → diagram-OA4YK3LP-BRu_2nwx.js} +1 -1
  33. package/web/assets/{diagram-WEI45ONY-gD_xjCgB.js → diagram-WEI45ONY-B9cpw86D.js} +1 -1
  34. package/web/assets/{ebnfDiagram-CCIWWBDH-aW4E-V1v.js → ebnfDiagram-CCIWWBDH-DaBQ7T4R.js} +1 -1
  35. package/web/assets/{erDiagram-Q63AITRT-BWndxFVg.js → erDiagram-Q63AITRT-D6PQiFj3.js} +1 -1
  36. package/web/assets/{flowDiagram-23GEKE2U-CVc37qLo.js → flowDiagram-23GEKE2U-p_8Wj7It.js} +1 -1
  37. package/web/assets/{ganttDiagram-NO4QXBWP-DV-2BraZ.js → ganttDiagram-NO4QXBWP-BZBwuqu3.js} +1 -1
  38. package/web/assets/{gitGraphDiagram-IHSO6WYX-YCy7dADs.js → gitGraphDiagram-IHSO6WYX-DyPcQ-91.js} +1 -1
  39. package/web/assets/{highlighted-body-OFNGDK62-q_mVDG84.js → highlighted-body-OFNGDK62-Cq7RJZcn.js} +1 -1
  40. package/web/assets/index-BN-QUr5b.js +350 -0
  41. package/web/assets/index-D6-LVQ5m.css +1 -0
  42. package/web/assets/{infoDiagram-FWYZ7A6U-ig4wRILz.js → infoDiagram-FWYZ7A6U-sAgBAeWC.js} +1 -1
  43. package/web/assets/{ishikawaDiagram-FXEZZL3T-BE8nJNWr.js → ishikawaDiagram-FXEZZL3T-COEn9wCs.js} +1 -1
  44. package/web/assets/{journeyDiagram-5HDEW3XC-_olL4LFF.js → journeyDiagram-5HDEW3XC-BPddPRXW.js} +1 -1
  45. package/web/assets/{kanban-definition-HUTT4EX6-DTFmbw9d.js → kanban-definition-HUTT4EX6-B8TW97Oy.js} +1 -1
  46. package/web/assets/{linear-CbmtQHdI.js → linear-CnW5uQ_k.js} +1 -1
  47. package/web/assets/{mermaid.core-CyLpSBwh.js → mermaid.core-bOV5iNX_.js} +4 -4
  48. package/web/assets/{mindmap-definition-LN4V7U3C-BFXQ_blT.js → mindmap-definition-LN4V7U3C-Dtr4xW_-.js} +1 -1
  49. package/web/assets/{pegDiagram-2B236MQR-BzFkomCY.js → pegDiagram-2B236MQR-DqBITK5N.js} +1 -1
  50. package/web/assets/{pieDiagram-ENE6RG2P-r6gWlwuV.js → pieDiagram-ENE6RG2P-DXjxZgy3.js} +1 -1
  51. package/web/assets/{quadrantDiagram-ABIIQ3AL-DTFxtS0e.js → quadrantDiagram-ABIIQ3AL-D4MXx3U7.js} +1 -1
  52. package/web/assets/{railroadDiagram-RFXS5EU6-CbZAYLVe.js → railroadDiagram-RFXS5EU6-LPfpHg4_.js} +1 -1
  53. package/web/assets/{requirementDiagram-TGXJPOKE-DzGi5GvN.js → requirementDiagram-TGXJPOKE-CiDswbjt.js} +1 -1
  54. package/web/assets/{sankeyDiagram-HTMAVEWB-CBWOwAUD.js → sankeyDiagram-HTMAVEWB-VfN_qTEI.js} +1 -1
  55. package/web/assets/{sequenceDiagram-DBY2YBRQ-QX9rQ9ow.js → sequenceDiagram-DBY2YBRQ-kJRIx4zm.js} +1 -1
  56. package/web/assets/{sizeCapture-X5ZJPWSS-X8SU5JUW.js → sizeCapture-X5ZJPWSS-D8vJbAt-.js} +1 -1
  57. package/web/assets/{stateDiagram-2N3HPSRC-kQgWzKK8.js → stateDiagram-2N3HPSRC-9qku-qAj.js} +1 -1
  58. package/web/assets/stateDiagram-v2-6OUMAXLB-DbOQICyv.js +1 -0
  59. package/web/assets/{swimlanes-5IMT3BWC-eKT1_uaa.js → swimlanes-5IMT3BWC-Cdb5FDUM.js} +2 -2
  60. package/web/assets/swimlanesDiagram-G3AALYLV-EDpLmqQv.js +8 -0
  61. package/web/assets/{timeline-definition-FHXFAJF6-BRKXt19C.js → timeline-definition-FHXFAJF6-D0hyJCMm.js} +1 -1
  62. package/web/assets/{vennDiagram-L72KCM5P-C-mbeAXb.js → vennDiagram-L72KCM5P-KsCkGMtr.js} +1 -1
  63. package/web/assets/{wardleyDiagram-EHGQE667-Doojx4Uh.js → wardleyDiagram-EHGQE667-B4jT_1ak.js} +1 -1
  64. package/web/assets/{xychartDiagram-FW5EYKEG-CQZh8Trp.js → xychartDiagram-FW5EYKEG-Bmy0rt8j.js} +1 -1
  65. package/web/index.html +17 -2
  66. package/web/assets/channel-8ymR7Shm.js +0 -1
  67. package/web/assets/classDiagram-OUVF2IWQ-g3gy3kDO.js +0 -1
  68. package/web/assets/classDiagram-v2-EOCWNBFH-g3gy3kDO.js +0 -1
  69. package/web/assets/index-BG6acTBk.css +0 -1
  70. package/web/assets/index-CBBSnvKL.js +0 -335
  71. package/web/assets/stateDiagram-v2-6OUMAXLB-2kXWc9Fv.js +0 -1
  72. package/web/assets/swimlanesDiagram-G3AALYLV-B5i-k7ui.js +0 -8
package/dist/cli.js CHANGED
@@ -1,97 +1,106 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // packages/cli/src/index.ts
4
- import { pathToFileURL as pathToFileURL2 } from "node:url";
4
+ import { realpathSync as realpathSync2 } from "node:fs";
5
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
5
6
 
6
7
  // packages/server/src/index.ts
7
8
  import { serve as honoServe } from "@hono/node-server";
8
9
 
9
10
  // packages/shared/src/annotations.ts
10
- import { z as z2 } from "zod";
11
+ import { z as z3 } from "zod";
11
12
 
12
13
  // packages/shared/src/messages.ts
14
+ import { z as z2 } from "zod";
15
+
16
+ // packages/shared/src/session.ts
13
17
  import { z } from "zod";
14
- var attachmentSchema = z.object({
18
+ var sessionKindSchema = z.enum(["everyday", "spec"]);
19
+ var sessionStatusSchema = z.enum(["active", "ended"]);
20
+ var sessionMetaSchema = z.object({
15
21
  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()
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()
20
36
  });
21
- var messagePayloadSchema = z.object({
22
- text: z.string().min(1).optional(),
23
- attachments: z.array(attachmentSchema).optional()
37
+ var messagePayloadSchema = z2.object({
38
+ text: z2.string().min(1).optional(),
39
+ attachments: z2.array(attachmentSchema).optional()
24
40
  }).refine((p) => (p.text?.length ?? 0) > 0 || (p.attachments?.length ?? 0) > 0, {
25
41
  message: "a message needs text or at least one attachment"
26
42
  });
27
- var systemReasonSchema = z.enum(["session-created", "user-moved-away", "session-ended"]);
28
- var systemPayloadSchema = z.object({
43
+ var systemReasonSchema = z2.enum([
44
+ "session-created",
45
+ "user-moved-away",
46
+ "session-ended",
47
+ "kind-chosen"
48
+ ]);
49
+ var systemPayloadSchema = z2.object({
29
50
  reason: systemReasonSchema,
30
- to: z.string().min(1).optional()
51
+ to: z2.string().min(1).optional(),
52
+ kind: sessionKindSchema.optional()
31
53
  });
32
54
 
33
55
  // packages/shared/src/annotations.ts
34
56
  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()
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()
39
61
  });
40
- var annotationComponentSchema = z2.object({
41
- name: z2.string().min(1),
62
+ var annotationComponentSchema = z3.object({
63
+ name: z3.string().min(1),
42
64
  source: annotationSourceSchema.optional()
43
65
  });
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()
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()
50
72
  }),
51
- label: z2.string().optional(),
73
+ label: z3.string().optional(),
52
74
  component: annotationComponentSchema.optional()
53
75
  });
54
- var annotationItemSchema = z2.object({
55
- note: z2.string().min(1).optional(),
76
+ var annotationItemSchema = z3.object({
77
+ note: z3.string().min(1).optional(),
56
78
  target: annotationTargetSchema,
57
79
  screenshot: attachmentSchema.optional()
58
80
  });
59
- var annotationBatchPayloadSchema = z2.object({
60
- items: z2.array(annotationItemSchema).min(1)
81
+ var annotationBatchPayloadSchema = z3.object({
82
+ items: z3.array(annotationItemSchema).min(1)
61
83
  });
62
84
 
63
85
  // packages/shared/src/api.ts
64
86
  import { z as z7 } from "zod";
65
87
 
66
88
  // 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),
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),
75
97
  source: eventSourceSchema,
76
- payload: z3.unknown()
98
+ payload: z4.unknown()
77
99
  });
78
100
 
79
101
  // 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
102
  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
- });
103
+ var presenceSchema = z5.enum(["listening", "working", "away"]);
95
104
 
96
105
  // packages/shared/src/spec.ts
97
106
  import { z as z6 } from "zod";
@@ -134,6 +143,9 @@ var createSessionReqSchema = z7.object({
134
143
  kind: sessionKindSchema.optional(),
135
144
  title: z7.string().min(1).optional()
136
145
  });
146
+ var setSessionKindReqSchema = z7.object({
147
+ kind: sessionKindSchema
148
+ });
137
149
  var messageBodySchema = z7.object({
138
150
  text: z7.string().min(1).optional(),
139
151
  attachmentIds: z7.array(z7.string().min(1)).optional()
@@ -166,7 +178,8 @@ var eventResSchema = z7.object({
166
178
  });
167
179
  var pollResSchema = z7.object({
168
180
  events: z7.array(eventEnvelopeSchema),
169
- cursor: cursorSchema
181
+ cursor: cursorSchema,
182
+ ended: z7.boolean().optional()
170
183
  });
171
184
  var fetchMoreSchema = z7.object({
172
185
  hasMore: z7.boolean(),
@@ -192,7 +205,10 @@ var sessionResSchema = z7.object({
192
205
  });
193
206
  var healthResSchema = z7.object({
194
207
  ok: z7.boolean(),
195
- sessions: cursorSchema
208
+ sessions: cursorSchema,
209
+ home: z7.string().min(1).optional(),
210
+ port: z7.number().int().positive().optional(),
211
+ version: z7.string().min(1).optional()
196
212
  });
197
213
 
198
214
  // packages/shared/src/issues.ts
@@ -224,6 +240,10 @@ var wsServerMessageSchema = z9.discriminatedUnion("t", [
224
240
  t: z9.literal("presence"),
225
241
  state: presenceSchema
226
242
  }),
243
+ z9.object({
244
+ t: z9.literal("kind"),
245
+ meta: sessionMetaSchema
246
+ }),
227
247
  z9.object({
228
248
  t: z9.literal("ended"),
229
249
  meta: sessionMetaSchema
@@ -234,36 +254,176 @@ var wsServerMessageSchema = z9.discriminatedUnion("t", [
234
254
  import { createNodeWebSocket } from "@hono/node-ws";
235
255
  import { Hono } from "hono";
236
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
+
237
268
  // packages/server/src/render.ts
238
269
  import { marked } from "marked";
239
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
+
240
288
  // 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");
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);
245
345
  }
246
346
  function sessionsDir(home) {
247
- return path.join(home, "sessions");
347
+ return path3.join(home, "sessions");
248
348
  }
249
349
  function sessionDir(home, id) {
250
- return path.join(sessionsDir(home), id);
350
+ return path3.join(sessionsDir(home), id);
251
351
  }
252
352
  function metaPath(home, id) {
253
- return path.join(sessionDir(home, id), "meta.json");
353
+ return path3.join(sessionDir(home, id), "meta.json");
354
+ }
355
+ function pidfilePath(home) {
356
+ return path3.join(home, "server.json");
254
357
  }
255
358
  function agentStatePath(home) {
256
- return path.join(home, "agent-state.json");
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
+ }
257
401
  }
258
402
 
259
403
  // packages/server/src/port.ts
260
404
  var DEFAULT_PORT = 4319;
261
- function resolvePort(argv = process.argv.slice(2)) {
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) {
262
414
  const flag = readFlag(argv, "--port");
263
415
  if (flag !== void 0) return parsePort(flag, "--port");
264
416
  const env = process.env.CHAMBA_PORT;
265
417
  if (env !== void 0 && env.length > 0) return parsePort(env, "CHAMBA_PORT");
266
- return DEFAULT_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;
267
427
  }
268
428
  function readFlag(argv, name) {
269
429
  for (let i = 0; i < argv.length; i++) {
@@ -275,38 +435,38 @@ function readFlag(argv, name) {
275
435
  }
276
436
  function parsePort(value, source) {
277
437
  const port = Number(value);
278
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
438
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
279
439
  throw new Error(`invalid ${source}: ${value}`);
280
440
  }
281
441
  return port;
282
442
  }
283
443
 
284
444
  // packages/server/src/version.ts
285
- import { existsSync, readFileSync } from "node:fs";
286
- import path2 from "node:path";
287
- import { fileURLToPath } from "node:url";
445
+ import { existsSync as existsSync4, readFileSync } from "node:fs";
446
+ import path4 from "node:path";
447
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
288
448
  function chambaVersion() {
289
- let dir = path2.dirname(fileURLToPath(import.meta.url));
449
+ let dir = path4.dirname(fileURLToPath3(import.meta.url));
290
450
  for (; ; ) {
291
- const manifest = path2.join(dir, "package.json");
292
- if (existsSync(manifest)) {
451
+ const manifest = path4.join(dir, "package.json");
452
+ if (existsSync4(manifest)) {
293
453
  try {
294
454
  const { version } = JSON.parse(readFileSync(manifest, "utf8"));
295
455
  if (typeof version === "string" && version) return version;
296
456
  } catch {
297
457
  }
298
458
  }
299
- const parent = path2.dirname(dir);
459
+ const parent = path4.dirname(dir);
300
460
  if (parent === dir) return "0.0.0-dev";
301
461
  dir = parent;
302
462
  }
303
463
  }
304
464
 
305
465
  // packages/server/src/sessions/meta.ts
306
- import { readdir, readFile, rename, writeFile } from "node:fs/promises";
466
+ import { readdir, readFile as readFile3, rename, rm as rm2, writeFile } from "node:fs/promises";
307
467
  async function readMeta(home, id) {
308
468
  try {
309
- const raw = await readFile(metaPath(home, id), "utf8");
469
+ const raw = await readFile3(metaPath(home, id), "utf8");
310
470
  return sessionMetaSchema.parse(JSON.parse(raw));
311
471
  } catch (err) {
312
472
  if (err.code === "ENOENT") return null;
@@ -354,8 +514,8 @@ function parseArgs(argv) {
354
514
  }
355
515
 
356
516
  // packages/cli/src/client.ts
357
- import { readFile as readFile2 } from "node:fs/promises";
358
- import path3 from "node:path";
517
+ import { readFile as readFile4 } from "node:fs/promises";
518
+ import path5 from "node:path";
359
519
  var ChannelError = class extends Error {
360
520
  constructor(status2, message) {
361
521
  super(message);
@@ -449,9 +609,9 @@ var Client = class {
449
609
  * bytes into the session directory rather than smuggling them through JSON.
450
610
  */
451
611
  async uploadAttachment(id, filePath) {
452
- const bytes = await readFile2(filePath);
612
+ const bytes = await readFile4(filePath);
453
613
  const form = new FormData();
454
- form.append("file", new Blob([bytes]), path3.basename(filePath));
614
+ form.append("file", new Blob([bytes]), path5.basename(filePath));
455
615
  const res = await this.json(
456
616
  `/api/sessions/${encodeURIComponent(id)}/attachments`,
457
617
  { method: "POST", body: form, timeoutMs: 3e4 },
@@ -494,17 +654,17 @@ var Client = class {
494
654
  );
495
655
  return res.session;
496
656
  }
497
- async json(path9, options, schema) {
498
- const res = await this.fetch(path9, options);
657
+ async json(path12, options, schema) {
658
+ const res = await this.fetch(path12, options);
499
659
  if (!res.ok) throw await toError(res);
500
660
  return schema.parse(await res.json());
501
661
  }
502
- async fetch(path9, options) {
662
+ async fetch(path12, options) {
503
663
  const { method = "GET", body, timeoutMs } = options;
504
664
  const controller = timeoutMs !== void 0 ? new AbortController() : void 0;
505
665
  const timer = controller && timeoutMs !== void 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
506
666
  try {
507
- return await fetch(new URL(path9, this.baseUrl), {
667
+ return await fetch(new URL(path12, this.baseUrl), {
508
668
  method,
509
669
  signal: controller?.signal,
510
670
  ...bodyInit(body)
@@ -530,7 +690,7 @@ async function toError(res) {
530
690
  }
531
691
 
532
692
  // packages/cli/src/output.ts
533
- import path4 from "node:path";
693
+ import path6 from "node:path";
534
694
  import { z as z10 } from "zod";
535
695
  function out(message) {
536
696
  process.stdout.write(`${message}
@@ -554,8 +714,25 @@ function eventAttachments(event) {
554
714
  const parsed = z10.array(attachmentSchema).safeParse(event.payload.attachments);
555
715
  return parsed.success ? parsed.data : [];
556
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
+ }
557
734
  function formatEvent(event) {
558
- if (event.source === "system") return `\xB7 ${eventText(event)}`;
735
+ if (event.source === "system") return formatSystemEvent(event);
559
736
  const who = event.source === "user" ? "you" : "agent";
560
737
  const text = eventText(event);
561
738
  return text ? `${who}: ${text}` : `${who}:`;
@@ -582,7 +759,7 @@ function formatAnnotationBatch(event, home) {
582
759
  lines.push(` component: ${target.component.name}${at}`);
583
760
  }
584
761
  if (item.screenshot) {
585
- const abs = path4.join(sessionDir(home, event.sessionId), item.screenshot.relPath);
762
+ const abs = path6.join(sessionDir(home, event.sessionId), item.screenshot.relPath);
586
763
  lines.push(` [shot] ${item.screenshot.name} -> ${abs}`);
587
764
  }
588
765
  });
@@ -614,7 +791,7 @@ function formatArtifact(event, home) {
614
791
  );
615
792
  if (!parsed.success) return formatEvent(event);
616
793
  const ref = parsed.data;
617
- const abs = path4.join(sessionDir(home, event.sessionId), ref.relPath);
794
+ const abs = path6.join(sessionDir(home, event.sessionId), ref.relPath);
618
795
  return `agent [artifact ${ref.kind} "${ref.name}"] -> ${abs}`;
619
796
  }
620
797
  function printEvents(events, home) {
@@ -637,7 +814,7 @@ function printEvents(events, home) {
637
814
  }
638
815
  out(formatEvent(event));
639
816
  for (const att of eventAttachments(event)) {
640
- out(` [file] ${att.name} -> ${path4.join(sessionDir(home, event.sessionId), att.relPath)}`);
817
+ out(` [file] ${att.name} -> ${path6.join(sessionDir(home, event.sessionId), att.relPath)}`);
641
818
  }
642
819
  }
643
820
  }
@@ -645,15 +822,16 @@ function formatSession(meta, presence, current) {
645
822
  const marker = current ? "*" : " ";
646
823
  const title = meta.title ? ` "${meta.title}"` : "";
647
824
  const live = presence ? ` ${presence}` : "";
648
- return `${marker} ${meta.id} ${meta.status}${live} ${meta.kind}${title}`;
825
+ return `${marker} ${meta.id} ${meta.status}${live} ${meta.kind ?? "unset"}${title}`;
649
826
  }
650
827
 
651
828
  // packages/cli/src/commands/artifact.ts
652
- import { readFile as readFile4 } from "node:fs/promises";
829
+ import { readFile as readFile6 } from "node:fs/promises";
653
830
 
654
831
  // 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";
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";
657
835
  import { z as z11 } from "zod";
658
836
  var agentStateSchema = z11.object({
659
837
  currentSessionId: z11.string().optional(),
@@ -661,7 +839,7 @@ var agentStateSchema = z11.object({
661
839
  });
662
840
  async function readState(home = resolveHome()) {
663
841
  try {
664
- const raw = await readFile3(agentStatePath(home), "utf8");
842
+ const raw = await readFile5(agentStatePath(home), "utf8");
665
843
  return agentStateSchema.parse(JSON.parse(raw));
666
844
  } catch {
667
845
  return { cursors: {} };
@@ -669,11 +847,16 @@ async function readState(home = resolveHome()) {
669
847
  }
670
848
  async function writeState(state, home = resolveHome()) {
671
849
  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)}
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)}
675
854
  `, "utf8");
676
- await rename2(tmp, file);
855
+ await rename2(tmp, file);
856
+ } catch (err) {
857
+ await rm3(tmp, { force: true }).catch(() => void 0);
858
+ throw err;
859
+ }
677
860
  }
678
861
  function cursorFor(state, id) {
679
862
  return state.cursors[id] ?? 0;
@@ -690,7 +873,11 @@ var UsageError = class extends Error {
690
873
  }
691
874
  };
692
875
  async function resolveSessionId(ctx) {
693
- if (ctx.flags.session) return ctx.flags.session;
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;
694
881
  const state = await readState(ctx.home);
695
882
  if (state.currentSessionId) return state.currentSessionId;
696
883
  throw new UsageError("no current session - run `chamba open` or pass --session <id>");
@@ -715,7 +902,7 @@ async function artifact(ctx) {
715
902
  if (!file || file === "true") throw new UsageError("chamba artifact needs --file <path>");
716
903
  let content;
717
904
  try {
718
- content = await readFile4(file, "utf8");
905
+ content = await readFile6(file, "utf8");
719
906
  } catch (err) {
720
907
  throw new UsageError(`could not read ${file}: ${err.message}`);
721
908
  }
@@ -733,8 +920,8 @@ async function artifact(ctx) {
733
920
  }
734
921
 
735
922
  // packages/cli/src/commands/ask.ts
736
- import { randomUUID } from "node:crypto";
737
- import { readFile as readFile5 } from "node:fs/promises";
923
+ import { randomUUID as randomUUID2 } from "node:crypto";
924
+ import { readFile as readFile7 } from "node:fs/promises";
738
925
  async function ask(ctx) {
739
926
  const source = await readCardSource(ctx);
740
927
  let parsed;
@@ -744,7 +931,7 @@ async function ask(ctx) {
744
931
  throw new UsageError(`--json is not valid JSON: ${err.message}`);
745
932
  }
746
933
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && !("cardId" in parsed)) {
747
- parsed.cardId = randomUUID();
934
+ parsed.cardId = randomUUID2();
748
935
  }
749
936
  const result = postQuestionCardReqSchema.safeParse(parsed);
750
937
  if (!result.success) {
@@ -762,7 +949,7 @@ async function readCardSource(ctx) {
762
949
  const file = ctx.positionals[0];
763
950
  if (file) {
764
951
  try {
765
- return await readFile5(file, "utf8");
952
+ return await readFile7(file, "utf8");
766
953
  } catch (err) {
767
954
  throw new UsageError(`could not read card file ${file}: ${err.message}`);
768
955
  }
@@ -774,7 +961,8 @@ async function readCardSource(ctx) {
774
961
 
775
962
  // packages/cli/src/commands/attach.ts
776
963
  async function attach(ctx) {
777
- const id = ctx.positionals[0] ?? ctx.flags.session;
964
+ const flagId = ctx.flags.session === "true" ? void 0 : ctx.flags.session || void 0;
965
+ const id = ctx.positionals[0] ?? flagId;
778
966
  if (!id) throw new UsageError("usage: chamba attach <session-id>");
779
967
  const tail = intFlag(ctx.flags.tail, 20);
780
968
  const digest = await ctx.client.attach(id, tail);
@@ -819,8 +1007,8 @@ async function end(ctx) {
819
1007
  }
820
1008
 
821
1009
  // 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";
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";
824
1012
  async function exportCmd(ctx) {
825
1013
  const dest = ctx.positionals[0];
826
1014
  if (!dest) throw new UsageError("usage: chamba export <dest> [--session <id>]");
@@ -829,12 +1017,12 @@ async function exportCmd(ctx) {
829
1017
  const summary = await assemblePackage({
830
1018
  events,
831
1019
  sessionDir: sessionDir(ctx.home, id),
832
- dest: path6.resolve(dest)
1020
+ dest: path8.resolve(dest)
833
1021
  });
834
1022
  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"})`
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"})`
836
1024
  );
837
- out(path6.resolve(dest));
1025
+ out(path8.resolve(dest));
838
1026
  return 0;
839
1027
  }
840
1028
  async function assemblePackage(input) {
@@ -855,32 +1043,33 @@ async function assemblePackage(input) {
855
1043
  if (parsed.success) decisions.push(parsed.data);
856
1044
  }
857
1045
  }
858
- await mkdir2(input.dest, { recursive: true });
859
- const assetsDir = path6.join(input.dest, "assets");
1046
+ await mkdir3(input.dest, { recursive: true });
1047
+ const assetsDir = path8.join(input.dest, "assets");
1048
+ await rm4(assetsDir, { recursive: true, force: true });
860
1049
  const draftBody = canonicalDraft ? await readArtifact(input.sessionDir, canonicalDraft) : void 0;
861
1050
  await writeFile3(
862
- path6.join(input.dest, "spec.md"),
1051
+ path8.join(input.dest, "spec.md"),
863
1052
  draftBody ?? "# Spec\n\n_No draft was authored in this session._\n",
864
1053
  "utf8"
865
1054
  );
866
1055
  const assets = [];
867
1056
  for (const ref of artifacts.values()) {
868
1057
  if (ref === canonicalDraft) continue;
869
- const src = path6.join(input.sessionDir, ref.relPath);
870
- const base = path6.basename(ref.relPath);
1058
+ const src = path8.join(input.sessionDir, ref.relPath);
1059
+ const base = path8.basename(ref.relPath);
871
1060
  try {
872
- await mkdir2(assetsDir, { recursive: true });
873
- await copyFile(src, path6.join(assetsDir, base));
1061
+ await mkdir3(assetsDir, { recursive: true });
1062
+ await copyFile(src, path8.join(assetsDir, base));
874
1063
  assets.push(base);
875
1064
  } catch {
876
1065
  }
877
1066
  }
878
- await writeFile3(path6.join(input.dest, "decisions.md"), renderDecisions(decisions), "utf8");
1067
+ await writeFile3(path8.join(input.dest, "decisions.md"), renderDecisions(decisions), "utf8");
879
1068
  return { hasDraft: Boolean(draftBody), assets, decisions: decisions.length };
880
1069
  }
881
1070
  async function readArtifact(dir, ref) {
882
1071
  try {
883
- return await readFile6(path6.join(dir, ref.relPath), "utf8");
1072
+ return await readFile8(path8.join(dir, ref.relPath), "utf8");
884
1073
  } catch {
885
1074
  return void 0;
886
1075
  }
@@ -908,22 +1097,31 @@ var USAGE = `chamba - a browser companion for your terminal agent
908
1097
 
909
1098
  Usage:
910
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
911
1103
  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)
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)
916
1112
  chamba reply <text> send a line from the agent into the session
917
1113
  [--file a,b] attach files by path (comma-separated); --image is
918
1114
  [--image a,b] an alias. text is optional when files are attached
919
1115
  chamba attach <id> load a session's recent tail and make it current
920
1116
  [--tail N]
921
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
922
1120
 
923
1121
  Spec sessions (open with --kind spec):
924
1122
  chamba ask --json '{...}' post a question card (question + 2-6 options with
925
1123
  | <file.json> descriptions, optional "recommended"); a cardId is
926
- minted for you. The answer returns as a message
1124
+ created for you. The answer returns as a message
927
1125
  chamba decision record a decision: --status confirmed|assumed
928
1126
  --status S --summary "..." [--detail "..."]
929
1127
  --summary T
@@ -941,45 +1139,133 @@ Spec sessions (open with --kind spec):
941
1139
 
942
1140
  Global:
943
1141
  --session <id> act on a specific session instead of the current one
944
- --port <n> server port (default: CHAMBA_PORT or 4319)`;
1142
+ --port <n> server port (default: CHAMBA_PORT, else a per-project
1143
+ port derived from the workspace location)`;
945
1144
  function help(_ctx) {
946
1145
  note(USAGE);
947
1146
  return Promise.resolve(0);
948
1147
  }
949
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
+
950
1185
  // packages/cli/src/launch.ts
951
- import { spawn } from "node:child_process";
952
- import { existsSync as existsSync2 } from "node:fs";
1186
+ import { spawn, spawnSync } from "node:child_process";
1187
+ import { existsSync as existsSync5 } from "node:fs";
953
1188
  import { createRequire } from "node:module";
954
- import path7 from "node:path";
955
- import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
1189
+ import path10 from "node:path";
1190
+ import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
956
1191
  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);
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);
961
1200
  return { spawned: true };
962
1201
  }
963
- function spawnServer() {
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) {
964
1215
  const child = spawn(process.execPath, serverLaunchArgs(), {
965
1216
  detached: true,
966
1217
  stdio: "ignore",
967
- env: process.env
1218
+ env: { ...process.env, CHAMBA_DIR: target.home, CHAMBA_PORT: String(target.port) }
968
1219
  });
969
1220
  child.unref();
970
1221
  }
971
1222
  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];
1223
+ const here = path10.dirname(fileURLToPath4(import.meta.url));
1224
+ const packagedServer = path10.join(here, "server.js");
1225
+ if (existsSync5(packagedServer)) return [packagedServer];
975
1226
  const serverBin = require2.resolve("@chamba/server/bin");
976
1227
  const tsxLoader = pathToFileURL(require2.resolve("tsx")).href;
977
1228
  return ["--import", tsxLoader, serverBin];
978
1229
  }
979
- async function waitUntilUp(client, timeoutMs = 1e4) {
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) {
980
1260
  const deadline = Date.now() + timeoutMs;
981
1261
  while (Date.now() < deadline) {
982
- if (await client.isUp()) return;
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
+ }
983
1269
  await delay(150);
984
1270
  }
985
1271
  throw new Error("server did not become healthy in time");
@@ -989,9 +1275,19 @@ function delay(ms) {
989
1275
  }
990
1276
 
991
1277
  // 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");
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}`);
995
1291
  const req = {
996
1292
  ...ctx.flags.kind ? { kind: parseKind(ctx.flags.kind) } : {},
997
1293
  ...ctx.flags.title ? { title: ctx.flags.title } : {}
@@ -1002,7 +1298,17 @@ async function open(ctx) {
1002
1298
  const state = await readState(ctx.home);
1003
1299
  await writeState(withSession(state, id, digest.cursor), ctx.home);
1004
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`);
1005
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
+ }
1006
1312
  return 0;
1007
1313
  }
1008
1314
  function parseKind(value) {
@@ -1012,9 +1318,45 @@ function parseKind(value) {
1012
1318
  }
1013
1319
 
1014
1320
  // packages/cli/src/commands/poll.ts
1015
- async function poll(ctx) {
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) {
1016
1332
  const id = await resolveSessionId(ctx);
1017
- const wait = Math.min(intFlag(ctx.flags.wait, 25e3), 3e4);
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) {
1018
1360
  const state = await readState(ctx.home);
1019
1361
  const since = cursorFor(state, id);
1020
1362
  const res = await ctx.client.poll(id, since, wait);
@@ -1026,8 +1368,53 @@ async function poll(ctx) {
1026
1368
  await writeState(withSession(state, id, res.cursor), ctx.home);
1027
1369
  return 0;
1028
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
+ }
1029
1415
 
1030
1416
  // packages/cli/src/commands/reply.ts
1417
+ import { stat } from "node:fs/promises";
1031
1418
  async function reply(ctx) {
1032
1419
  const text = ctx.positionals.join(" ").trim();
1033
1420
  const files = filePaths(ctx.flags);
@@ -1035,6 +1422,15 @@ async function reply(ctx) {
1035
1422
  throw new UsageError("usage: chamba reply <text> [--file a,b] [--image a,b]");
1036
1423
  }
1037
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
+ }
1038
1434
  const attachmentIds = [];
1039
1435
  for (const file of files) {
1040
1436
  const attachment = await ctx.client.uploadAttachment(id, file);
@@ -1053,10 +1449,10 @@ function filePaths(flags) {
1053
1449
  }
1054
1450
 
1055
1451
  // packages/cli/src/commands/skill.ts
1056
- import { cpSync, existsSync as existsSync3 } from "node:fs";
1452
+ import { cpSync, existsSync as existsSync6, rmSync } from "node:fs";
1057
1453
  import { homedir } from "node:os";
1058
- import path8 from "node:path";
1059
- import { fileURLToPath as fileURLToPath3 } from "node:url";
1454
+ import path11 from "node:path";
1455
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
1060
1456
  async function skill(ctx) {
1061
1457
  if (ctx.positionals[0] !== "install") {
1062
1458
  throw new UsageError("usage: chamba skill install [--global]");
@@ -1067,28 +1463,36 @@ async function skill(ctx) {
1067
1463
  return 1;
1068
1464
  }
1069
1465
  const base = ctx.flags.global ? homedir() : process.cwd();
1070
- const target = path8.join(base, ".claude", "skills", "chamba");
1466
+ const target = path11.join(base, ".claude", "skills", "chamba");
1467
+ rmSync(target, { recursive: true, force: true });
1071
1468
  cpSync(source, target, { recursive: true });
1072
1469
  note(`installed the chamba skill to ${target}`);
1073
1470
  note("reload your agent's skills, then use /chamba");
1074
1471
  return 0;
1075
1472
  }
1076
1473
  function resolveSkillSource() {
1077
- const here = path8.dirname(fileURLToPath3(import.meta.url));
1474
+ const here = path11.dirname(fileURLToPath5(import.meta.url));
1078
1475
  const candidates = [
1079
- path8.resolve(here, "../skill"),
1476
+ path11.resolve(here, "../skill"),
1080
1477
  // packaged: dist/cli.js -> <pkg>/skill
1081
- path8.resolve(here, "../../../../skill")
1478
+ path11.resolve(here, "../../../../skill")
1082
1479
  // dev: packages/cli/src/commands -> <repo>/skill
1083
1480
  ];
1084
- return candidates.find((dir) => existsSync3(path8.join(dir, "SKILL.md"))) ?? null;
1481
+ return candidates.find((dir) => existsSync6(path11.join(dir, "SKILL.md"))) ?? null;
1085
1482
  }
1086
1483
 
1087
1484
  // packages/cli/src/commands/status.ts
1088
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
+ }
1089
1492
  const state = await readState(ctx.home);
1090
- const up = await ctx.client.isUp();
1493
+ const up = await serverOwnsHome(ctx);
1091
1494
  const rows = up ? await rowsFromServer(ctx) : await rowsFromDisk(ctx);
1495
+ note(`home: ${ctx.home}`);
1092
1496
  if (rows.length === 0) {
1093
1497
  note("no sessions yet - run `chamba open` to start one");
1094
1498
  return 0;
@@ -1099,6 +1503,14 @@ async function status(ctx) {
1099
1503
  }
1100
1504
  return 0;
1101
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
+ }
1102
1514
  async function rowsFromServer(ctx) {
1103
1515
  const summaries = await ctx.client.list();
1104
1516
  return summaries.map((s) => ({ meta: s.meta, presence: s.presence }));
@@ -1109,14 +1521,61 @@ async function rowsFromDisk(ctx) {
1109
1521
  return metas.filter((meta) => meta !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map((meta) => ({ meta }));
1110
1522
  }
1111
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
+
1112
1569
  // packages/cli/src/commands/index.ts
1113
1570
  var COMMANDS = {
1114
- open,
1571
+ init,
1572
+ open: open2,
1115
1573
  poll,
1116
1574
  reply,
1117
1575
  end,
1118
1576
  attach,
1119
1577
  status,
1578
+ stop,
1120
1579
  // Spec sessions: structured interview + live artifacts + export.
1121
1580
  ask,
1122
1581
  decision,
@@ -1137,21 +1596,40 @@ function runCommand(name, ctx) {
1137
1596
  }
1138
1597
 
1139
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
+ ]);
1140
1609
  async function main(argv = process.argv.slice(2)) {
1141
- const port = resolvePort(argv);
1142
1610
  const { positionals, flags } = parseArgs(argv);
1143
1611
  if (flags.version && positionals.length === 0) {
1144
1612
  out(chambaVersion());
1145
1613
  return 0;
1146
1614
  }
1147
1615
  const command = positionals[0] ?? (flags.help ? "help" : "status");
1616
+ const existing = findExistingHome();
1617
+ const home = existing ?? plannedHome();
1618
+ const port = resolvePort(argv, home);
1148
1619
  const ctx = {
1149
1620
  client: new Client(`http://127.0.0.1:${port}/`),
1150
- home: resolveHome(),
1621
+ home,
1151
1622
  port,
1152
1623
  flags,
1153
- positionals: positionals.slice(1)
1624
+ positionals: positionals.slice(1),
1625
+ workspaceExists: existing !== null
1154
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
+ }
1155
1633
  try {
1156
1634
  return await runCommand(command, ctx);
1157
1635
  } catch (err) {
@@ -1167,16 +1645,39 @@ function reportError(err) {
1167
1645
  note(`error: ${err.message}`);
1168
1646
  return 1;
1169
1647
  }
1170
- const cause = err.cause;
1171
- if (cause?.code === "ECONNREFUSED") {
1648
+ if (errorCode(err) === "ECONNREFUSED") {
1172
1649
  note("cannot reach the chamba server - run `chamba open` first");
1173
1650
  return 1;
1174
1651
  }
1175
1652
  note(`error: ${err.message}`);
1176
1653
  return 1;
1177
1654
  }
1178
- var invokedDirectly = process.argv[1] !== void 0 && pathToFileURL2(process.argv[1]).href === import.meta.url;
1179
- if (invokedDirectly) {
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()) {
1180
1681
  main().then(
1181
1682
  (code) => process.exit(code),
1182
1683
  (err) => {