@ccmsg/cli 0.8.1 → 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 +50 -39
  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
@@ -4,7 +4,7 @@ import { fields, type Item } from "./item.ts";
4
4
  *
5
5
  * A type is drawn by one function, the way the same type is drawn by one
6
6
  * component where the destination is a screen instead of text. What the two
7
- * share is the classification; how a `tool:Bash` reads is the drawing's own
7
+ * share is the classification; how a `tool.Bash` reads is the drawing's own
8
8
  * business, and neither side carries the other's.
9
9
  *
10
10
  * A type nobody wrote a drawing for is still drawn. The generic shape says the
@@ -33,8 +33,8 @@ const EMPTY: readonly string[] = [];
33
33
  /** The drawing for one item, whatever its type. */
34
34
  export function fragment(item: Item): Fragment {
35
35
  const type = item.type;
36
- if (type.startsWith("tool:")) {
37
- const name = type.slice("tool:".length);
36
+ if (type.startsWith("tool.")) {
37
+ const name = type.slice("tool.".length);
38
38
  const result = isResult(item);
39
39
  const draw = (result ? RESULTS : USES)[name];
40
40
  if (draw !== undefined) return draw(item);
@@ -44,8 +44,8 @@ export function fragment(item: Item): Fragment {
44
44
  }
45
45
  const draw = ITEMS[type];
46
46
  if (draw !== undefined) return draw(item);
47
- if (type.startsWith("hook:")) return hook(item);
48
- if (type.startsWith("system:attachment:"))
47
+ if (type.startsWith("hook.")) return hook(item);
48
+ if (type.startsWith("system.attachment."))
49
49
  return { head: "", body: summary(fields(item)["attachment"]) };
50
50
  return { head: "", body: summary(own(item)) };
51
51
  }
@@ -60,16 +60,16 @@ function isResult(item: Item): boolean {
60
60
  * kept as they were written: a dump is read to find out what somebody actually
61
61
  * wrote, and a reader who wants less asks for less. */
62
62
  const SAID: readonly string[] = [
63
- "message:user:in",
64
- "message:user:out",
63
+ "message.user.in",
64
+ "message.user.out",
65
65
  "thinking",
66
- "system:compact",
66
+ "system.compact",
67
67
  ];
68
68
 
69
69
  const ITEMS: Record<string, Draw> = {
70
70
  ...Object.fromEntries(SAID.map((type) => [type, said])),
71
71
 
72
- "message:sub:out": (item) => ({
72
+ "message.sub.out": (item) => ({
73
73
  head: words(
74
74
  field(item, "agent_id", "agent="),
75
75
  field(item, "subagent_type", "type="),
@@ -79,7 +79,7 @@ const ITEMS: Record<string, Draw> = {
79
79
  body: lines(str(item, "prompt")),
80
80
  }),
81
81
 
82
- "message:sub:in": (item) => ({
82
+ "message.sub.in": (item) => ({
83
83
  head: words(
84
84
  field(item, "agent_id", "agent="),
85
85
  field(item, "status", "status="),
@@ -91,17 +91,17 @@ const ITEMS: Record<string, Draw> = {
91
91
  // The one above and the ones alongside. A name is on the heading wherever
92
92
  // the record gave one — an answer handed back as prose names nobody, and a
93
93
  // heading that invented a name for it would say more than the file does.
94
- "message:parent:in": (item) => ({
94
+ "message.parent.in": (item) => ({
95
95
  head: words(field(item, "harness_name", "from="), mid(item)),
96
96
  body: lines(str(item, "text")),
97
97
  }),
98
98
 
99
- "message:parent:out": (item) => ({
99
+ "message.parent.out": (item) => ({
100
100
  head: words(field(item, "harness_name", "to="), str(item, "summary")),
101
101
  body: lines(str(item, "text")),
102
102
  }),
103
103
 
104
- "message:team:out": (item) => ({
104
+ "message.team.out": (item) => ({
105
105
  head: words(
106
106
  field(item, "harness_name", "to="),
107
107
  field(item, "agent_id", "agent="),
@@ -115,7 +115,7 @@ const ITEMS: Record<string, Draw> = {
115
115
  // Both halves of a teammate's correspondence arrive under one type: a letter
116
116
  // it wrote, which names who wrote it, and its run ending, which names how it
117
117
  // ended. Each heading says whichever of those the item carried.
118
- "message:team:in": (item) => ({
118
+ "message.team.in": (item) => ({
119
119
  head: words(
120
120
  field(item, "harness_name", "from="),
121
121
  mid(item),
@@ -126,12 +126,12 @@ const ITEMS: Record<string, Draw> = {
126
126
  body: lines(str(item, "text")),
127
127
  }),
128
128
 
129
- "message:session:out": (item) => ({
129
+ "message.session.out": (item) => ({
130
130
  head: words(field(item, "to", "to="), field(item, "reply_to", "reply_to="), mid(item)),
131
131
  body: lines(str(item, "text")),
132
132
  }),
133
133
 
134
- "message:session:in": (item) => ({
134
+ "message.session.in": (item) => ({
135
135
  head: words(field(item, "from", "from="), mid(item)),
136
136
  body: lines(str(item, "text")),
137
137
  }),
@@ -140,15 +140,15 @@ const ITEMS: Record<string, Draw> = {
140
140
  // voice. Both are why a conversation jumps rather than part of it, so they
141
141
  // are a line each and the line names what happened. Everything the record
142
142
  // held is in the JSON dump beside this one, addressable by the id shown.
143
- "notice:slash": (item) => ({
143
+ "notice.slash": (item) => ({
144
144
  head: words(`/${str(item, "command") ?? ""}`, str(item, "args"), first(str(item, "stdout"))),
145
145
  body: EMPTY,
146
146
  }),
147
- "notice:interrupt": (item) => ({ head: first(str(item, "text")) ?? "", body: EMPTY }),
148
- "system:api-error": (item) => ({ head: first(str(item, "text")) ?? "", body: EMPTY }),
149
- "system:caveat": (item) => ({ head: first(str(item, "text")) ?? "", body: EMPTY }),
150
- "system:resume": (item) => ({ head: first(str(item, "text")) ?? "", body: EMPTY }),
151
- "system:task": (item) => ({
147
+ "notice.interrupt": (item) => ({ head: first(str(item, "text")) ?? "", body: EMPTY }),
148
+ "system.api.error": (item) => ({ head: first(str(item, "text")) ?? "", body: EMPTY }),
149
+ "system.caveat": (item) => ({ head: first(str(item, "text")) ?? "", body: EMPTY }),
150
+ "system.resume": (item) => ({ head: first(str(item, "text")) ?? "", body: EMPTY }),
151
+ "system.task": (item) => ({
152
152
  head: words(
153
153
  field(item, "task_id", "task="),
154
154
  field(item, "event", "event="),
@@ -156,7 +156,7 @@ const ITEMS: Record<string, Draw> = {
156
156
  ),
157
157
  body: EMPTY,
158
158
  }),
159
- "system:unknown": (item) => ({ head: "", body: summary(fields(item)["record"]) }),
159
+ "system.unknown": (item) => ({ head: "", body: summary(fields(item)["record"]) }),
160
160
  };
161
161
 
162
162
  function said(item: Item): Fragment {
@@ -214,7 +214,7 @@ const USES: Record<string, Draw> = {
214
214
  Glob: pattern,
215
215
  WebFetch: (item) => ({ head: words(str(item, "url"), str(item, "prompt")), body: EMPTY }),
216
216
  WebSearch: (item) => ({ head: field(item, "query", "query=") ?? "", body: EMPTY }),
217
- // The brief itself is the `message:sub:out` beside this call, so the call
217
+ // The brief itself is the `message.sub.out` beside this call, so the call
218
218
  // says which agent was started and leaves the words to the message.
219
219
  Agent: (item) => ({
220
220
  head: words(
@@ -11,8 +11,8 @@ import type { Item } from "./item.ts";
11
11
  * shape a person actually reaches for — every tool but the reads, the whole
12
12
  * conversation but not the thinking.
13
13
  *
14
- * A prefix matches at segment boundaries, so `tool` reaches `tool:Bash` and
15
- * `message:user` reaches both directions, while `notice` never reaches a type
14
+ * A prefix matches at segment boundaries, so `tool` reaches `tool.Bash` and
15
+ * `message.user` reaches both directions, while `notice` never reaches a type
16
16
  * that merely starts with those letters. */
17
17
 
18
18
  /** What a dump keeps when nobody said: every family there is, less the
@@ -29,7 +29,7 @@ const DEFAULT_TYPES = [
29
29
  "notice",
30
30
  "system",
31
31
  "hook",
32
- "-system:attachment",
32
+ "-system.attachment",
33
33
  ];
34
34
 
35
35
  export interface Selection {
@@ -55,7 +55,7 @@ export interface Ask {
55
55
  }
56
56
 
57
57
  const NO_THINKING = ["-thinking"];
58
- const NO_AGENT = ["-message:sub", "-tool:Agent"];
58
+ const NO_AGENT = ["-message.sub", "-tool.Agent"];
59
59
 
60
60
  export function selection(ask: Ask, presets: readonly DumpPreset[]): Selection {
61
61
  const asked = [...(ask.preset?.opts.types ?? []), ...(ask.types ?? [])];
@@ -94,7 +94,7 @@ function decide(type: string, elements: readonly string[]): boolean {
94
94
  }
95
95
 
96
96
  function reaches(name: string, type: string): boolean {
97
- return type === name || type.startsWith(`${name}:`);
97
+ return type === name || type.startsWith(`${name}.`);
98
98
  }
99
99
 
100
100
  /** A preset named in a selection, put where it was named.
@@ -4,7 +4,7 @@ import { NO_FACTS, type TranscriptFacts, TranscriptFold } from "./fold.ts";
4
4
  import { Classification, type Item, positioned } from "./items/index.ts";
5
5
  import { type Appended, TranscriptTail } from "./tail.ts";
6
6
 
7
- /** How many items a subscription to `transcript_items:<sid>` opens with.
7
+ /** How many items a subscription to `transcript.items:<sid>` opens with.
8
8
  *
9
9
  * The tail of the same megabyte the fold is seeded from, bounded by a count
10
10
  * because that read is bounded by bytes: a file of many small records would
@@ -17,7 +17,7 @@ export const ITEMS_SNAPSHOT = 200;
17
17
  /** Which of the two topics a name is. Both are fed by one tail, so the
18
18
  * resource is entered by either name and answers each in its own vocabulary. */
19
19
  function isItems(topic: string): boolean {
20
- return topic.startsWith("transcript_items:");
20
+ return topic.startsWith("transcript.items:");
21
21
  }
22
22
 
23
23
  export interface TranscriptsDeps {
@@ -183,7 +183,7 @@ export class Transcripts implements UpstreamResource {
183
183
  // A record still being written was not read, so there is nothing to say
184
184
  // about it yet; a chunk whose records were all the interface's own
185
185
  // bookkeeping says nothing either.
186
- if (items.length > 0) this.deps.publish(`transcript_items:${sid}`, { sid, items });
186
+ if (items.length > 0) this.deps.publish(`transcript.items:${sid}`, { sid, items });
187
187
  if (changed) this.deps.onFacts(sid);
188
188
  }
189
189
 
@@ -121,7 +121,7 @@ function read(line: string, id: string, expected: number): TranslateResult[] {
121
121
 
122
122
  export function translateHandlers(translate: Translate) {
123
123
  return {
124
- translate_run: (input: HandlerInput): Promise<TranslateRunResult> =>
124
+ "translate.run": (input: HandlerInput): Promise<TranslateRunResult> =>
125
125
  translate.run(input.args as unknown as TranslateRunArgs),
126
126
  };
127
127
  }
@@ -1,4 +1,4 @@
1
- import { MAX_FRAME_BYTES, type OpName, type Role, type Sid } from "@ccmsg/protocol";
1
+ import { helloRole, isHelloOp, MAX_FRAME_BYTES, type Sid } from "@ccmsg/protocol";
2
2
  import { type DispatchResult, failure, type Requester } from "../dispatch/index.ts";
3
3
  import type { Conn } from "./conn.ts";
4
4
 
@@ -8,10 +8,9 @@ export interface FrameHandler {
8
8
  (frame: unknown, conn: Requester): Promise<DispatchResult>;
9
9
  }
10
10
 
11
- /** The op whose reply settles the connection's identity. Transport knows this
12
- * one op name because binding the identity is its job (daemon-v2 §3.1); every
13
- * other op is opaque to it. */
14
- const HELLO = "hello" satisfies OpName;
11
+ /** The ops whose reply settles the connection's identity. Transport knows these
12
+ * three op names because binding the identity is its job (daemon-v2 §3.1);
13
+ * every other op is opaque to it. */
15
14
 
16
15
  /** Drive one connection: a line in, a frame answered on the same connection.
17
16
  *
@@ -67,19 +66,22 @@ export function createDriver(conn: Conn, handle: FrameHandler) {
67
66
  };
68
67
  }
69
68
 
70
- /** Bind role and sid at the moment the `hello` reply goes out.
69
+ /** Bind role and sid at the moment a greeting's reply goes out.
71
70
  *
72
- * The frame is safe to read because dispatch only answers `reply` after the
73
- * op's own schema accepted it, so `role` is a role and `sid` required of a
74
- * session and absent otherwise is a sid. */
71
+ * The role is read from the op that carried the greeting rather than from a
72
+ * field of it: there is one op per role, so the name is the only place the
73
+ * role is said. The frame is safe to read because dispatch only answers
74
+ * `reply` after the op's own schema accepted it, so `sid` — asked for by
75
+ * `hello.session` and by neither of the others — is a sid. */
75
76
  function settleIfHello(conn: Conn, frame: unknown, result: DispatchResult): void {
76
77
  if (result.kind !== "reply") return;
77
78
  const fields = frame as Record<string, unknown>;
78
- if (fields["op"] !== HELLO) return;
79
+ const op = fields["op"];
80
+ if (typeof op !== "string" || !isHelloOp(op)) return;
79
81
  const sid = fields["sid"];
80
82
  conn.settle({
81
83
  state: "settled",
82
- role: fields["role"] as Role,
84
+ role: helloRole(op),
83
85
  ...(typeof sid === "string" ? { sid: sid as Sid } : {}),
84
86
  });
85
87
  }
@@ -95,13 +95,13 @@ export function gatewayHandlers(setup: GatewaySetup, fetcher?: typeof fetch) {
95
95
  ...(usageUrl === undefined
96
96
  ? {}
97
97
  : {
98
- llm_usage_read: (input: HandlerInput): Promise<LlmUsageReadResult> =>
98
+ "llm.usage.read": (input: HandlerInput): Promise<LlmUsageReadResult> =>
99
99
  readUsage({ url: usageUrl, ...call }, input.args as unknown as LlmUsageReadArgs),
100
100
  }),
101
101
  ...(statsUrl === undefined
102
102
  ? {}
103
103
  : {
104
- llm_stats_read: (input: HandlerInput): Promise<LlmStatsReadResult> =>
104
+ "llm.stats.read": (input: HandlerInput): Promise<LlmStatsReadResult> =>
105
105
  readStats({ url: statsUrl, ...call }, input.args as unknown as LlmStatsReadArgs),
106
106
  }),
107
107
  };
@@ -165,7 +165,7 @@ export class Gateway {
165
165
  };
166
166
  }
167
167
 
168
- /** The resource behind `llm_status`. A stand-in that states nothing when the
168
+ /** The resource behind `llm.status`. A stand-in that states nothing when the
169
169
  * gateway's address is not configured — the topic's capability is absent
170
170
  * then, so nothing reaches it, and the attachment stays unconditional. */
171
171
  get statusResource(): UpstreamResource {
@@ -156,7 +156,7 @@ export class LlmRequests implements UpstreamResource {
156
156
  }
157
157
 
158
158
  private publish(): void {
159
- this.deps.publish("llm_requests", this.entries());
159
+ this.deps.publish("llm.requests", this.entries());
160
160
  }
161
161
 
162
162
  /** Note the session was seen, and say what that moved.
@@ -141,7 +141,7 @@ export class LlmStatus implements UpstreamResource {
141
141
  return;
142
142
  }
143
143
  this.#report = report;
144
- this.deps.publish("llm_status", report);
144
+ this.deps.publish("llm.status", report);
145
145
  }
146
146
  }
147
147
 
package/src/version.ts CHANGED
@@ -3,6 +3,6 @@ import { version } from "../package.json";
3
3
  /** This build of ccmsg, taken from the package it is published as so that the
4
4
  * binary, the CLI and the plugin it installs never name three versions.
5
5
  *
6
- * It is also the daemon build `hello` and `instance_ping` report, which is what
6
+ * It is also the daemon build `hello` and `instance.ping` report, which is what
7
7
  * makes "what a client is told" and "what was released" the same number. */
8
8
  export const VERSION: string = version;