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.
- package/README.md +17 -1
- package/dist/cli.js +1190 -0
- package/dist/server.js +1675 -0
- package/inject/annotate.js +49 -0
- package/package.json +24 -3
- package/skill/README.md +12 -0
- package/skill/SKILL.md +213 -0
- package/web/assets/abnfDiagram-VRR7QNED-aw1_rWFY.js +1 -0
- package/web/assets/arc-BMhSK0Sy.js +1 -0
- package/web/assets/architectureDiagram-ZJ3FMSHR-BZDcNKY3.js +36 -0
- package/web/assets/blockDiagram-677ZJIJ3-CLVR_gtH.js +132 -0
- package/web/assets/c4Diagram-LMCZKHZV-YjWWkNOJ.js +10 -0
- package/web/assets/channel-8ymR7Shm.js +1 -0
- package/web/assets/chunk-2Q5K7J3B-Bd9qqRl0.js +1 -0
- package/web/assets/chunk-32BRIVSS-Cq9EhooX.js +1 -0
- package/web/assets/chunk-5VM5RSS4-DYxyelz5.js +15 -0
- package/web/assets/chunk-EX3LRPZG-COaiqIlv.js +231 -0
- package/web/assets/chunk-JWPE2WC7-CMWXKnkF.js +1 -0
- package/web/assets/chunk-MOJQB5TN-BRzAoPL7.js +88 -0
- package/web/assets/chunk-RYQCIY6F-BRght6pl.js +1 -0
- package/web/assets/chunk-V7JOEXUC-BlbCYGxG.js +206 -0
- package/web/assets/chunk-VR4S4FIN-CPoKxaoM.js +1 -0
- package/web/assets/chunk-XXDRQBXY-DeNDfu5r.js +1 -0
- package/web/assets/classDiagram-OUVF2IWQ-g3gy3kDO.js +1 -0
- package/web/assets/classDiagram-v2-EOCWNBFH-g3gy3kDO.js +1 -0
- package/web/assets/cose-bilkent-JH36ORCC-C_V1sCw0.js +1 -0
- package/web/assets/cynefin-VYW2F7L2-CtWqChj_.js +178 -0
- package/web/assets/cynefinDiagram-TSTJHNR4-Q0g7rhQg.js +62 -0
- package/web/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/web/assets/dagre-VKFMJZFB-spMt8V5o.js +4 -0
- package/web/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/assets/diagram-FQU43EPY-CF7kRW9U.js +3 -0
- package/web/assets/diagram-G47NLZAW-B-5ygaqt.js +24 -0
- package/web/assets/diagram-NH7WQ7WH-CdOGPV2-.js +24 -0
- package/web/assets/diagram-OA4YK3LP-D1_1pb2t.js +30 -0
- package/web/assets/diagram-WEI45ONY-gD_xjCgB.js +41 -0
- package/web/assets/ebnfDiagram-CCIWWBDH-aW4E-V1v.js +1 -0
- package/web/assets/erDiagram-Q63AITRT-BWndxFVg.js +85 -0
- package/web/assets/flowDiagram-23GEKE2U-CVc37qLo.js +156 -0
- package/web/assets/ganttDiagram-NO4QXBWP-DV-2BraZ.js +292 -0
- package/web/assets/gitGraphDiagram-IHSO6WYX-YCy7dADs.js +106 -0
- package/web/assets/graph-C9eacEi8.js +1 -0
- package/web/assets/highlighted-body-OFNGDK62-q_mVDG84.js +1 -0
- package/web/assets/index-BG6acTBk.css +1 -0
- package/web/assets/index-CBBSnvKL.js +335 -0
- package/web/assets/infoDiagram-FWYZ7A6U-ig4wRILz.js +2 -0
- package/web/assets/init-Gi6I4Gst.js +1 -0
- package/web/assets/ishikawaDiagram-FXEZZL3T-BE8nJNWr.js +70 -0
- package/web/assets/journeyDiagram-5HDEW3XC-_olL4LFF.js +139 -0
- package/web/assets/kanban-definition-HUTT4EX6-DTFmbw9d.js +89 -0
- package/web/assets/katex-C5jXJg4s.js +257 -0
- package/web/assets/layout-DEXfKzaS.js +1 -0
- package/web/assets/linear-CbmtQHdI.js +1 -0
- package/web/assets/map-Czzmt4hB.js +1 -0
- package/web/assets/mermaid.core-CyLpSBwh.js +314 -0
- package/web/assets/mindmap-definition-LN4V7U3C-BFXQ_blT.js +96 -0
- package/web/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/assets/pegDiagram-2B236MQR-BzFkomCY.js +1 -0
- package/web/assets/pieDiagram-ENE6RG2P-r6gWlwuV.js +39 -0
- package/web/assets/quadrantDiagram-ABIIQ3AL-DTFxtS0e.js +7 -0
- package/web/assets/railroadDiagram-RFXS5EU6-CbZAYLVe.js +1 -0
- package/web/assets/requirementDiagram-TGXJPOKE-DzGi5GvN.js +84 -0
- package/web/assets/sankeyDiagram-HTMAVEWB-CBWOwAUD.js +40 -0
- package/web/assets/sequenceDiagram-DBY2YBRQ-QX9rQ9ow.js +162 -0
- package/web/assets/sizeCapture-X5ZJPWSS-X8SU5JUW.js +1 -0
- package/web/assets/stateDiagram-2N3HPSRC-kQgWzKK8.js +1 -0
- package/web/assets/stateDiagram-v2-6OUMAXLB-2kXWc9Fv.js +1 -0
- package/web/assets/swimlanes-5IMT3BWC-eKT1_uaa.js +2 -0
- package/web/assets/swimlanesDiagram-G3AALYLV-B5i-k7ui.js +8 -0
- package/web/assets/timeline-definition-FHXFAJF6-BRKXt19C.js +120 -0
- package/web/assets/vennDiagram-L72KCM5P-C-mbeAXb.js +34 -0
- package/web/assets/wardleyDiagram-EHGQE667-Doojx4Uh.js +78 -0
- package/web/assets/xychartDiagram-FW5EYKEG-CQZh8Trp.js +7 -0
- 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
|
+
};
|