khala-cli 0.4.0 → 0.4.2

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.
@@ -4,17 +4,17 @@ import {
4
4
  MODEL_NAMES,
5
5
  checkName,
6
6
  isDefaultAgentName
7
- } from "./chunk-AAX35R7C.mjs";
7
+ } from "./chunk-BZ53MZEE.mjs";
8
8
  import {
9
9
  StateError,
10
10
  readJson,
11
11
  stateRoot,
12
12
  writeJsonAtomic
13
- } from "./chunk-AYPFL33Q.mjs";
13
+ } from "./chunk-B5BFL7OD.mjs";
14
14
  import {
15
15
  HARNESSES,
16
16
  validateAgentName
17
- } from "./chunk-SMCTSZYU.mjs";
17
+ } from "./chunk-N6LMHR7P.mjs";
18
18
 
19
19
  // src/local/identity.ts
20
20
  import * as fs from "node:fs/promises";
@@ -143,9 +143,17 @@ async function requestJoin(input, deps = {}) {
143
143
  const label = validateAgentName(input.label);
144
144
  if (!label.ok || [...label.name].length > 40) fail("invalid_label");
145
145
  const { origin } = parsed;
146
- const response = await post(new URL("/api/agent/join", origin), origin, { link: input.link, harness: input.harness, label: label.name }, deps);
147
- const body = await json(response);
148
- const code = serverCode(body);
146
+ const url = new URL("/api/agent/join", origin);
147
+ const base = { link: input.link, harness: input.harness, label: label.name };
148
+ const rejoin = input.sessionId !== void 0 && input.rejoinSecret !== void 0;
149
+ let response = await post(url, origin, rejoin ? { ...base, sessionId: input.sessionId, rejoinSecret: input.rejoinSecret } : base, deps);
150
+ let body = await json(response);
151
+ let code = serverCode(body);
152
+ if (rejoin && response.status === 400 && code === "invalid_link") {
153
+ response = await post(url, origin, base, deps);
154
+ body = await json(response);
155
+ code = serverCode(body);
156
+ }
149
157
  if (response.status !== 201) {
150
158
  if (response.status === 400 && code === "invalid_link") throw new KhalaClientError("invalid_link", "invalid_link");
151
159
  if (response.status === 400 && (code === "invalid_label" || code === "invalid_harness")) fail(code);
@@ -1,4 +1,16 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
+ import {
3
+ ensureHelper,
4
+ helperPaths,
5
+ readHelperFile
6
+ } from "./chunk-YP45XTOH.mjs";
7
+ import {
8
+ parseChannelLink,
9
+ resolveLocalOwnerName
10
+ } from "./chunk-2OO725IZ.mjs";
11
+ import {
12
+ KHALA_AGENT_VERSION
13
+ } from "./chunk-MHZWR4A3.mjs";
2
14
  import {
3
15
  LOCAL_AGENT_DEVICE_PREFIX,
4
16
  LOCAL_IDLE_EXIT_MS,
@@ -9,39 +21,28 @@ import {
9
21
  LOCAL_OWNER_USER_ID,
10
22
  LOCAL_TOKEN_BYTES,
11
23
  base64url,
24
+ checkName,
12
25
  decodeChannelSecrets,
13
26
  decodeLocalEvent,
14
27
  decodeOwnerProfile,
15
- ensureHelper,
16
- helperPaths,
28
+ defaultAgentName,
29
+ defaultHumanColor,
30
+ isDefaultAgentName,
31
+ isHumanColorId,
17
32
  isLocalRoomId,
18
33
  localRoomKey,
19
34
  newLocalAgentUserId,
20
35
  newLocalEventId,
21
36
  newLocalRoomId,
22
- readHelperFile
23
- } from "./chunk-3WTRQEVS.mjs";
24
- import {
25
- parseChannelLink,
26
- resolveLocalOwnerName
27
- } from "./chunk-THNGKHZD.mjs";
28
- import {
29
- KHALA_AGENT_VERSION
30
- } from "./chunk-ENNXMNKZ.mjs";
31
- import {
32
- checkName,
33
- defaultAgentName,
34
- defaultHumanColor,
35
- isDefaultAgentName,
36
- isHumanColorId,
37
37
  normalizeInitials
38
- } from "./chunk-AAX35R7C.mjs";
38
+ } from "./chunk-BZ53MZEE.mjs";
39
39
  import {
40
40
  StateError,
41
41
  ensureStateDir,
42
42
  readJson,
43
43
  writeJsonAtomic
44
- } from "./chunk-AYPFL33Q.mjs";
44
+ } from "./chunk-B5BFL7OD.mjs";
45
+ import "./chunk-5GLTKWJ5.mjs";
45
46
  import {
46
47
  CHANNEL_EVENT_TYPE,
47
48
  DEFAULT_LISTENING_MODE,
@@ -55,13 +56,16 @@ import {
55
56
  decodeWith,
56
57
  encodeChannelEvent,
57
58
  memberListeningMode,
59
+ memberRenameContent,
58
60
  object,
59
- utf8Length
60
- } from "./chunk-SMCTSZYU.mjs";
61
+ utf8Length,
62
+ validAgentRejoinSecret,
63
+ validAgentSessionId
64
+ } from "./chunk-N6LMHR7P.mjs";
61
65
  import {
62
66
  bundle
63
- } from "./chunk-NX27G3SB.mjs";
64
- import "./chunk-ZXJIFFFU.mjs";
67
+ } from "./chunk-RW3DXWNC.mjs";
68
+ import "./chunk-A3LJU4JA.mjs";
65
69
 
66
70
  // src/local/cli.ts
67
71
  import { setTimeout as sleepDefault } from "node:timers/promises";
@@ -112,6 +116,7 @@ async function openLocalStore(input) {
112
116
  byEventId: /* @__PURE__ */ new Map(),
113
117
  byTxn: /* @__PURE__ */ new Map(),
114
118
  lastMember: /* @__PURE__ */ new Map(),
119
+ previousMembers: /* @__PURE__ */ new Map(),
115
120
  secrets: emptySecrets(),
116
121
  waiters: /* @__PURE__ */ new Set()
117
122
  };
@@ -126,6 +131,8 @@ async function openLocalStore(input) {
126
131
  } else if (event.type === "m.room.name") channel.name = event.content.name;
127
132
  else if (event.type === "m.room.member") {
128
133
  const user = event.content.user;
134
+ const previous = channel.lastMember.get(user)?.event.content;
135
+ if (previous) channel.previousMembers.set(event.eventId, previous);
129
136
  const firstSeq = channel.lastMember.get(user)?.firstSeq ?? event.seq;
130
137
  channel.lastMember.set(user, { event, firstSeq });
131
138
  }
@@ -380,7 +387,11 @@ async function openLocalStore(input) {
380
387
  if (events[mid].seq <= after) lo = mid + 1;
381
388
  else hi = mid;
382
389
  }
383
- return structuredClone(events.slice(lo, lo + Math.max(0, limit)));
390
+ const channel = channels.get(roomId);
391
+ return structuredClone(events.slice(lo, lo + Math.max(0, limit)).map((event) => {
392
+ const previousContent = channel?.previousMembers.get(event.eventId);
393
+ return previousContent ? { ...event, previousContent } : event;
394
+ }));
384
395
  },
385
396
  waitForEvent: (roomId, after, timeoutMs, signal) => {
386
397
  const channel = channels.get(roomId);
@@ -390,7 +401,17 @@ async function openLocalStore(input) {
390
401
  const channel = channels.get(roomId);
391
402
  const cutoff = before !== void 0 ? channel?.byEventId.get(before)?.seq : Infinity;
392
403
  if (!channel || cutoff === void 0 || limit <= 0) return { events: [] };
393
- const candidates = channel.events.filter((e) => e.seq < cutoff && (e.type === "m.room.message" || e.type === "com.khala.event.v1"));
404
+ const previousMembers = /* @__PURE__ */ new Map();
405
+ const candidates = [];
406
+ for (const event of channel.events) {
407
+ if (event.seq >= cutoff) break;
408
+ if (event.type === "m.room.member") {
409
+ const user = event.content.user;
410
+ const content = memberRenameContent(event.content, previousMembers.get(user));
411
+ previousMembers.set(user, event.content);
412
+ if (content) candidates.push({ ...event, type: "com.khala.event.v1", content });
413
+ } else if (event.type === "m.room.message" || event.type === "com.khala.event.v1") candidates.push(event);
414
+ }
394
415
  const events = candidates.slice(-limit);
395
416
  return { events: structuredClone(events), ...candidates.length > events.length ? { nextBefore: events[0].eventId } : {} };
396
417
  },
@@ -422,11 +443,20 @@ async function openLocalStore(input) {
422
443
  }
423
444
  return null;
424
445
  }),
425
- setMemberToken: (roomId, userId, tokenSha256) => enqueue(async () => {
446
+ memberForSession: (roomId, sessionKey) => {
447
+ const channel = channels.get(roomId);
448
+ if (!channel) return void 0;
449
+ const userId = Object.entries(channel.secrets.members).find(([, secret]) => secret.sessionKey === sessionKey)?.[0];
450
+ return userId ? member(channel, userId) : void 0;
451
+ },
452
+ setMemberToken: (roomId, userId, tokenSha256, sessionKey) => enqueue(async () => {
426
453
  const channel = requireChannel(roomId);
427
454
  const next = { ...channel.secrets.members };
428
455
  if (tokenSha256 === null) delete next[userId];
429
- else if (userId !== LOCAL_OWNER_USER_ID) next[userId] = { tokenSha256 };
456
+ else if (userId !== LOCAL_OWNER_USER_ID) {
457
+ const identity = sessionKey ?? channel.secrets.members[userId]?.sessionKey;
458
+ next[userId] = { tokenSha256, ...identity ? { sessionKey: identity } : {} };
459
+ }
430
460
  await saveSecrets(channel, { ...channel.secrets, members: next });
431
461
  }),
432
462
  agentForToken: (token) => {
@@ -779,8 +809,10 @@ function agentJoinRoutes() {
779
809
  prune(ctx);
780
810
  if (typeof req.body !== "object" || req.body === null || Array.isArray(req.body)) return fail(400, "invalid_link");
781
811
  const body = req.body;
782
- if (!Object.hasOwn(body, "link") || Object.keys(body).some((key) => !["link", "harness", "label"].includes(key))) return fail(400, "invalid_link");
812
+ if (!Object.hasOwn(body, "link") || Object.keys(body).some((key) => !["link", "harness", "label", "sessionId", "rejoinSecret"].includes(key))) return fail(400, "invalid_link");
783
813
  if (typeof body.link !== "string" || !parseChannelLink(body.link)) return fail(400, "invalid_link");
814
+ if (Object.hasOwn(body, "sessionId") && !validAgentSessionId(body.sessionId)) return fail(400, "invalid_link");
815
+ if (Object.hasOwn(body, "rejoinSecret") && !validAgentRejoinSecret(body.rejoinSecret)) return fail(400, "invalid_link");
784
816
  const harness = body.harness;
785
817
  if (!HARNESSES.includes(harness)) return fail(400, "invalid_harness");
786
818
  const url = new URL(body.link);
@@ -793,22 +825,32 @@ function agentJoinRoutes() {
793
825
  const link = await ctx.store.consumeLink(token);
794
826
  if (!link || !ctx.store.hasChannel(link.roomId)) return fail(404, "link_unavailable");
795
827
  const roomId = link.roomId;
828
+ const sessionKey = body.sessionId === void 0 || body.rejoinSecret === void 0 ? void 0 : sha256hex(JSON.stringify([harness, body.sessionId, body.rejoinSecret]));
829
+ const previous = sessionKey ? ctx.store.memberForSession(roomId, sessionKey) : void 0;
796
830
  const taken = new Set(ctx.store.members(roomId).map((member) => member.displayName.toLowerCase()));
797
831
  let n = 1;
798
832
  while (taken.has(defaultAgentName(username, harness, n).toLowerCase())) n++;
799
833
  const checked = checkName(defaultAgentName(username, harness, n), "agent");
800
834
  if (!checked.ok) return fail(503, "unavailable");
801
- let userId = newLocalAgentUserId(ctx.random(4));
802
- for (let attempt = 0; attempt < 5 && ctx.store.channelOfMember(userId) !== void 0; attempt++) userId = newLocalAgentUserId(ctx.random(4));
803
- if (ctx.store.channelOfMember(userId) !== void 0) return fail(503, "unavailable");
835
+ let userId = previous?.userId ?? newLocalAgentUserId(ctx.random(4));
836
+ for (let attempt = 0; attempt < 5 && !previous && ctx.store.channelOfMember(userId) !== void 0; attempt++) userId = newLocalAgentUserId(ctx.random(4));
837
+ if (!previous && ctx.store.channelOfMember(userId) !== void 0) return fail(503, "unavailable");
804
838
  const deviceId = "KH_LOCAL_" + userId.slice("@agent-".length, -":local".length);
805
839
  const accessToken = b64(ctx.random(LOCAL_TOKEN_BYTES));
806
840
  await ctx.store.append(roomId, {
807
841
  type: "m.room.member",
808
842
  sender: LOCAL_OWNER_USER_ID,
809
- content: { user: userId, membership: "invite", displayname: checked.name, kind: "agent", harness, invitedBy: LOCAL_OWNER_USER_ID }
843
+ content: {
844
+ user: userId,
845
+ membership: "invite",
846
+ displayname: previous?.displayName ?? checked.name,
847
+ kind: "agent",
848
+ harness,
849
+ invitedBy: LOCAL_OWNER_USER_ID,
850
+ ...previous ? { "com.khala.listening_mode": previous.listeningMode ?? "sync" } : {}
851
+ }
810
852
  });
811
- await ctx.store.setMemberToken(roomId, userId, sha256hex(accessToken));
853
+ await ctx.store.setMemberToken(roomId, userId, sha256hex(accessToken), sessionKey);
812
854
  const joinId = b64(ctx.random(16)), pollSecret = b64(ctx.random(LOCAL_TOKEN_BYTES));
813
855
  const expiresAt = ctx.now() + LOCAL_LINK_TTL_MS;
814
856
  const credentials = { homeserver: ctx.origin, userId, accessToken, deviceId, roomId, transport: "local" };
@@ -887,6 +929,7 @@ function memberEvents(store, roomId, userId) {
887
929
  let current;
888
930
  let joined;
889
931
  let announced = false;
932
+ let joins = 0;
890
933
  for (; ; ) {
891
934
  const page = store.eventsAfter(roomId, after, 200);
892
935
  for (const event of page) {
@@ -897,13 +940,14 @@ function memberEvents(store, roomId, userId) {
897
940
  } else if (current?.content["membership"] !== "join") {
898
941
  joined = event;
899
942
  announced = false;
943
+ joins++;
900
944
  }
901
945
  current = event;
902
946
  } else if (joined && event.type === "com.khala.event.v1" && event.sender === LOCAL_OWNER_USER_ID && event.txnId === announcementTxn(joined)) {
903
947
  announced = true;
904
948
  }
905
949
  }
906
- if (page.length < 200) return { current, joined, announced };
950
+ if (page.length < 200) return { current, joined, announced, joins };
907
951
  after = page.at(-1).seq;
908
952
  }
909
953
  }
@@ -952,14 +996,18 @@ function roomRoutes() {
952
996
  let event;
953
997
  let joined;
954
998
  let announced = false;
999
+ let rejoin = false;
955
1000
  if (caller.member.membership === "join") {
956
- ({ current: event, joined, announced } = memberEvents(ctx.store, caller.roomId, caller.userId));
1001
+ let joins;
1002
+ ({ current: event, joined, announced, joins } = memberEvents(ctx.store, caller.roomId, caller.userId));
1003
+ rejoin = joins > 1;
957
1004
  } else {
1005
+ rejoin = memberEvents(ctx.store, caller.roomId, caller.userId).joins > 0;
958
1006
  event = await ctx.store.append(caller.roomId, { type: "m.room.member", sender: caller.userId, content: memberContent(caller.member) });
959
1007
  joined = event;
960
1008
  }
961
1009
  if (caller.kind === "agent" && joined && !announced) {
962
- const encoded = encodeChannelEvent({ kind: "member", summary: `${joined.content["displayname"]} joined`, status: "info", source: { system: "khala-local" } });
1010
+ const encoded = encodeChannelEvent({ kind: "member", summary: `${joined.content["displayname"]} ${rejoin ? "rejoined" : "joined"}`, status: "info", source: { system: "khala-local" } });
963
1011
  if (encoded.ok) await ctx.store.append(caller.roomId, {
964
1012
  type: "com.khala.event.v1",
965
1013
  sender: LOCAL_OWNER_USER_ID,
@@ -985,7 +1033,12 @@ function roomRoutes() {
985
1033
  if ("status" in current) return current;
986
1034
  events = ctx.store.eventsAfter(caller.roomId, after, 200);
987
1035
  }
988
- return { status: 200, json: { events, next: events.at(-1)?.seq ?? after } };
1036
+ const delivered = req.query.get("prev") === "1" ? events : events.map((event) => {
1037
+ const legacy = { ...event };
1038
+ delete legacy.previousContent;
1039
+ return legacy;
1040
+ });
1041
+ return { status: 200, json: { events: delivered, next: events.at(-1)?.seq ?? after } };
989
1042
  }),
990
1043
  route("GET", /^\/api\/local\/rooms\/([^/]+)\/messages$/u, "joined", async (req, _params, ctx, caller) => {
991
1044
  const before = req.query.get("before");
@@ -1232,14 +1285,14 @@ async function cascade(store, previous, next) {
1232
1285
  const members = store.members(roomId);
1233
1286
  const taken = new Set(members.map((m) => m.displayName.toLowerCase()));
1234
1287
  const owner2 = members.find((m) => m.userId === LOCAL_OWNER_USER_ID);
1235
- if (owner2 && owner2.displayName !== next) {
1288
+ if (owner2 && previous !== next) {
1236
1289
  try {
1237
1290
  await store.append(roomId, {
1238
1291
  type: "m.room.member",
1239
1292
  sender: LOCAL_OWNER_USER_ID,
1240
1293
  content: { user: LOCAL_OWNER_USER_ID, membership: owner2.membership, displayname: next, kind: "human" }
1241
1294
  });
1242
- taken.delete(owner2.displayName.toLowerCase());
1295
+ taken.delete(previous.toLowerCase());
1243
1296
  taken.add(next.toLowerCase());
1244
1297
  } catch {
1245
1298
  }
@@ -0,0 +1 @@
1
+ import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
@@ -4,8 +4,8 @@ import {
4
4
  } from "./chunk-VUE2DDYY.mjs";
5
5
  import {
6
6
  bundle
7
- } from "./chunk-NX27G3SB.mjs";
8
- import "./chunk-ZXJIFFFU.mjs";
7
+ } from "./chunk-RW3DXWNC.mjs";
8
+ import "./chunk-A3LJU4JA.mjs";
9
9
 
10
10
  // src/install/main.ts
11
11
  import { spawnSync } from "node:child_process";
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
 
3
3
  // <define:__KHALA_BUNDLE__>
4
- var define_KHALA_BUNDLE_default = { name: "khala-cli", version: "0.4.0" };
4
+ var define_KHALA_BUNDLE_default = { name: "khala-cli", version: "0.4.2" };
5
5
 
6
6
  export {
7
7
  define_KHALA_BUNDLE_default
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
3
  HARNESSES
4
- } from "./chunk-SMCTSZYU.mjs";
4
+ } from "./chunk-N6LMHR7P.mjs";
5
5
 
6
6
  // src/state.ts
7
7
  import { constants } from "node:fs";
@@ -105,7 +105,7 @@ function readStatus(files) {
105
105
  return readJson(files.status);
106
106
  }
107
107
  async function writeStateFile(dir, name, value) {
108
- if (!["join.json", "session.json", "cursor.json", "status.json", "mode.json"].includes(name)) throw new StateError("storage_failed");
108
+ if (!["join.json", "session.json", "cursor.json", "status.json", "mode.json", "rejoin.json"].includes(name)) throw new StateError("storage_failed");
109
109
  await writeJsonAtomic(path.join(dir, name), value);
110
110
  }
111
111
  function readStateFile(dir, name) {