@ccmsg/cli 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/package.json +2 -2
  2. package/src/auth/auth.ts +14 -14
  3. package/src/auth/http.ts +6 -6
  4. package/src/auth/records.ts +1 -1
  5. package/src/auth/topic.ts +1 -1
  6. package/src/cli.ts +21 -24
  7. package/src/daemon/control.ts +2 -2
  8. package/src/daemon/registry.ts +7 -7
  9. package/src/daemon/snapshot.ts +12 -10
  10. package/src/daemon/supervise.ts +1 -1
  11. package/src/files/containment.ts +5 -5
  12. package/src/files/files.ts +12 -12
  13. package/src/files/sandbox.ts +0 -0
  14. package/src/greeting/meta.ts +5 -2
  15. package/src/instance/config.ts +21 -9
  16. package/src/instance/instance.ts +15 -13
  17. package/src/kv/store.ts +4 -3
  18. package/src/launcher/launcher.ts +3 -3
  19. package/src/mesh/mesh.ts +7 -8
  20. package/src/mesh/relay.ts +1 -1
  21. package/src/messaging/delivery.ts +4 -4
  22. package/src/messaging/direct.ts +2 -2
  23. package/src/messaging/handlers.ts +4 -4
  24. package/src/messaging/inbox.ts +1 -1
  25. package/src/messaging/notify.ts +11 -11
  26. package/src/sessions/dump.ts +1 -1
  27. package/src/sessions/handlers.ts +17 -17
  28. package/src/sessions/harness.ts +1 -1
  29. package/src/sessions/last-live.ts +1 -1
  30. package/src/sessions/registry.ts +60 -77
  31. package/src/sessions/search.ts +1 -1
  32. package/src/sessions/status.ts +7 -7
  33. package/src/topics/handlers.ts +2 -2
  34. package/src/topics/topics.ts +0 -0
  35. package/src/transcript/items/classify.ts +42 -37
  36. package/src/transcript/items/document.ts +2 -2
  37. package/src/transcript/items/ids.ts +0 -0
  38. package/src/transcript/items/render.ts +24 -24
  39. package/src/transcript/items/select.ts +5 -5
  40. package/src/transcript/transcripts.ts +3 -3
  41. package/src/translate/translate.ts +1 -1
  42. package/src/transport/driver.ts +13 -11
  43. package/src/upstream/gateway.ts +3 -3
  44. package/src/upstream/requests.ts +1 -1
  45. package/src/upstream/status.ts +1 -1
  46. package/src/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/cli",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "The ccmsg daemon, CLI and agent plugins for one instance (= one config home)",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
@@ -20,7 +20,7 @@
20
20
  "test": "bun test"
21
21
  },
22
22
  "dependencies": {
23
- "@ccmsg/protocol": "1.20.0"
23
+ "@ccmsg/protocol": "1.22.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "^1.3.0",
package/src/auth/auth.ts CHANGED
@@ -4,9 +4,9 @@ import type {
4
4
  AuthChallenge,
5
5
  AuthChallengeResult,
6
6
  AuthRecord,
7
- AuthRefreshArgs,
7
+ AuthExtendArgs,
8
8
  AuthRefreshReason,
9
- AuthRefreshResult,
9
+ AuthExtendResult,
10
10
  AuthRegisterArgs,
11
11
  AuthResolveArgs,
12
12
  AuthResolveResult,
@@ -339,7 +339,7 @@ export class Auth {
339
339
  return closed;
340
340
  }
341
341
 
342
- /** Take what a peer wrote on `auth_records`, and act on the removals in it. */
342
+ /** Take what a peer wrote on `auth.records`, and act on the removals in it. */
343
343
  merge(records: readonly AuthRecord[]): void {
344
344
  const { removed } = this.deps.records.merge(records);
345
345
  for (const sub of removed) this.disconnect(sub);
@@ -372,7 +372,7 @@ export class Auth {
372
372
  this.spend(challenge.challenge);
373
373
  return;
374
374
  }
375
- await this.#atIssuer(challenge.issuer, "auth_resolve", {
375
+ await this.#atIssuer(challenge.issuer, "auth.resolve", {
376
376
  kind: "challenge",
377
377
  challenge: challenge.challenge,
378
378
  } satisfies AuthResolveArgs);
@@ -497,7 +497,7 @@ export class Auth {
497
497
  async #claimsOf(args: AuthRegisterArgs): Promise<RegisterClaims> {
498
498
  const stated = claimsOf(args.token);
499
499
  if (stated.iss === this.deps.self) return this.resolveRegistration(args.token, args.code);
500
- const answer = (await this.#atIssuer(stated.iss, "auth_resolve", {
500
+ const answer = (await this.#atIssuer(stated.iss, "auth.resolve", {
501
501
  kind: "register",
502
502
  token: args.token,
503
503
  code: args.code,
@@ -510,7 +510,7 @@ export class Auth {
510
510
 
511
511
  /** Check a registration URL against the secret that signed it, and spend it.
512
512
  *
513
- * Only the issuer can run this, which is what `auth_resolve` is for. The code
513
+ * Only the issuer can run this, which is what `auth.resolve` is for. The code
514
514
  * is checked here too: it was issued with the secret and is held beside it,
515
515
  * and letting another instance check it would be putting the one defence
516
516
  * against a leaked URL somewhere the URL's holder could reach. */
@@ -690,7 +690,7 @@ export class Auth {
690
690
  // remembered as a time and nothing else. The issuer writes them
691
691
  // unchecked, as it does the ones it observes itself (contract,
692
692
  // `AuthRotateArgs`).
693
- const answer = (await this.#atIssuer(held.body.iss, "auth_rotate", {
693
+ const answer = (await this.#atIssuer(held.body.iss, "auth.rotate", {
694
694
  refresh_token: value,
695
695
  ...(from.reason === undefined ? {} : { reason: from.reason }),
696
696
  ...(from.ip === undefined ? {} : { ip: from.ip }),
@@ -718,7 +718,7 @@ export class Auth {
718
718
  return;
719
719
  }
720
720
  try {
721
- await this.#atIssuer(owner.body.iss, "auth_rotate", {
721
+ await this.#atIssuer(owner.body.iss, "auth.rotate", {
722
722
  refresh_token: value,
723
723
  } satisfies AuthRotateArgs);
724
724
  } catch {
@@ -729,7 +729,7 @@ export class Auth {
729
729
  }
730
730
 
731
731
  /** Rotate a family this instance minted. The one writer's own operation, and
732
- * what `auth_rotate` runs on its behalf. */
732
+ * what `auth.rotate` runs on its behalf. */
733
733
  rotate(value: Base64Url, from: RefreshFrom = {}): AuthRotateResult {
734
734
  const held = this.deps.records.byRefresh(value);
735
735
  if (held === undefined) {
@@ -862,7 +862,7 @@ export class Auth {
862
862
  }
863
863
 
864
864
  /** Extend a live connection with a token got from `/auth/refresh` (§2.5). */
865
- extend(conn: Requester, args: AuthRefreshArgs): AuthRefreshResult {
865
+ extend(conn: Requester, args: AuthExtendArgs): AuthExtendResult {
866
866
  const held = this.#authorized.get(conn);
867
867
  if (held === undefined) {
868
868
  throw new OpError("auth_invalid", "この接続は token で開かれたものではありません");
@@ -919,9 +919,9 @@ export class Auth {
919
919
  * answers for another instance. */
920
920
  export function authHandlers(auth: Auth) {
921
921
  return {
922
- auth_refresh: (input: HandlerInput): AuthRefreshResult =>
923
- auth.extend(input.conn, input.args as unknown as AuthRefreshArgs),
924
- auth_resolve: (input: HandlerInput): AuthResolveResult => {
922
+ "auth.extend": (input: HandlerInput): AuthExtendResult =>
923
+ auth.extend(input.conn, input.args as unknown as AuthExtendArgs),
924
+ "auth.resolve": (input: HandlerInput): AuthResolveResult => {
925
925
  const args = input.args as unknown as AuthResolveArgs;
926
926
  if (args.kind === "challenge") {
927
927
  auth.spend(args.challenge);
@@ -932,7 +932,7 @@ export function authHandlers(auth: Auth) {
932
932
  // the URL and the count of tries against it (§2.2).
933
933
  return { kind: "register", claims: auth.resolveRegistration(args.token, args.code) };
934
934
  },
935
- auth_rotate: (input: HandlerInput): AuthRotateResult => {
935
+ "auth.rotate": (input: HandlerInput): AuthRotateResult => {
936
936
  const args = input.args as unknown as AuthRotateArgs;
937
937
  // The receiving instance's account of the person, taken as stated: it is
938
938
  // the only one that saw them, and `last_refresh` is a hint nothing is
package/src/auth/http.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import {
3
3
  type AuthAssertArgs,
4
- type AuthRefreshTokenArgs,
4
+ type AuthTokenRefreshArgs,
5
5
  type AuthRegisterArgs,
6
6
  type ErrorCode,
7
7
  type InstanceId,
@@ -24,10 +24,10 @@ type Route = (typeof ROUTES)[number];
24
24
  /** Which op each route carries. The route is a name a proxy can see; the op is
25
25
  * what the attribute table and the schemas are keyed by. */
26
26
  const OP_OF: Record<Route, OpName> = {
27
- challenge: "auth_challenge",
28
- register: "auth_register",
29
- assert: "auth_assert",
30
- refresh: "auth_refresh_token",
27
+ challenge: "auth.challenge",
28
+ register: "auth.register",
29
+ assert: "auth.assert",
30
+ refresh: "auth.token.refresh",
31
31
  };
32
32
 
33
33
  /** Cap on an `/auth/*` body. Everything these take is a handful of base64url
@@ -197,7 +197,7 @@ export async function handleAuth(
197
197
  if (held === undefined) {
198
198
  return refusal("auth_invalid", "この要求には refresh token がありません", cors);
199
199
  }
200
- const { reason } = args as unknown as AuthRefreshTokenArgs;
200
+ const { reason } = args as unknown as AuthTokenRefreshArgs;
201
201
  const minted = await deps.auth.refreshToken(held, {
202
202
  ...(reason === undefined ? {} : { reason }),
203
203
  ...(seen.ip === undefined ? {} : { ip: seen.ip }),
@@ -63,7 +63,7 @@ export interface RecordsDeps {
63
63
  * from a peer refusable on sight: a family this instance minted is written by
64
64
  * this instance alone (DR-0001 §2.4). */
65
65
  readonly self: InstanceId;
66
- /** Hand what this instance wrote to the peers, on the `auth_records` topic. */
66
+ /** Hand what this instance wrote to the peers, on the `auth.records` topic. */
67
67
  readonly publish: (records: readonly AuthRecord[]) => void;
68
68
  readonly now?: () => Timestamp;
69
69
  }
package/src/auth/topic.ts CHANGED
@@ -3,7 +3,7 @@ import type { Requester } from "../dispatch/index.ts";
3
3
  import type { TopicValue, UpstreamResource } from "../topics/index.ts";
4
4
  import type { AuthRecords } from "./records.ts";
5
5
 
6
- /** `auth_records` as the topic mechanism sees it (DR-0001 §2.6).
6
+ /** `auth.records` as the topic mechanism sees it (DR-0001 §2.6).
7
7
  *
8
8
  * The records are here whether anyone is subscribed or not — they are what
9
9
  * authenticates a person, not a watch on something — so there is nothing to
package/src/cli.ts CHANGED
@@ -202,7 +202,7 @@ const ROOT: Command = {
202
202
  },
203
203
  {
204
204
  name: "stop",
205
- summary: "監督者に、子を止めさせる (instance_shutdown、以後は上げ直さない)",
205
+ summary: "監督者に、子を止めさせる (instance.shutdown、以後は上げ直さない)",
206
206
  usage: "ccmsg daemon stop <dir> | --all",
207
207
  run: (args) => supervised("supervise_stop", args),
208
208
  },
@@ -214,7 +214,7 @@ const ROOT: Command = {
214
214
  },
215
215
  {
216
216
  name: "status",
217
- summary: "監督者が各子に instance_ping して version・network・peers を答える",
217
+ summary: "監督者が各子に instance.ping して version・network・peers を答える",
218
218
  usage: "ccmsg daemon status [dir] | --all",
219
219
  bare: true,
220
220
  run: (args) => supervised("supervise_status", args, true),
@@ -395,7 +395,7 @@ const ROOT: Command = {
395
395
  summary: "この instance が持つ preset の名前と中身を並べる",
396
396
  usage: "ccmsg dump presets",
397
397
  bare: true,
398
- run: () => instanceAsk({ op: "dump_presets_read" }),
398
+ run: () => instanceAsk({ op: "dump.presets.read" }),
399
399
  },
400
400
  ],
401
401
  run: (args) => dump(args),
@@ -585,7 +585,7 @@ async function runInstance(dir: string | undefined): Promise<unknown> {
585
585
  }
586
586
  const instance = outcome;
587
587
  // A signal is a request to leave, and leaving is the ordered shutdown of
588
- // §8.5 — the same one `instance_shutdown` runs, so a client sees the same
588
+ // §8.5 — the same one `instance.shutdown` runs, so a client sees the same
589
589
  // departure either way. The listeners are removed once it has run, because a
590
590
  // signal listener keeps the event loop alive and the process would sit at an
591
591
  // empty loop instead of exiting.
@@ -852,8 +852,8 @@ function peers(args: readonly string[]): Promise<unknown> {
852
852
  "peers",
853
853
  parsed.flags.has("all"),
854
854
  sid === undefined || sid === ""
855
- ? { op: "hello", role: "user", protocol_version: PROTOCOL_VERSION }
856
- : { op: "hello", role: "session", sid, protocol_version: PROTOCOL_VERSION, ...statedMeta() },
855
+ ? { op: "hello.user", protocol_version: PROTOCOL_VERSION }
856
+ : { op: "hello.session", sid, protocol_version: PROTOCOL_VERSION, ...statedMeta() },
857
857
  );
858
858
  }
859
859
 
@@ -864,8 +864,7 @@ function peers(args: readonly string[]): Promise<unknown> {
864
864
  function agents(args: readonly string[]): Promise<unknown> {
865
865
  const parsed = options(args, [], ["all", "json"]);
866
866
  return topic("agents", parsed.flags.has("all"), {
867
- op: "hello",
868
- role: "user",
867
+ op: "hello.user",
869
868
  protocol_version: PROTOCOL_VERSION,
870
869
  });
871
870
  }
@@ -919,7 +918,7 @@ async function dump(args: readonly string[]): Promise<unknown> {
919
918
  );
920
919
  }
921
920
  const written = (await instanceAsk({
922
- op: "session_dump_write",
921
+ op: "session.dump.write",
923
922
  ...dumpArgs(subject, parsed.named),
924
923
  })) as unknown as SessionDumpWriteResult;
925
924
  const body = readFileSync(written.path, "utf8");
@@ -1028,10 +1027,10 @@ function post(args: readonly string[]): Promise<unknown> {
1028
1027
  * delivered envelope as `ccmsg-from`.
1029
1028
  *
1030
1029
  * Without `--to` the answer is for a person, because that is the one message
1031
- * whose reply line carries no addressee: `user` is not a sid and `message_send`
1030
+ * whose reply line carries no addressee: `user` is not a sid and `message.send`
1032
1031
  * addresses a sid, so there is nothing to send to. The contract leaves how such
1033
1032
  * an answer arrives to the instance and names the route — it reaches them as a
1034
- * notification, which is `notify_send`. The caller runs the line either way and
1033
+ * notification, which is `notify.send`. The caller runs the line either way and
1035
1034
  * does not have to know which of the two it became. */
1036
1035
  function reply(args: readonly string[]): Promise<unknown> {
1037
1036
  const parsed = options(args, ["sid", "to"]);
@@ -1080,7 +1079,7 @@ export async function stopping(args: readonly string[], read?: Read): Promise<un
1080
1079
  // what the working directory reveals.
1081
1080
  return await call(
1082
1081
  parsed.named.get("sid") ?? event.sid,
1083
- { op: "session_stopping", ...(reason === undefined ? {} : { reason }) },
1082
+ { op: "session.stopping", ...(reason === undefined ? {} : { reason }) },
1084
1083
  stated(parsed.named, event),
1085
1084
  );
1086
1085
  }
@@ -1113,8 +1112,7 @@ export async function hello(args: readonly string[], read?: Read): Promise<unkno
1113
1112
  if (conn === undefined) return { greeted: false, reason: "no_instance" };
1114
1113
  try {
1115
1114
  await conn.ask({
1116
- op: "hello",
1117
- role: "session",
1115
+ op: "hello.session",
1118
1116
  sid,
1119
1117
  protocol_version: PROTOCOL_VERSION,
1120
1118
  ...meta,
@@ -1224,19 +1222,19 @@ async function plugin(
1224
1222
  return outcome;
1225
1223
  }
1226
1224
 
1227
- /** One `message_send`. */
1225
+ /** One `message.send`. */
1228
1226
  function send(named: string | undefined, args: MessageSendArgs): Promise<unknown> {
1229
- return call(named, { op: "message_send", ...args });
1227
+ return call(named, { op: "message.send", ...args });
1230
1228
  }
1231
1229
 
1232
- /** One `notify_send`. */
1230
+ /** One `notify.send`. */
1233
1231
  function announce(named: string | undefined, args: NotifySendArgs): Promise<unknown> {
1234
- return call(named, { op: "notify_send", ...args });
1232
+ return call(named, { op: "notify.send", ...args });
1235
1233
  }
1236
1234
 
1237
1235
  /** One op, spoken as the session this process runs inside.
1238
1236
  *
1239
- * The greeting is `role: "session"` because that is what the caller is: the
1237
+ * The greeting is `hello.session` because that is what the caller is: the
1240
1238
  * instance takes the sender and the subject from the connection rather than
1241
1239
  * from the arguments, so a connection that greeted as anything else has nobody
1242
1240
  * to answer and nothing to be about.
@@ -1259,7 +1257,7 @@ async function call(
1259
1257
  );
1260
1258
  }
1261
1259
  return await exchange(
1262
- { op: "hello", role: "session", sid, protocol_version: PROTOCOL_VERSION, ...meta },
1260
+ { op: "hello.session", sid, protocol_version: PROTOCOL_VERSION, ...meta },
1263
1261
  request,
1264
1262
  );
1265
1263
  }
@@ -1269,7 +1267,7 @@ async function call(
1269
1267
  * Which is who is asking: reading a transcript is not something a session is a
1270
1268
  * party to, and the ops that do it are open to a person and to nobody else. */
1271
1269
  function instanceAsk(request: Record<string, unknown>): Promise<unknown> {
1272
- return exchange({ op: "hello", role: "user", protocol_version: PROTOCOL_VERSION }, request);
1270
+ return exchange({ op: "hello.user", protocol_version: PROTOCOL_VERSION }, request);
1273
1271
  }
1274
1272
 
1275
1273
  /** Greet this config home's instance, ask it one thing, and answer with what
@@ -1354,13 +1352,12 @@ async function posted(text: string): Promise<void> {
1354
1352
  let timer: ReturnType<typeof setTimeout> | undefined;
1355
1353
  const record = async (): Promise<void> => {
1356
1354
  const greeting = await conn.ask({
1357
- op: "hello",
1358
- role: "session",
1355
+ op: "hello.session",
1359
1356
  sid,
1360
1357
  protocol_version: PROTOCOL_VERSION,
1361
1358
  ...statedMeta(),
1362
1359
  });
1363
- if (greeting["ok"] === true) await conn.ask({ op: "say_post", text });
1360
+ if (greeting["ok"] === true) await conn.ask({ op: "say.post", text });
1364
1361
  };
1365
1362
  const budget = new Promise<void>((resolve) => {
1366
1363
  timer = setTimeout(resolve, SAY_POST_MS);
@@ -49,10 +49,10 @@ export async function connect(path: string): Promise<Conn | undefined> {
49
49
 
50
50
  /** Greet as the person running the command.
51
51
  *
52
- * `role: "user"` because that is what the caller is: the lifecycle ops belong
52
+ * `hello.user` because that is what the caller is: the lifecycle ops belong
53
53
  * to whoever operates the host, not to a session speaking from inside a turn. */
54
54
  export function greetAsUser(conn: Conn): Promise<Record<string, unknown>> {
55
- return conn.ask({ op: "hello", role: "user", protocol_version: PROTOCOL_VERSION });
55
+ return conn.ask({ op: "hello.user", protocol_version: PROTOCOL_VERSION });
56
56
  }
57
57
 
58
58
  /** Reassemble the replies of one exchange, by arrival order. */
@@ -110,31 +110,31 @@ const STARTING_PRESETS = [
110
110
  {
111
111
  name: "file",
112
112
  description: "ファイル操作。読み書きと探索をひとまとめに",
113
- opts: { types: ["tool:Read", "tool:Write", "tool:Edit", "tool:Glob", "tool:Grep"] },
113
+ opts: { types: ["tool.Read", "tool.Write", "tool.Edit", "tool.Glob", "tool.Grep"] },
114
114
  },
115
115
  {
116
116
  name: "howto",
117
117
  description: "調査のノウハウだけ。何を考えて何を叩いて何を読み書きしたか",
118
118
  opts: {
119
- types: ["thinking", "message:user", "message:parent", "message:sub", "tool:Bash", "@file"],
119
+ types: ["thinking", "message.user", "message.parent", "message.sub", "tool.Bash", "@file"],
120
120
  },
121
121
  },
122
122
  {
123
123
  name: "journal",
124
124
  description: "日記用。人との往復と worker の答え、思考は要点だけ",
125
125
  opts: {
126
- types: ["message:user", "message:parent", "message:sub:in", "message:team:in", "thinking"],
126
+ types: ["message.user", "message.parent", "message.sub.in", "message.team.in", "thinking"],
127
127
  },
128
128
  },
129
129
  {
130
130
  name: "handoff",
131
131
  description: "後継セッションへの引き継ぎ。直近の会話と、走っているものの台帳",
132
- opts: { types: ["message", "system:task", "ids"] },
132
+ opts: { types: ["message", "system.task", "ids"] },
133
133
  },
134
134
  {
135
135
  name: "audit",
136
136
  description: "何をしたかの追跡。会話は落として操作と通知だけ",
137
- opts: { types: ["@file", "tool:Bash", "notice", "ids"] },
137
+ opts: { types: ["@file", "tool.Bash", "notice", "ids"] },
138
138
  },
139
139
  ];
140
140
 
@@ -225,7 +225,7 @@ export async function status(target: Target): Promise<StatusRow> {
225
225
  if (greeting["ok"] !== true) return row;
226
226
  const peers =
227
227
  (greeting["instances"] as { id: InstanceId; endpoint: Endpoint }[] | undefined) ?? [];
228
- const answer = await conn.ask({ op: "instance_ping" });
228
+ const answer = await conn.ask({ op: "instance.ping" });
229
229
  if (answer["ok"] !== true) return row;
230
230
  const ping = answer as unknown as InstancePingResult;
231
231
  return {
@@ -259,7 +259,7 @@ export async function stop(target: Target): Promise<{ dir: string; stopped: bool
259
259
  if (greeting["ok"] !== true) {
260
260
  throw new CommandError("forbidden", `hello が拒否されました: ${JSON.stringify(greeting)}`);
261
261
  }
262
- const answer = await conn.ask({ op: "instance_shutdown" });
262
+ const answer = await conn.ask({ op: "instance.shutdown" });
263
263
  if (answer["ok"] !== true) {
264
264
  throw new CommandError("internal_error", `停止を拒否されました: ${JSON.stringify(answer)}`);
265
265
  }
@@ -2,12 +2,12 @@ import type { InstanceId } from "@ccmsg/protocol";
2
2
  import type { Conn } from "./control.ts";
3
3
  import { CommandError } from "./link.ts";
4
4
 
5
- /** One instance's whole value on a topic, as the frame carried it.
5
+ /** What one instance opened a topic with, as the frame carried it.
6
6
  *
7
- * The pair rather than the payload alone, because a cluster topic is a whole
8
- * value *per instance* (§6.2): two instances state the same topic name, and a
9
- * payload with the name of its author taken off could not be told from the
10
- * other's. */
7
+ * The pair rather than the payload alone, because two instances state the same
8
+ * topic name: a payload with the name of its author taken off could not be told
9
+ * from the other's, and which instance said it is half of what a cluster read
10
+ * answers. */
11
11
  export interface Snapshot {
12
12
  readonly instance: InstanceId;
13
13
  readonly data: unknown;
@@ -46,17 +46,19 @@ interface Greeted {
46
46
  * frame is deferred behind the acknowledgement and is therefore already on the
47
47
  * wire; the relayed ones are what the budget is for.
48
48
  *
49
- * Later frames are folded in the same way a subscriber would fold them: a
50
- * second frame from an instance replaces that instance's entry and leaves the
51
- * others alone. Nothing else is waited for the first frame from every
52
- * expected instance ends the read. */
49
+ * The opening frame is the whole of what its instance holds, whichever kind of
50
+ * topic it is: a `per_instance_whole` one states that instance's reading
51
+ * entire, and an `element` one opens with every row it has rather than with the
52
+ * rows that just changed. So one frame per instance is a complete read, and
53
+ * nothing after it is waited for — the first frame from every expected instance
54
+ * ends it. */
53
55
  export async function snapshots(
54
56
  conn: Conn,
55
57
  topic: string,
56
58
  expected: readonly InstanceId[],
57
59
  budgetMs: number = SNAPSHOT_BUDGET_MS,
58
60
  ): Promise<Snapshot[]> {
59
- const ack = await conn.ask({ op: "topic_subscribe", topic });
61
+ const ack = await conn.ask({ op: "topic.subscribe", topic });
60
62
  if (ack["ok"] !== true) {
61
63
  const error = ack["error"] as { code?: string; msg?: string } | undefined;
62
64
  throw new CommandError(
@@ -273,7 +273,7 @@ export class Supervisor {
273
273
  *
274
274
  * Asked over its own socket rather than signalled, so what runs is the
275
275
  * ordered shutdown of §8.5 — the same departure a client sees from an
276
- * `instance_shutdown`. */
276
+ * `instance.shutdown`. */
277
277
  async stopOne(dir: string): Promise<{ dir: string; stopped: boolean }> {
278
278
  const unit = this.#units.get(dir);
279
279
  if (unit === undefined) throw new CommandError("not_found", `${dir} は登録されていません`);
@@ -16,7 +16,7 @@ export interface SessionRoots {
16
16
  * session greeted with, or its working directory. Absent when the session
17
17
  * greeted with neither, which admits no contained path at all. */
18
18
  readonly root?: string;
19
- /** Where `file_write` puts a file. Kept apart from `root`, which may be the
19
+ /** Where `file.write` puts a file. Kept apart from `root`, which may be the
20
20
  * container above the working copy. */
21
21
  readonly cwd?: string;
22
22
  /** Absolute folder paths, each admitting its whole subtree. */
@@ -40,7 +40,7 @@ export interface Located {
40
40
  readonly path: string;
41
41
  /** The same path with its own last segment unresolved: what was named, not
42
42
  * what it points at. An op that acts on the name rather than on the file —
43
- * `file_delete`, which unlinks a name — asks what kind of thing was named,
43
+ * `file.delete`, which unlinks a name — asks what kind of thing was named,
44
44
  * and only this distinguishes a file from a symlink to one. */
45
45
  readonly named: string;
46
46
  }
@@ -101,7 +101,7 @@ export class Containment {
101
101
  return undefined;
102
102
  }
103
103
 
104
- /** Where `file_write` writes, which is the one destination no kind names: the
104
+ /** Where `file.write` writes, which is the one destination no kind names: the
105
105
  * session's working directory, and within it the inbox the destination is
106
106
  * fixed to (DR-0019). A name that leaves the inbox is refused as unwritable
107
107
  * rather than as forbidden — the path is reachable, and only writing there
@@ -195,7 +195,7 @@ export class Containment {
195
195
  * about rather than one it guesses a range for. An op with no `scope` states
196
196
  * no role, and has none to narrow by: dispatch already settled who may call it.
197
197
  *
198
- * The visible range is one function rather than one per op: `transcript_read`
198
+ * The visible range is one function rather than one per op: `transcript.read`
199
199
  * and the file ops narrow by the same rule, and two spellings of it could come
200
200
  * apart while both still passing their own tests. */
201
201
  export function sees(sid: Sid, viewer: Viewer): boolean {
@@ -223,7 +223,7 @@ export interface DirArgs {
223
223
  readonly path?: string;
224
224
  }
225
225
 
226
- /** The directory `file_write` writes into, relative to the working directory. */
226
+ /** The directory `file.write` writes into, relative to the working directory. */
227
227
  const INBOX = join("docs", "inbox");
228
228
 
229
229
  const KINDS = ["contained", "workspace", "external"] as const;
@@ -28,8 +28,8 @@ import type {
28
28
  FileFindResult,
29
29
  FileReadArgs,
30
30
  FileReadResult,
31
- FileStatBatchArgs,
32
- FileStatBatchResult,
31
+ FileStatArgs,
32
+ FileStatResult,
33
33
  FileStatEntry,
34
34
  FileWriteArgs,
35
35
  FileWriteResult,
@@ -38,7 +38,7 @@ import type {
38
38
  import { type HandlerInput, OpError } from "../dispatch/index.ts";
39
39
  import type { Containment, Located, Viewer } from "./containment.ts";
40
40
 
41
- /** How much of a file `file_read` carries. Larger files are answered with their
41
+ /** How much of a file `file.read` carries. Larger files are answered with their
42
42
  * head and `truncated`, so one file can never cost the connection more than
43
43
  * this however big it grew (DR-0008 §5). */
44
44
  const READ_LIMIT = 512 * 1024;
@@ -65,7 +65,7 @@ export function fileHandlers(paths: Containment) {
65
65
  const viewer = (input: HandlerInput): Viewer => ({ role: input.role, sid: input.identity?.sid });
66
66
 
67
67
  return {
68
- dir_list: (input: HandlerInput): DirListResult => {
68
+ "dir.list": (input: HandlerInput): DirListResult => {
69
69
  const args = input.args as unknown as DirListArgs;
70
70
  const at = paths.root(args, viewer(input));
71
71
  const stat = existing(at);
@@ -73,7 +73,7 @@ export function fileHandlers(paths: Containment) {
73
73
  return { sid: args.sid, path: at.path, entries: entriesOf(at.real) };
74
74
  },
75
75
 
76
- file_read: (input: HandlerInput): FileReadResult => {
76
+ "file.read": (input: HandlerInput): FileReadResult => {
77
77
  const args = input.args as unknown as FileReadArgs;
78
78
  const at = paths.locate(args, viewer(input));
79
79
  const stat = existing(at);
@@ -95,7 +95,7 @@ export function fileHandlers(paths: Containment) {
95
95
  };
96
96
  },
97
97
 
98
- file_write: (input: HandlerInput): FileWriteResult => {
98
+ "file.write": (input: HandlerInput): FileWriteResult => {
99
99
  const args = input.args as unknown as FileWriteArgs;
100
100
  const at = paths.inbox(args.sid, args.path, viewer(input));
101
101
  // The inbox takes new notes, so an existing name is refused rather than
@@ -106,7 +106,7 @@ export function fileHandlers(paths: Containment) {
106
106
  return { sid: args.sid, path: at.path };
107
107
  },
108
108
 
109
- file_create: (input: HandlerInput): FileCreateResult => {
109
+ "file.create": (input: HandlerInput): FileCreateResult => {
110
110
  const args = input.args as unknown as FileCreateArgs;
111
111
  const at = paths.locate(args, viewer(input));
112
112
  const parent = dirname(at.real);
@@ -117,7 +117,7 @@ export function fileHandlers(paths: Containment) {
117
117
  return { sid: args.sid, path: at.path };
118
118
  },
119
119
 
120
- file_edit: (input: HandlerInput): FileEditResult => {
120
+ "file.edit": (input: HandlerInput): FileEditResult => {
121
121
  const args = input.args as unknown as FileEditArgs;
122
122
  const at = paths.locate(args, viewer(input));
123
123
  const stat = existing(at);
@@ -133,7 +133,7 @@ export function fileHandlers(paths: Containment) {
133
133
  return { sid: args.sid, path: at.path, size: after.size, mtime_at: mtimeOf(after) };
134
134
  },
135
135
 
136
- file_delete: (input: HandlerInput): FileDeleteResult => {
136
+ "file.delete": (input: HandlerInput): FileDeleteResult => {
137
137
  const args = input.args as unknown as FileDeleteArgs;
138
138
  const at = paths.locate(args, viewer(input));
139
139
  // What is unlinked is what is named, so this reads the name itself rather
@@ -150,7 +150,7 @@ export function fileHandlers(paths: Containment) {
150
150
  return { sid: args.sid, path: at.path };
151
151
  },
152
152
 
153
- file_find: (input: HandlerInput): FileFindResult => {
153
+ "file.find": (input: HandlerInput): FileFindResult => {
154
154
  const args = input.args as unknown as FileFindArgs;
155
155
  const at = paths.root(
156
156
  { sid: args.sid, kind: args.kind, ...(args.root === undefined ? {} : { path: args.root }) },
@@ -164,8 +164,8 @@ export function fileHandlers(paths: Containment) {
164
164
  return { sid: args.sid, hits: walk.hits, truncated: walk.truncated };
165
165
  },
166
166
 
167
- file_stat_batch: (input: HandlerInput): FileStatBatchResult => {
168
- const args = input.args as unknown as FileStatBatchArgs;
167
+ "file.stat": (input: HandlerInput): FileStatResult => {
168
+ const args = input.args as unknown as FileStatArgs;
169
169
  const results = args.paths.map((path): FileStatEntry | null => {
170
170
  const at = paths.identify(args.sid, path, viewer(input));
171
171
  if (at === undefined || !isFile(at.real)) return null;
Binary file
@@ -1,11 +1,14 @@
1
1
  import { basename, dirname } from "node:path";
2
- import type { HelloArgs } from "@ccmsg/protocol";
2
+ import type { HelloSessionArgs } from "@ccmsg/protocol";
3
3
 
4
4
  /** What a session can say about itself when it greets: the contract's shared
5
5
  * fields, each of them optional because a greeting states what it knows and
6
6
  * the instance leaves the rest unknown (contract, `SessionMetaFields`). */
7
7
  export type StatedMeta = Partial<
8
- Pick<HelloArgs, "repo" | "ws" | "cwd" | "repo_root" | "branch" | "transcript_path" | "title">
8
+ Pick<
9
+ HelloSessionArgs,
10
+ "repo" | "ws" | "cwd" | "repo_root" | "branch" | "transcript_path" | "title"
11
+ >
9
12
  >;
10
13
 
11
14
  /** How a question is put to the version control the session works under. The