@otto-code/protocol 0.6.1 → 0.6.3

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/messages.js CHANGED
@@ -177,6 +177,23 @@ const AgentPersonalityVoiceSchema = z
177
177
  name: z.string().min(1),
178
178
  })
179
179
  .passthrough();
180
+ // The three Visualizer lifecycle moments a personality voice-cue line can
181
+ // belong to. Protocol owns this vocabulary — the daemon's cue generator, the
182
+ // personality editor, and the Visualizer playback hook all import it from here.
183
+ export const CUE_MOMENTS = ["join", "thinking", "done"];
184
+ // Pre-generated (and user-editable) spoken "voice cue" lines for the personality
185
+ // — a few short variations for each of three Visualizer moments (its node joins
186
+ // the graph, first starts thinking, completes). Stored on the personality so
187
+ // they're deterministic and hand-tunable in the editor; the Visualizer reads
188
+ // them directly (no runtime generation). All groups optional/loose — a
189
+ // personality may have none, or only some. See docs/visualizer.md "Voice cues".
190
+ const AgentPersonalityVoiceCuesSchema = z
191
+ .object({
192
+ join: z.array(z.string()).optional(),
193
+ thinking: z.array(z.string()).optional(),
194
+ done: z.array(z.string()).optional(),
195
+ })
196
+ .passthrough();
180
197
  // A named, reusable agent template stored per-host. `id` is the stable identity
181
198
  // everything binds to; `name` is a freely-renamable label. Effort and roles are
182
199
  // plain strings on the wire (like speech engine/model ids) so the daemon can
@@ -199,6 +216,7 @@ export const AgentPersonalitySchema = z
199
216
  roles: z.array(z.string().min(1)).optional(),
200
217
  spinner: AgentPersonalitySpinnerSchema.optional(),
201
218
  voice: AgentPersonalityVoiceSchema.optional(),
219
+ voiceCues: AgentPersonalityVoiceCuesSchema.optional(),
202
220
  })
203
221
  .passthrough();
204
222
  const MutableAgentPersonalitiesConfigSchema = z
@@ -367,6 +385,22 @@ const AgentProviderNoticeSchema = z.discriminatedUnion("type", [
367
385
  z.object({ type: z.literal("warning"), message: z.string() }),
368
386
  z.object({ type: z.literal("error"), message: z.string() }),
369
387
  ]);
388
+ // Provider-reported plan rate-limit status (e.g. Claude claude.ai plan
389
+ // windows), pushed on the agent stream when it changes. Presentation-only:
390
+ // the app decides whether to show it (rateLimitWarningsEnabled setting).
391
+ export const AgentRateLimitInfoSchema = z.object({
392
+ status: z.enum(["allowed", "warning", "rejected"]),
393
+ // Percentage of the limit window used, 0-100. Absent when the provider
394
+ // does not report it (Claude only includes it near the limit).
395
+ utilizationPercent: z.number().optional(),
396
+ // Provider-reported window identifier, e.g. "five_hour" | "seven_day".
397
+ // Open set — display code falls back to a generic label for unknown values.
398
+ limitType: z.string().optional(),
399
+ // ISO 8601 timestamp when the window resets.
400
+ resetsAt: z.string().optional(),
401
+ // True when usage is currently drawing from overage/extra usage credits.
402
+ isUsingOverage: z.boolean().optional(),
403
+ });
370
404
  export const AgentFeatureToggleSchema = z.object({
371
405
  type: z.literal("toggle"),
372
406
  id: z.string(),
@@ -436,6 +470,13 @@ const AgentCapabilityFlagsSchema = z
436
470
  supportsRewindBoth: z.boolean().optional().default(false),
437
471
  })
438
472
  .catchall(z.boolean());
473
+ const ContextCompositionSchema = z.object({
474
+ systemPrompt: z.number().optional(),
475
+ userMessages: z.number().optional(),
476
+ toolResults: z.number().optional(),
477
+ reasoning: z.number().optional(),
478
+ subagentResults: z.number().optional(),
479
+ });
439
480
  const AgentUsageSchema = z.object({
440
481
  inputTokens: z.number().optional(),
441
482
  cachedInputTokens: z.number().optional(),
@@ -443,6 +484,9 @@ const AgentUsageSchema = z.object({
443
484
  totalCostUsd: z.number().optional(),
444
485
  contextWindowMaxTokens: z.number().optional(),
445
486
  contextWindowUsedTokens: z.number().optional(),
487
+ // Provider-graded context breakdown for the visualizer ring/bar; absent ⇒
488
+ // occupancy only (pre-composition behavior). See ContextComposition.
489
+ contextComposition: ContextCompositionSchema.optional(),
446
490
  });
447
491
  const AgentSessionConfigSchema = z.object({
448
492
  provider: AgentProviderSchema,
@@ -754,6 +798,22 @@ export const AgentStreamEventPayloadSchema = z.discriminatedUnion("type", [
754
798
  })
755
799
  .optional(),
756
800
  }),
801
+ // Predicted next-user-prompt suggestion emitted after a turn. Transient: the
802
+ // app shows the latest as composer ghost text (Tab to accept) and clears it on
803
+ // the next turn_started. COMPAT(promptSuggestions): added in v0.6.3.
804
+ z.object({
805
+ type: z.literal("prompt_suggestion"),
806
+ provider: AgentProviderSchema,
807
+ suggestion: z.string(),
808
+ }),
809
+ // Provider-reported plan rate-limit status (Claude claude.ai plan windows).
810
+ // Transient: the app shows a suppressible warning strip near the composer.
811
+ // Deduped provider-side. COMPAT(rateLimitEvents): added in v0.6.3.
812
+ z.object({
813
+ type: z.literal("rate_limit_updated"),
814
+ provider: AgentProviderSchema,
815
+ info: AgentRateLimitInfoSchema,
816
+ }),
757
817
  ]);
758
818
  const AgentPersistenceHandleSchema = z
759
819
  .object({
@@ -1229,6 +1289,32 @@ export const SpeechTtsPreviewRequestSchema = z.object({
1229
1289
  .passthrough()
1230
1290
  .optional(),
1231
1291
  });
1292
+ // COMPAT(visualizerVoiceCues): added in v0.6.3; gate lives in
1293
+ // features.visualizerVoiceCues. Author short spoken "cue" lines for a
1294
+ // personality — a handful of variations each for three Visualizer moments
1295
+ // (join / thinking / done) — via the Writer mini-task chain, flavored by the
1296
+ // persona's `name` + `prompt`. The persona is passed inline (not a stored id)
1297
+ // so the personality editor can generate for an unsaved draft too; the result
1298
+ // is stored on the personality (`voiceCues`) and edited there, so this is an
1299
+ // editor-time action, not a runtime one. `cwd` scopes provider resolution to a
1300
+ // workspace; omitted falls back to any resolvable one.
1301
+ export const VisualizerVoiceCuesGenerateRequestSchema = z.object({
1302
+ type: z.literal("visualizer.voiceCues.generate.request"),
1303
+ requestId: z.string(),
1304
+ name: z.string(),
1305
+ prompt: z.string().optional(),
1306
+ cwd: z.string().optional(),
1307
+ // The persona's roles (e.g. "researcher", "coder") so the writer can flavor
1308
+ // the lines to what the agent does. Permissive strings to match the stored
1309
+ // personality shape (forward-compatible with roles this daemon predates).
1310
+ roles: z.array(z.string().min(1)).optional(),
1311
+ // When present, author only this one moment's lines (a focused single-moment
1312
+ // prompt) and return only that group. The editor issues one request per
1313
+ // moment so it can show generation progress and keep the moments distinct.
1314
+ // Omitted → author all three at once (the original all-in-one path, still
1315
+ // used by older clients).
1316
+ moment: z.enum(CUE_MOMENTS).optional(),
1317
+ });
1232
1318
  export const AgentPersonalitiesGetStatsRequestSchema = z.object({
1233
1319
  type: z.literal("agentPersonalities.get_stats.request"),
1234
1320
  requestId: z.string(),
@@ -1620,11 +1706,22 @@ const SuggestedTaskActionResponsePayloadSchema = z.object({
1620
1706
  error: z.string().nullable(),
1621
1707
  });
1622
1708
  // Start one or more suggested tasks, applying the SAME mode to each — no
1623
- // combining. `worktree` spins a new worktree-backed workspace per task, `local`
1624
- // a new session in the same repo/cwd per task, `in_session` steers the parent
1625
- // agent with the task prompt. The daemon resolves the parent agent's brain
1626
- // (provider/model/personality) so a started task continues the suggesting agent.
1627
- export const TasksSuggestedStartModeSchema = z.enum(["worktree", "local", "in_session"]);
1709
+ // combining. Four modes, only `subagent` links the new agent to the parent:
1710
+ // - `new_chat`: a fresh independent agent in its own tab, same repo/cwd, NO
1711
+ // parent link survives the parent's cancel/archive.
1712
+ // - `subagent`: a bound child agent that shows in the parent's Subagents
1713
+ // track and archive-cascades with it.
1714
+ // - `worktree`: an independent agent on a new git worktree (auto branch-off),
1715
+ // isolated workspace — also unlinked from the parent.
1716
+ // - `in_session`: steers the parent agent with the task prompt (no new agent).
1717
+ // The daemon resolves the parent agent's brain (provider/model/personality) so a
1718
+ // started task continues the suggesting agent.
1719
+ export const TasksSuggestedStartModeSchema = z.enum([
1720
+ "new_chat",
1721
+ "subagent",
1722
+ "worktree",
1723
+ "in_session",
1724
+ ]);
1628
1725
  export const TasksSuggestedStartRequestMessageSchema = z.object({
1629
1726
  type: z.literal("tasks.suggested.start.request"),
1630
1727
  parentAgentId: z.string(),
@@ -2608,6 +2705,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
2608
2705
  SetDaemonConfigRequestMessageSchema,
2609
2706
  SpeechSettingsGetOptionsRequestSchema,
2610
2707
  SpeechTtsPreviewRequestSchema,
2708
+ VisualizerVoiceCuesGenerateRequestSchema,
2611
2709
  AgentPersonalitiesGetStatsRequestSchema,
2612
2710
  ReadProjectConfigRequestMessageSchema,
2613
2711
  WriteProjectConfigRequestMessageSchema,
@@ -2956,6 +3054,8 @@ export const ServerInfoStatusPayloadSchema = z
2956
3054
  agentPersonalities: z.boolean().optional(),
2957
3055
  // COMPAT(ttsPreview): added in v0.4.7, drop the gate when daemon floor >= v0.4.7.
2958
3056
  ttsPreview: z.boolean().optional(),
3057
+ // COMPAT(visualizerVoiceCues): added in v0.6.3, drop the gate when daemon floor >= v0.6.3.
3058
+ visualizerVoiceCues: z.boolean().optional(),
2959
3059
  // COMPAT(setAgentPersonality): added in v0.5.0, drop the gate when daemon floor >= v0.5.0.
2960
3060
  setAgentPersonality: z.boolean().optional(),
2961
3061
  // COMPAT(checkoutGitCommit): added in v0.5.1, drop the gate when daemon floor >= v0.5.1.
@@ -2984,6 +3084,15 @@ export const ServerInfoStatusPayloadSchema = z
2984
3084
  // permissions). The client gates this behind an "edit anyway" warning;
2985
3085
  // an old daemon leaves the flag unset and out-of-project files are not offered.
2986
3086
  fileOutsideWorkspace: z.boolean().optional(),
3087
+ // COMPAT(promptSuggestions): added in v0.6.3, drop the gate when daemon floor >= v0.6.3.
3088
+ // Set when the daemon emits agent_stream `prompt_suggestion` events (native
3089
+ // Claude next-prompt predictions). The client gates the Settings toggle on
3090
+ // this; suggestions already degrade silently on an old daemon (no event).
3091
+ promptSuggestions: z.boolean().optional(),
3092
+ // COMPAT(rateLimitEvents): added in v0.6.3, drop the gate when daemon floor >= v0.6.3.
3093
+ // Set when the daemon emits agent_stream `rate_limit_updated` events (Claude
3094
+ // plan rate-limit status). Warnings degrade silently on an old daemon (no event).
3095
+ rateLimitEvents: z.boolean().optional(),
2987
3096
  })
2988
3097
  .optional(),
2989
3098
  })
@@ -3666,6 +3775,18 @@ export const SpeechTtsPreviewResponseSchema = z.object({
3666
3775
  })
3667
3776
  .passthrough(),
3668
3777
  });
3778
+ export const VisualizerVoiceCuesGenerateResponseSchema = z.object({
3779
+ type: z.literal("visualizer.voiceCues.generate.response"),
3780
+ payload: z
3781
+ .object({
3782
+ requestId: z.string(),
3783
+ // Absent when generation failed (see error) or no writer/provider
3784
+ // resolves on this host. Reuses the stored-cues shape.
3785
+ cues: AgentPersonalityVoiceCuesSchema.optional(),
3786
+ error: z.string().optional(),
3787
+ })
3788
+ .passthrough(),
3789
+ });
3669
3790
  export const AgentPersonalitiesGetStatsResponseSchema = z.object({
3670
3791
  type: z.literal("agentPersonalities.get_stats.response"),
3671
3792
  payload: z
@@ -5141,6 +5262,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
5141
5262
  SetDaemonConfigResponseMessageSchema,
5142
5263
  SpeechSettingsGetOptionsResponseSchema,
5143
5264
  SpeechTtsPreviewResponseSchema,
5265
+ VisualizerVoiceCuesGenerateResponseSchema,
5144
5266
  AgentPersonalitiesGetStatsResponseSchema,
5145
5267
  ReadProjectConfigResponseMessageSchema,
5146
5268
  WriteProjectConfigResponseMessageSchema,
@@ -0,0 +1,26 @@
1
+ /** Hard cap so no provider summary can produce a wall-of-text label. */
2
+ export declare const OBSERVED_SUBAGENT_TITLE_MAX = 60;
3
+ export declare function normalizeObservedTitleSource(value: string | undefined | null): string | null;
4
+ export declare function normalizeObservedSubagentType(value: string | undefined | null): string | null;
5
+ /**
6
+ * Derive the frozen row name for an observed subagent. Prefer the stable
7
+ * `subAgentType` (e.g. "code-explorer") over the description — a
8
+ * `task_progress` description is the ever-changing AI summary, which must
9
+ * never become the label. Generic catch-all types ("general-purpose") are
10
+ * skipped in favor of the description. Callers freeze the result at the first
11
+ * named update.
12
+ */
13
+ export declare function deriveObservedSubagentTitle(update: {
14
+ subAgentType?: string;
15
+ description?: string;
16
+ }): string;
17
+ /**
18
+ * True when this update carries a real name source we can freeze the title on.
19
+ * A generic catch-all type alone doesn't count — freezing "general-purpose"
20
+ * would lock out the description a later update may carry.
21
+ */
22
+ export declare function observedUpdateHasTitleSource(update: {
23
+ subAgentType?: string;
24
+ description?: string;
25
+ }): boolean;
26
+ //# sourceMappingURL=observed-subagent-title.d.ts.map
@@ -0,0 +1,63 @@
1
+ // Shared naming rule for provider-managed (observed) subagents. This is the
2
+ // single source of truth for how an observed subagent is titled — the daemon
3
+ // uses it to freeze the track-row label (agent-projections.ts), and the app's
4
+ // visualizer uses it so page-side child labels (subagent_dispatch/return
5
+ // particles ride an edge keyed by child NAME) resolve to exactly the same
6
+ // string as the child node the daemon-titled agent row spawned. If the two
7
+ // ever diverge, the dispatch/return visuals silently stop rendering.
8
+ // See docs/agent-lifecycle.md (Item 4) and docs/visualizer.md.
9
+ /** Hard cap so no provider summary can produce a wall-of-text label. */
10
+ export const OBSERVED_SUBAGENT_TITLE_MAX = 60;
11
+ /**
12
+ * Catch-all subagent types that make lousy row labels — Claude's default Task
13
+ * runs as "general-purpose". These never become the title; the task
14
+ * description names the row instead (user-locked).
15
+ */
16
+ const GENERIC_OBSERVED_SUBAGENT_TYPES = new Set([
17
+ "general-purpose",
18
+ "general",
19
+ "task",
20
+ "agent",
21
+ "subagent",
22
+ ]);
23
+ export function normalizeObservedTitleSource(value) {
24
+ if (typeof value !== "string") {
25
+ return null;
26
+ }
27
+ const collapsed = value.replace(/\s+/g, " ").trim();
28
+ return collapsed.length > 0 ? collapsed : null;
29
+ }
30
+ export function normalizeObservedSubagentType(value) {
31
+ const normalized = normalizeObservedTitleSource(value);
32
+ if (normalized === null || GENERIC_OBSERVED_SUBAGENT_TYPES.has(normalized.toLowerCase())) {
33
+ return null;
34
+ }
35
+ return normalized;
36
+ }
37
+ /**
38
+ * Derive the frozen row name for an observed subagent. Prefer the stable
39
+ * `subAgentType` (e.g. "code-explorer") over the description — a
40
+ * `task_progress` description is the ever-changing AI summary, which must
41
+ * never become the label. Generic catch-all types ("general-purpose") are
42
+ * skipped in favor of the description. Callers freeze the result at the first
43
+ * named update.
44
+ */
45
+ export function deriveObservedSubagentTitle(update) {
46
+ const base = normalizeObservedSubagentType(update.subAgentType) ??
47
+ normalizeObservedTitleSource(update.description) ??
48
+ "Subagent";
49
+ if (base.length <= OBSERVED_SUBAGENT_TITLE_MAX) {
50
+ return base;
51
+ }
52
+ return `${base.slice(0, OBSERVED_SUBAGENT_TITLE_MAX - 1).trimEnd()}…`;
53
+ }
54
+ /**
55
+ * True when this update carries a real name source we can freeze the title on.
56
+ * A generic catch-all type alone doesn't count — freezing "general-purpose"
57
+ * would lock out the description a later update may carry.
58
+ */
59
+ export function observedUpdateHasTitleSource(update) {
60
+ return (normalizeObservedSubagentType(update.subAgentType) !== null ||
61
+ normalizeObservedTitleSource(update.description) !== null);
62
+ }
63
+ //# sourceMappingURL=observed-subagent-title.js.map
@@ -7,5 +7,14 @@ export interface ToolCallDisplayModel {
7
7
  summary?: string;
8
8
  errorText?: string;
9
9
  }
10
+ /**
11
+ * Friendly display name for a bare tool identifier, used at every surface that
12
+ * shows a tool/action name without a full timeline item to run through
13
+ * {@link buildToolCallDisplayModel} (the visualizer's action labels, sub-agent
14
+ * activity rows). Strips the MCP/Otto namespace, consults the known-tool
15
+ * registry, then title-cases as a fallback — so "mcp__otto__spawn_task",
16
+ * "otto.spawn_task", and a bare "spawn_task" all render as "Spawn Task".
17
+ */
18
+ export declare function getToolDisplayName(name: string): string;
10
19
  export declare function buildToolCallDisplayModel(input: ToolCallDisplayInput): ToolCallDisplayModel;
11
20
  //# sourceMappingURL=tool-call-display.d.ts.map
@@ -1,4 +1,4 @@
1
- import { getOttoToolLeafName, isOttoToolName } from "./tool-name-normalization.js";
1
+ import { getMcpToolLeafName, getOttoToolLeafName } from "./tool-name-normalization.js";
2
2
  import { stripCwdPrefix } from "./path-utils.js";
3
3
  function isRecord(value) {
4
4
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -6,26 +6,71 @@ function isRecord(value) {
6
6
  function readString(value) {
7
7
  return typeof value === "string" && value.length > 0 ? value : undefined;
8
8
  }
9
+ // Curated display names — the registry of tools we explicitly "know about".
10
+ // Keyed by the lowercased leaf name (transport namespace already stripped).
11
+ //
12
+ // Only tools whose bare id does NOT title-case cleanly on its own need an entry
13
+ // here: lowercase compound names ("websearch") a splitter can't segment, or
14
+ // ones we want to word deliberately. Well-formed snake_case ("spawn_task") and
15
+ // camelCase ("WebSearch") names are handled by the algorithmic humanizer below
16
+ // and do NOT need listing.
17
+ //
18
+ // Anything not here falls through to the humanizer and still renders readably —
19
+ // so an unmapped tool shows up looking slightly generic (e.g. a stray provider
20
+ // tool), which is the cue to add it here. Extend this map to teach Otto a tool.
21
+ const KNOWN_TOOL_DISPLAY_NAMES = {
22
+ websearch: "Web Search",
23
+ web_search: "Web Search",
24
+ webfetch: "Web Fetch",
25
+ web_fetch: "Web Fetch",
26
+ todowrite: "Update Todos",
27
+ todoread: "Read Todos",
28
+ multiedit: "Multi Edit",
29
+ notebookedit: "Edit Notebook",
30
+ notebookread: "Read Notebook",
31
+ bashoutput: "Bash Output",
32
+ killshell: "Kill Shell",
33
+ exitplanmode: "Exit Plan Mode",
34
+ applypatch: "Apply Patch",
35
+ ls: "List Files",
36
+ };
37
+ // Split camelCase / PascalCase and separator-delimited identifiers into words,
38
+ // then Title-Case them: "WebSearch" -> "Web Search", "spawn_task" -> "Spawn
39
+ // Task", "HTTPServer" -> "HTTP Server".
40
+ function titleCaseToolId(value) {
41
+ return value
42
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
43
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
44
+ .replace(/[._-]+/g, " ")
45
+ .split(" ")
46
+ .filter((segment) => segment.length > 0)
47
+ .map((segment) => `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1)}`)
48
+ .join(" ");
49
+ }
9
50
  function humanizeToolName(name) {
10
51
  const trimmed = name.trim();
11
52
  if (!trimmed) {
12
53
  return name;
13
54
  }
14
- if (isOttoToolName(trimmed)) {
15
- const leaf = getOttoToolLeafName(trimmed);
16
- if (leaf) {
17
- return humanizeToolName(leaf);
18
- }
19
- }
20
- if (/[:./]/.test(trimmed) || trimmed.includes("__")) {
21
- return trimmed;
55
+ // Strip the transport namespace first ("mcp__otto__spawn_task" ->
56
+ // "spawn_task", "otto.list_agents" -> "list_agents") so both the known-tool
57
+ // lookup and the fallback operate on the bare tool id.
58
+ const leaf = getMcpToolLeafName(trimmed) ?? getOttoToolLeafName(trimmed);
59
+ if (leaf) {
60
+ return humanizeToolName(leaf);
22
61
  }
23
- return trimmed
24
- .replace(/[._-]+/g, " ")
25
- .split(" ")
26
- .filter((segment) => segment.length > 0)
27
- .map((segment) => `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1)}`)
28
- .join(" ");
62
+ return KNOWN_TOOL_DISPLAY_NAMES[trimmed.toLowerCase()] ?? titleCaseToolId(trimmed);
63
+ }
64
+ /**
65
+ * Friendly display name for a bare tool identifier, used at every surface that
66
+ * shows a tool/action name without a full timeline item to run through
67
+ * {@link buildToolCallDisplayModel} (the visualizer's action labels, sub-agent
68
+ * activity rows). Strips the MCP/Otto namespace, consults the known-tool
69
+ * registry, then title-cases as a fallback — so "mcp__otto__spawn_task",
70
+ * "otto.spawn_task", and a bare "spawn_task" all render as "Spawn Task".
71
+ */
72
+ export function getToolDisplayName(name) {
73
+ return humanizeToolName(name);
29
74
  }
30
75
  function formatErrorText(error) {
31
76
  if (error === null || error === undefined) {
@@ -5,5 +5,21 @@ export declare function isSpeakToolName(name: string): boolean;
5
5
  export declare function isLikelyNamespacedToolName(name: string): boolean;
6
6
  export declare function isOttoToolName(name: string): boolean;
7
7
  export declare function getOttoToolLeafName(name: string): string | null;
8
+ /**
9
+ * Strip a leading MCP namespace for DISPLAY, returning the bare tool id.
10
+ *
11
+ * Tools hosted over MCP arrive namespaced as `mcp__<server>__<tool>` (Claude
12
+ * Code format). The `mcp__<server>__` part is transport plumbing that means
13
+ * nothing to a reader — "Create Issue", not "mcp__linear__create_issue". This
14
+ * generalizes {@link getOttoToolLeafName} to ANY server so every MCP tool reads
15
+ * cleanly, not just Otto's own.
16
+ *
17
+ * Returns `null` when the name carries no `mcp__…__` namespace, so callers keep
18
+ * plain tool names (`Read`, `bash`) and dotted/other forms untouched. Case is
19
+ * preserved — MCP tool ids are already snake_case and callers humanize after.
20
+ * `speak` is never treated as namespaced (it renders as a chat bubble, not an
21
+ * action row).
22
+ */
23
+ export declare function getMcpToolLeafName(name: string): string | null;
8
24
  export declare function isLikelyExternalToolName(name: string): boolean;
9
25
  //# sourceMappingURL=tool-name-normalization.d.ts.map
@@ -69,6 +69,32 @@ export function getOttoToolLeafName(name) {
69
69
  }
70
70
  return null;
71
71
  }
72
+ /**
73
+ * Strip a leading MCP namespace for DISPLAY, returning the bare tool id.
74
+ *
75
+ * Tools hosted over MCP arrive namespaced as `mcp__<server>__<tool>` (Claude
76
+ * Code format). The `mcp__<server>__` part is transport plumbing that means
77
+ * nothing to a reader — "Create Issue", not "mcp__linear__create_issue". This
78
+ * generalizes {@link getOttoToolLeafName} to ANY server so every MCP tool reads
79
+ * cleanly, not just Otto's own.
80
+ *
81
+ * Returns `null` when the name carries no `mcp__…__` namespace, so callers keep
82
+ * plain tool names (`Read`, `bash`) and dotted/other forms untouched. Case is
83
+ * preserved — MCP tool ids are already snake_case and callers humanize after.
84
+ * `speak` is never treated as namespaced (it renders as a chat bubble, not an
85
+ * action row).
86
+ */
87
+ export function getMcpToolLeafName(name) {
88
+ const trimmed = name.trim();
89
+ if (!trimmed || isSpeakToolName(trimmed) || !trimmed.includes("__")) {
90
+ return null;
91
+ }
92
+ const segments = trimmed.split("__").filter((segment) => segment.length > 0);
93
+ if (segments.length >= 3 && segments[0].toLowerCase() === "mcp") {
94
+ return segments.slice(2).join("__");
95
+ }
96
+ return null;
97
+ }
72
98
  export function isLikelyExternalToolName(name) {
73
99
  const normalized = normalizeToolName(name);
74
100
  if (!normalized) {
@@ -183,6 +183,33 @@ export declare const WSOutboundMessageSchema: {
183
183
  args: import("zod").ZodObject<{
184
184
  browserId: import("zod").ZodString;
185
185
  }, import("zod/v4/core").$strict>;
186
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
187
+ command: import("zod").ZodLiteral<"focus_tab">;
188
+ args: import("zod").ZodObject<{
189
+ browserId: import("zod").ZodString;
190
+ }, import("zod/v4/core").$strict>;
191
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
192
+ command: import("zod").ZodLiteral<"page_text">;
193
+ args: import("zod").ZodObject<{
194
+ browserId: import("zod").ZodString;
195
+ maxChars: import("zod").ZodDefault<import("zod").ZodNumber>;
196
+ }, import("zod/v4/core").$strict>;
197
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
198
+ command: import("zod").ZodLiteral<"set_color_scheme">;
199
+ args: import("zod").ZodObject<{
200
+ browserId: import("zod").ZodString;
201
+ colorScheme: import("zod").ZodEnum<{
202
+ auto: "auto";
203
+ light: "light";
204
+ dark: "dark";
205
+ }>;
206
+ }, import("zod/v4/core").$strict>;
207
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
208
+ command: import("zod").ZodLiteral<"screenshot_element">;
209
+ args: import("zod").ZodObject<{
210
+ browserId: import("zod").ZodString;
211
+ ref: import("zod").ZodString;
212
+ }, import("zod/v4/core").$strict>;
186
213
  }, import("zod/v4/core").$strip>], "command">;
187
214
  }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
188
215
  type: import("zod").ZodLiteral<"activity_log">;
@@ -1073,6 +1100,24 @@ export declare const WSOutboundMessageSchema: {
1073
1100
  }>;
1074
1101
  }, import("zod/v4/core").$strip>;
1075
1102
  }, import("zod/v4/core").$strip>>;
1103
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
1104
+ type: import("zod").ZodLiteral<"prompt_suggestion">;
1105
+ provider: import("zod").ZodString;
1106
+ suggestion: import("zod").ZodString;
1107
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
1108
+ type: import("zod").ZodLiteral<"rate_limit_updated">;
1109
+ provider: import("zod").ZodString;
1110
+ info: import("zod").ZodObject<{
1111
+ status: import("zod").ZodEnum<{
1112
+ warning: "warning";
1113
+ allowed: "allowed";
1114
+ rejected: "rejected";
1115
+ }>;
1116
+ utilizationPercent: import("zod").ZodOptional<import("zod").ZodNumber>;
1117
+ limitType: import("zod").ZodOptional<import("zod").ZodString>;
1118
+ resetsAt: import("zod").ZodOptional<import("zod").ZodString>;
1119
+ isUsingOverage: import("zod").ZodOptional<import("zod").ZodBoolean>;
1120
+ }, import("zod/v4/core").$strip>;
1076
1121
  }, import("zod/v4/core").$strip>], "type">;
1077
1122
  timestamp: import("zod").ZodString;
1078
1123
  seq: import("zod").ZodOptional<import("zod").ZodNumber>;
@@ -3446,6 +3491,11 @@ export declare const WSOutboundMessageSchema: {
3446
3491
  model: import("zod").ZodString;
3447
3492
  name: import("zod").ZodString;
3448
3493
  }, import("zod/v4/core").$loose>>;
3494
+ voiceCues: import("zod").ZodOptional<import("zod").ZodObject<{
3495
+ join: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
3496
+ thinking: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
3497
+ done: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
3498
+ }, import("zod/v4/core").$loose>>;
3449
3499
  }, import("zod/v4/core").$loose>>>;
3450
3500
  }, import("zod/v4/core").$loose>>;
3451
3501
  agentTeams: import("zod").ZodDefault<import("zod").ZodObject<{
@@ -3570,6 +3620,11 @@ export declare const WSOutboundMessageSchema: {
3570
3620
  model: import("zod").ZodString;
3571
3621
  name: import("zod").ZodString;
3572
3622
  }, import("zod/v4/core").$loose>>;
3623
+ voiceCues: import("zod").ZodOptional<import("zod").ZodObject<{
3624
+ join: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
3625
+ thinking: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
3626
+ done: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
3627
+ }, import("zod/v4/core").$loose>>;
3573
3628
  }, import("zod/v4/core").$loose>>>;
3574
3629
  }, import("zod/v4/core").$loose>>;
3575
3630
  agentTeams: import("zod").ZodDefault<import("zod").ZodObject<{
@@ -3637,6 +3692,17 @@ export declare const WSOutboundMessageSchema: {
3637
3692
  format: import("zod").ZodOptional<import("zod").ZodString>;
3638
3693
  error: import("zod").ZodOptional<import("zod").ZodString>;
3639
3694
  }, import("zod/v4/core").$loose>;
3695
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
3696
+ type: import("zod").ZodLiteral<"visualizer.voiceCues.generate.response">;
3697
+ payload: import("zod").ZodObject<{
3698
+ requestId: import("zod").ZodString;
3699
+ cues: import("zod").ZodOptional<import("zod").ZodObject<{
3700
+ join: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
3701
+ thinking: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
3702
+ done: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
3703
+ }, import("zod/v4/core").$loose>>;
3704
+ error: import("zod").ZodOptional<import("zod").ZodString>;
3705
+ }, import("zod/v4/core").$loose>;
3640
3706
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
3641
3707
  type: import("zod").ZodLiteral<"agentPersonalities.get_stats.response">;
3642
3708
  payload: import("zod").ZodObject<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-code/protocol",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Otto shared protocol schemas and wire types",
5
5
  "files": [
6
6
  "dist",