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