@huynle/hub 0.8.1 → 0.8.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/README.md +32 -1
  3. package/package.json +1 -1
  4. package/packages/claude-code/package.json +1 -1
  5. package/packages/cli/dist/hub-cli.js +116 -53
  6. package/packages/cli/package.json +1 -1
  7. package/packages/cli/src/artifact-jobs-mcp.ts +10 -9
  8. package/packages/cli/src/artifacts-mcp.ts +8 -7
  9. package/packages/cli/src/mcp.ts +4 -3
  10. package/packages/cli/src/profiles-mcp.ts +3 -3
  11. package/packages/cli/src/team-intake-mcp.ts +8 -7
  12. package/packages/cli/src/teams-mcp.ts +2 -1
  13. package/packages/cli/src/tool-args.ts +6 -0
  14. package/packages/cli/src/watchlist.ts +5 -4
  15. package/packages/codex/package.json +1 -1
  16. package/packages/node/package.json +1 -1
  17. package/packages/preset/package.json +1 -1
  18. package/packages/protocol/package.json +1 -1
  19. package/packages/protocol/src/concierge.ts +4 -1
  20. package/packages/protocol/src/embedded-media.ts +59 -0
  21. package/packages/protocol/src/index.ts +92 -7
  22. package/packages/protocol/src/voice-recording.ts +107 -0
  23. package/packages/protocol/src/wire.ts +1 -0
  24. package/packages/runtime-openshell/package.json +1 -1
  25. package/packages/runtime-sdk/package.json +1 -1
  26. package/packages/sdk/package.json +1 -1
  27. package/packages/sdk/src/client.ts +3 -1
  28. package/packages/spoke-sdk/package.json +1 -1
  29. package/packages/spoke-sdk/src/connection.ts +13 -1
  30. package/src/central/agent/acting-routes.ts +3 -2
  31. package/src/central/agent/acting.ts +67 -12
  32. package/src/central/agent/delivery.ts +12 -3
  33. package/src/central/agent/escalation.ts +30 -9
  34. package/src/central/agent/launch-spec.ts +38 -19
  35. package/src/central/agent/routes.ts +14 -1
  36. package/src/central/artifacts/blob-store.ts +6 -1
  37. package/src/central/artifacts/blob-support.ts +3 -1
  38. package/src/central/concierge/board-render.ts +34 -11
  39. package/src/central/concierge/context.ts +3 -1
  40. package/src/central/concierge/index.ts +1 -1
  41. package/src/central/concierge/notifier.ts +2 -1
  42. package/src/central/concierge/prompt.ts +7 -4
  43. package/src/central/config.ts +4 -0
  44. package/src/central/csp.ts +55 -0
  45. package/src/central/http.ts +134 -29
  46. package/src/central/profiles/content.ts +16 -0
  47. package/src/central/recording/clock.ts +76 -0
  48. package/src/central/recording/concierge-taps.ts +104 -0
  49. package/src/central/recording/export.ts +215 -0
  50. package/src/central/recording/index.ts +658 -0
  51. package/src/central/recording/ingest.ts +209 -0
  52. package/src/central/recording/retention.ts +71 -0
  53. package/src/central/recording/routes.ts +281 -0
  54. package/src/central/recording/schema.ts +232 -0
  55. package/src/central/recording/settings.ts +35 -0
  56. package/src/central/recording/store.ts +316 -0
  57. package/src/central/recording/taps.ts +105 -0
  58. package/src/central/recording/zip.ts +138 -0
  59. package/src/central/registry.ts +91 -6
  60. package/src/central/revert-windows.ts +66 -0
  61. package/src/central/spoke-ws.ts +8 -3
  62. package/src/central/teams/staffing.ts +2 -2
  63. package/src/index.ts +26 -14
  64. package/src/spoke/deliver.ts +6 -0
  65. package/src/spoke/fork-revert.ts +94 -9
  66. package/src/spoke/hub-tool.ts +2 -2
  67. package/src/spoke/locations.ts +80 -0
  68. package/src/spoke/profile-apply.ts +24 -14
  69. package/src/spoke/session-context.ts +96 -6
  70. package/src/spoke/spoke.ts +119 -36
  71. package/src/spoke/timeline.ts +30 -3
  72. package/src/spoke/tracker.ts +23 -2
  73. package/src/status.ts +33 -6
  74. package/web/dist/assets/{ArtifactPage-mNzjlP_U.js → ArtifactPage-CSotAWob.js} +1 -1
  75. package/web/dist/assets/{ArtifactPane-CPQcuX7c.js → ArtifactPane-DPHjiWce.js} +1 -1
  76. package/web/dist/assets/{ArtifactWorkbench-NoFC5Sb2.js → ArtifactWorkbench-CkmWnP6L.js} +1 -1
  77. package/web/dist/assets/{Library-LXHsP5Fw.js → Library-DTLEyekn.js} +2 -2
  78. package/web/dist/assets/ProfilesDestination-GriuOpiZ.js +1 -0
  79. package/web/dist/assets/{TeamsPage-DBrO5hoL.js → TeamsPage-C-5lYeeM.js} +1 -1
  80. package/web/dist/assets/Watchtower-DOlPsiIy.js +2 -0
  81. package/web/dist/assets/index-6mdQ7eUY.js +34 -0
  82. package/web/dist/assets/index-C5OmwA1y.css +1 -0
  83. package/web/dist/assets/{live-BDq0AABg.js → live-CDWcyavI.js} +1 -1
  84. package/web/dist/assets/{pane-layout-DWXfDU-1.js → pane-layout-Csk-vPNe.js} +1 -1
  85. package/web/dist/assets/{renderer2d-DAr4IHM8.js → renderer2d-CNFdTDTr.js} +1 -1
  86. package/web/dist/assets/{renderer3d-DWZj595u.js → renderer3d-C45jPvYX.js} +4 -56
  87. package/web/dist/assets/renderer3d-Co5yCKMg.css +1 -0
  88. package/web/dist/assets/{safe-area-JUb6OeI3.js → safe-area-sA9a8vYJ.js} +1 -1
  89. package/web/dist/assets/{silero-worker-C8-8vLVS.js → silero-worker-DhlIHyD_.js} +1 -1
  90. package/web/dist/assets/{source-CDU04kYw.js → source-BFLJQ43_.js} +1 -1
  91. package/web/dist/assets/{ui-Dh1-78Cl.js → ui-Qo2Pd9-O.js} +1 -1
  92. package/web/dist/index.html +5 -16
  93. package/web/dist/theme-boot.js +12 -0
  94. package/web/dist/assets/ProfilesDestination-DhVVcAep.js +0 -1
  95. package/web/dist/assets/Watchtower-DxaJYQp_.js +0 -2
  96. package/web/dist/assets/index-BabrNDB3.css +0 -1
  97. package/web/dist/assets/index-lIOfSYck.js +0 -34
@@ -7,7 +7,7 @@ import { createInterface } from "readline";
7
7
  // packages/cli/package.json
8
8
  var package_default = {
9
9
  name: "@huynle/hub-cli",
10
- version: "0.8.1",
10
+ version: "0.8.3",
11
11
  private: true,
12
12
  description: "The hub CLI (`hub` / `hubctl`) for agents and scripts (internal; bundled into @huynle/hub, not published on its own).",
13
13
  license: "MIT",
@@ -863,7 +863,7 @@ class HubClient {
863
863
  return this.call("POST", `/api/decisions/${segment(id)}/answer`, { body, ...signalOf(answer) });
864
864
  }
865
865
  approve(addr, requestID, options = {}) {
866
- const body = { requestID, ...options.always ? { always: true } : {}, ...whyField(options) };
866
+ const body = { requestID, ...options.always ? { always: true } : {}, ...options.recommend ? { recommend: true } : {}, ...whyField(options) };
867
867
  return this.call("POST", `${AGENT}/sessions/${segment(addr)}/approve`, { body, ...signalOf(options) });
868
868
  }
869
869
  deny(addr, requestID, options = {}) {
@@ -3826,6 +3826,33 @@ var GRAPH_LIMITS = {
3826
3826
  defaultWindowMs: 24 * 60 * 60000
3827
3827
  };
3828
3828
  var WATCHTOWER_CHUNK_BUDGET = 700 * 1024;
3829
+ // packages/protocol/src/voice-recording.ts
3830
+ var RECORDING_LIMITS = {
3831
+ retentionMs: 7 * 24 * 3600000,
3832
+ userBytes: 500 * 1024 * 1024,
3833
+ recordingBytes: 150 * 1024 * 1024,
3834
+ segmentBytes: 2 * 1024 * 1024,
3835
+ segmentsPerRecording: 5000,
3836
+ eventsPerBatch: 200,
3837
+ batchBytes: 256 * 1024,
3838
+ keepaliveBatchBytes: 60 * 1024,
3839
+ eventsPerRecording: 50000,
3840
+ eventBytesPerRecording: 32 * 1024 * 1024,
3841
+ recordingsStored: 200,
3842
+ activePerUser: 4,
3843
+ maxDurationMs: 4 * 3600000,
3844
+ idleEndMs: 10 * 60000,
3845
+ sweepMs: 15 * 60000,
3846
+ textChars: 2000,
3847
+ utteranceChars: 4000,
3848
+ replyChars: 32000,
3849
+ messageChars: 300,
3850
+ consoleBufferEvents: 2000,
3851
+ flushMs: 5000,
3852
+ flushEvents: 100,
3853
+ hubFlushMs: 2000,
3854
+ hubFlushEvents: 100
3855
+ };
3829
3856
  // packages/protocol/src/index.ts
3830
3857
  var PROTOCOL_VERSION = 1;
3831
3858
  var FORM_ANSWER_MAX_KEYS = 50;
@@ -3852,6 +3879,9 @@ function formAnswerProblem(v) {
3852
3879
  }
3853
3880
  var REVERT_FILES_MAX = 50;
3854
3881
  var REVERT_FILE_MAX_CHARS = 1000;
3882
+ function isRevertCommitted(v) {
3883
+ return isObj(v) && nonEmpty(v.messageID) && v.messageID.length <= 300 && (v.from === undefined || num(v.from)) && num(v.until) && num(v.at);
3884
+ }
3855
3885
  var FORWARDED_EVENTS = new Set([
3856
3886
  "session.created",
3857
3887
  "session.renamed",
@@ -3906,7 +3936,7 @@ var IMPLIED_OPENCODE_FEATURES = [
3906
3936
  "expose",
3907
3937
  "create"
3908
3938
  ];
3909
- var OPTIONAL_SPOKE_FEATURES = [...IMPLIED_OPENCODE_FEATURES, "autoName", "sessionTokens", "deliver", "hubPrompt", "permissionMessage", "actTokens", "launch", "fork", "revert"];
3939
+ var OPTIONAL_SPOKE_FEATURES = [...IMPLIED_OPENCODE_FEATURES, "autoName", "sessionTokens", "deliver", "hubPrompt", "permissionMessage", "actTokens", "launch", "fork", "revert", "dismissError"];
3910
3940
  var CORE_OPS = ["turn", "steer", "interrupt", "stop", "permission", "form"];
3911
3941
  var KNOWN_SPOKE_FEATURES = [...CORE_OPS, ...OPTIONAL_SPOKE_FEATURES, "turnEvents", PROFILE_APPLY_FEATURE, SESSION_CONTEXT_FEATURE];
3912
3942
  var SPOKE_FEATURES = [...OPTIONAL_SPOKE_FEATURES, PROFILE_APPLY_FEATURE, SESSION_CONTEXT_FEATURE];
@@ -3943,13 +3973,14 @@ function isLaunchCreate(req) {
3943
3973
  }
3944
3974
  var AGENT_NAME_MAX = 100;
3945
3975
  var PERMISSION_MESSAGE_MAX = 1000;
3976
+ var RES_CODE_MAX = 40;
3946
3977
  var isModelSelection = (v) => isObj(v) && typeof v.providerID === "string" && ID.test(v.providerID) && typeof v.modelID === "string" && ID.test(v.modelID) && (v.variant === undefined || typeof v.variant === "string" && ID.test(v.variant));
3947
3978
  var SESSION_TOKEN_TEXT = new RegExp(`^[\\x21-\\x7e]{1,${SESSION_TOKEN_MAX_CHARS}}$`);
3948
3979
  function isSessionToken(v) {
3949
3980
  return isObj(v) && nonEmpty(v.sessionID) && v.sessionID.length <= 300 && str2(v.token) && SESSION_TOKEN_TEXT.test(v.token) && num(v.exp);
3950
3981
  }
3951
3982
  function isSession(v) {
3952
- return isObj(v) && nonEmpty(v.id) && str2(v.title) && str2(v.directory) && num(v.updated) && typeof v.owned === "boolean" && (v.parentID === undefined || str2(v.parentID)) && (v.exposed === undefined || typeof v.exposed === "boolean") && (v.project === undefined || str2(v.project)) && (v.projectName === undefined || str2(v.projectName)) && (v.archived === undefined || typeof v.archived === "boolean") && (v.open === undefined || typeof v.open === "boolean") && (v.terminal === undefined || isTerminal(v.terminal)) && (v.revert === undefined || isSessionRevert(v.revert)) && (v.agent === undefined || nonEmpty(v.agent) && v.agent.length <= AGENT_NAME_MAX) && (v.contextRevision === undefined || Number.isSafeInteger(v.contextRevision) && v.contextRevision >= 0) && (v.locked === undefined || v.locked === true);
3983
+ return isObj(v) && nonEmpty(v.id) && str2(v.title) && str2(v.directory) && num(v.updated) && typeof v.owned === "boolean" && (v.parentID === undefined || str2(v.parentID)) && (v.exposed === undefined || typeof v.exposed === "boolean") && (v.project === undefined || str2(v.project)) && (v.projectName === undefined || str2(v.projectName)) && (v.archived === undefined || typeof v.archived === "boolean") && (v.open === undefined || typeof v.open === "boolean") && (v.terminal === undefined || isTerminal(v.terminal)) && (v.revert === undefined || isSessionRevert(v.revert)) && (v.revertCommitted === undefined || isRevertCommitted(v.revertCommitted)) && (v.agent === undefined || nonEmpty(v.agent) && v.agent.length <= AGENT_NAME_MAX) && (v.contextRevision === undefined || Number.isSafeInteger(v.contextRevision) && v.contextRevision >= 0) && (v.locked === undefined || v.locked === true);
3953
3984
  }
3954
3985
  var REVERT_STATUS = new Set(["added", "deleted", "modified"]);
3955
3986
  var count = (v) => Number.isSafeInteger(v) && v >= 0;
@@ -3957,7 +3988,7 @@ function isRevertFile(v) {
3957
3988
  return isObj(v) && nonEmpty(v.file) && v.file.length <= REVERT_FILE_MAX_CHARS && (v.status === undefined || REVERT_STATUS.has(v.status)) && (v.additions === undefined || count(v.additions)) && (v.deletions === undefined || count(v.deletions));
3958
3989
  }
3959
3990
  function isSessionRevert(v) {
3960
- return isObj(v) && nonEmpty(v.messageID) && v.messageID.length <= 300 && count(v.hidden) && (v.at === undefined || num(v.at)) && Array.isArray(v.files) && v.files.length <= REVERT_FILES_MAX && v.files.every(isRevertFile) && count(v.filesTotal) && v.filesTotal >= v.files.length;
3991
+ return isObj(v) && nonEmpty(v.messageID) && v.messageID.length <= 300 && count(v.hidden) && (v.at === undefined || num(v.at)) && Array.isArray(v.files) && v.files.length <= REVERT_FILES_MAX && v.files.every(isRevertFile) && count(v.filesTotal) && v.filesTotal >= v.files.length && (v.stagedAt === undefined || num(v.stagedAt));
3961
3992
  }
3962
3993
  var TERMINAL_STATUS = new Set(["idle", "working", "blocked", "done", "unknown"]);
3963
3994
  function isTerminal(v) {
@@ -4129,6 +4160,7 @@ function isServerRequest(v) {
4129
4160
  return nonEmpty(v.sessionID) && nonEmpty(v.messageID) && v.messageID.length <= 300 && typeof v.files === "boolean";
4130
4161
  case "revertRestore":
4131
4162
  case "revertCommit":
4163
+ case "dismissError":
4132
4164
  return nonEmpty(v.sessionID);
4133
4165
  default:
4134
4166
  return false;
@@ -4141,8 +4173,12 @@ function parseSpokeMessage(raw) {
4141
4173
  switch (m.t) {
4142
4174
  case "hello":
4143
4175
  return (m.role === undefined || m.role === "spoke") && num(m.v) && nonEmpty(m.processID) && (m.kind === "service" || m.kind === "standalone") && str2(m.directory) && num(m.pid) && (m.mode === "always" || m.mode === "session") && (str2(m.opencodeVersion) || m.opencodeVersion === undefined && m.harness !== undefined && m.harness !== HARNESS_OPENCODE) && str2(m.pluginVersion) && (m.features === undefined || featureList(m.features)) && (m.harness === undefined || token(m.harness, 64)) && (m.harnessVersion === undefined || str2(m.harnessVersion) && m.harnessVersion.length <= 100) && (m.evidence === undefined || token(m.evidence, 32)) && (m.launchID === undefined || str2(m.launchID) && ID.test(m.launchID)) ? m : undefined;
4144
- case "sessions":
4145
- return Array.isArray(m.sessions) && m.sessions.every(isSession) ? m : undefined;
4176
+ case "sessions": {
4177
+ if (!Array.isArray(m.sessions))
4178
+ return;
4179
+ const sessions = m.sessions.map(lenientSession);
4180
+ return sessions.every(isSession) ? { ...m, sessions } : undefined;
4181
+ }
4146
4182
  case "terminals":
4147
4183
  return m.herdr === undefined || isHerdrInfo(m.herdr) ? m : undefined;
4148
4184
  case "event":
@@ -4150,13 +4186,34 @@ function parseSpokeMessage(raw) {
4150
4186
  case "req":
4151
4187
  return nonEmpty(m.id) && isSpokeRequest(m.req) ? m : undefined;
4152
4188
  case "res":
4153
- return isResFrame(m) ? m : undefined;
4189
+ return isResFrame(m) ? lenientRes(m) : undefined;
4154
4190
  case "bye":
4155
4191
  return isByeFrame(m) ? m : undefined;
4156
4192
  default:
4157
4193
  return;
4158
4194
  }
4159
4195
  }
4196
+ function lenientRes(m) {
4197
+ if (m.code === undefined || nonEmpty(m.code) && m.code.length <= RES_CODE_MAX)
4198
+ return m;
4199
+ const { code: _dropped, ...rest } = m;
4200
+ return rest;
4201
+ }
4202
+ function lenientSession(v) {
4203
+ if (!isObj(v))
4204
+ return v;
4205
+ let out = v;
4206
+ if (out.revertCommitted !== undefined && !isRevertCommitted(out.revertCommitted)) {
4207
+ const { revertCommitted: _dropped, ...rest } = out;
4208
+ out = rest;
4209
+ }
4210
+ const revert = out.revert;
4211
+ if (isObj(revert) && revert.stagedAt !== undefined && !num(revert.stagedAt)) {
4212
+ const { stagedAt: _dropped, ...rest } = revert;
4213
+ out = { ...out, revert: rest };
4214
+ }
4215
+ return out;
4216
+ }
4160
4217
  function parseServerMessage(raw) {
4161
4218
  const m = parseFrame(raw);
4162
4219
  if (!m)
@@ -4167,7 +4224,7 @@ function parseServerMessage(raw) {
4167
4224
  case "req":
4168
4225
  return nonEmpty(m.id) && isServerRequest(m.req) ? m : undefined;
4169
4226
  case "res":
4170
- return isResFrame(m) ? m : undefined;
4227
+ return isResFrame(m) ? lenientRes(m) : undefined;
4171
4228
  case "bye":
4172
4229
  return isByeFrame(m) ? m : undefined;
4173
4230
  default:
@@ -5847,6 +5904,10 @@ function launchText(l) {
5847
5904
  `);
5848
5905
  }
5849
5906
 
5907
+ // packages/cli/src/tool-args.ts
5908
+ class ToolArgError extends Error {
5909
+ }
5910
+
5850
5911
  // packages/cli/src/artifacts-mcp.ts
5851
5912
  var MCP_INLINE_TEXT_BYTES = 256 * 1024;
5852
5913
  var KINDS = ["markdown", "code", "image", "table", "chart", "dashboard", "html", "app", "file"];
@@ -5870,7 +5931,7 @@ var FILE_ARGS = {
5870
5931
  var need = (args, key) => {
5871
5932
  const value = args[key];
5872
5933
  if (typeof value !== "string" || !value.trim())
5873
- throw new Error(`"${key}" is required (a non-empty string)`);
5934
+ throw new ToolArgError(`"${key}" is required (a non-empty string)`);
5874
5935
  return value;
5875
5936
  };
5876
5937
  var optional2 = (args, key) => {
@@ -5878,20 +5939,20 @@ var optional2 = (args, key) => {
5878
5939
  if (value === undefined || value === null)
5879
5940
  return;
5880
5941
  if (typeof value !== "string")
5881
- throw new Error(`"${key}" must be a string`);
5942
+ throw new ToolArgError(`"${key}" must be a string`);
5882
5943
  return value.trim() ? value : undefined;
5883
5944
  };
5884
5945
  function files(args, kind) {
5885
5946
  const text = args.text;
5886
5947
  if (text !== undefined && typeof text !== "string")
5887
- throw new Error('"text" must be a string');
5948
+ throw new ToolArgError('"text" must be a string');
5888
5949
  const blobs = args.blobs;
5889
5950
  if (blobs !== undefined && (!Array.isArray(blobs) || !blobs.every((b) => typeof b === "object" && b !== null)))
5890
- throw new Error('"blobs" must be a list of blob references');
5951
+ throw new ToolArgError('"blobs" must be a list of blob references');
5891
5952
  if (text === undefined && (blobs === undefined || blobs.length === 0))
5892
- throw new Error("give text or blobs (stage bigger content first and pass its reference)");
5953
+ throw new ToolArgError("give text or blobs (stage bigger content first and pass its reference)");
5893
5954
  if (text !== undefined && Buffer.byteLength(text, "utf8") > MCP_INLINE_TEXT_BYTES)
5894
- throw new Error(`text is larger than ${MCP_INLINE_TEXT_BYTES} bytes: stage it with uploadBlob and pass blobs`);
5955
+ throw new ToolArgError(`text is larger than ${MCP_INLINE_TEXT_BYTES} bytes: stage it with uploadBlob and pass blobs`);
5895
5956
  const path = optional2(args, "path");
5896
5957
  const entry = optional2(args, "entry");
5897
5958
  const mediaType = optional2(args, "mediaType") ?? (kind && DEFAULT_MEDIA[kind]) ?? "text/plain";
@@ -5958,7 +6019,7 @@ var artifactTools = [
5958
6019
  const title = need(args, "title");
5959
6020
  const kind = need(args, "kind");
5960
6021
  if (!KINDS.includes(kind))
5961
- throw new Error(`"kind" must be one of ${KINDS.join(", ")}`);
6022
+ throw new ToolArgError(`"kind" must be one of ${KINDS.join(", ")}`);
5962
6023
  const body = files(args, kind);
5963
6024
  const description = optional2(args, "description");
5964
6025
  const visibility = optional2(args, "visibility");
@@ -5997,7 +6058,7 @@ var KINDS2 = ["ask", "propose-edit", "run-action"];
5997
6058
  var need2 = (args, key) => {
5998
6059
  const value = args[key];
5999
6060
  if (typeof value !== "string" || !value.trim())
6000
- throw new Error(`"${key}" is required (a non-empty string)`);
6061
+ throw new ToolArgError(`"${key}" is required (a non-empty string)`);
6001
6062
  return value;
6002
6063
  };
6003
6064
  var optional3 = (args, key) => {
@@ -6005,24 +6066,24 @@ var optional3 = (args, key) => {
6005
6066
  if (value === undefined || value === null)
6006
6067
  return;
6007
6068
  if (typeof value !== "string")
6008
- throw new Error(`"${key}" must be a string`);
6069
+ throw new ToolArgError(`"${key}" must be a string`);
6009
6070
  return value.trim() ? value : undefined;
6010
6071
  };
6011
6072
  var integer = (args, key) => {
6012
6073
  const value = args[key];
6013
6074
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
6014
- throw new Error(`"${key}" is required (a whole number)`);
6075
+ throw new ToolArgError(`"${key}" is required (a whole number)`);
6015
6076
  return value;
6016
6077
  };
6017
6078
  function handlerOf(args) {
6018
6079
  const session = optional3(args, "session");
6019
6080
  const machine = optional3(args, "machine");
6020
6081
  if (session && machine)
6021
- throw new Error('give either "session" (an existing session) or "machine" + "repo" (a new handler session), not both');
6082
+ throw new ToolArgError('give either "session" (an existing session) or "machine" + "repo" (a new handler session), not both');
6022
6083
  if (session)
6023
6084
  return { session };
6024
6085
  if (!machine)
6025
- throw new Error('"machine" and "repo" are required to launch a handler session (or name an existing one with "session")');
6086
+ throw new ToolArgError('"machine" and "repo" are required to launch a handler session (or name an existing one with "session")');
6026
6087
  const repo = need2(args, "repo");
6027
6088
  const harness = optional3(args, "harness");
6028
6089
  const model = optional3(args, "model");
@@ -6086,7 +6147,7 @@ var artifactJobTools = [
6086
6147
  run: async (hub, args, signal) => {
6087
6148
  const kind = optional3(args, "kind");
6088
6149
  if (kind && !KINDS2.includes(kind))
6089
- throw new Error(`"kind" must be one of ${KINDS2.join(", ")}`);
6150
+ throw new ToolArgError(`"kind" must be one of ${KINDS2.join(", ")}`);
6090
6151
  const action = optional3(args, "action");
6091
6152
  const apply = optional3(args, "apply");
6092
6153
  const key = optional3(args, "key");
@@ -6155,12 +6216,12 @@ var artifactJobTools = [
6155
6216
  const answer = optional3(args, "answer");
6156
6217
  const text = args.text;
6157
6218
  if (text !== undefined && typeof text !== "string")
6158
- throw new Error('"text" must be a string');
6219
+ throw new ToolArgError('"text" must be a string');
6159
6220
  if (typeof text === "string" && Buffer.byteLength(text, "utf8") > MCP_JOB_TEXT_BYTES)
6160
- throw new Error(`"text" is larger than ${MCP_JOB_TEXT_BYTES} bytes`);
6221
+ throw new ToolArgError(`"text" is larger than ${MCP_JOB_TEXT_BYTES} bytes`);
6161
6222
  const keep = args.keep;
6162
6223
  if (keep !== undefined && (!Array.isArray(keep) || !keep.every((k) => typeof k === "string")))
6163
- throw new Error('"keep" must be a list of paths');
6224
+ throw new ToolArgError('"keep" must be a list of paths');
6164
6225
  const path = optional3(args, "path");
6165
6226
  const entry = optional3(args, "entry");
6166
6227
  const result = error ? { error } : text !== undefined || keep !== undefined ? {
@@ -6207,7 +6268,7 @@ var UPDATES = TEAM_INTAKE_STATUS_INPUTS;
6207
6268
  var need3 = (args, key) => {
6208
6269
  const value = args[key];
6209
6270
  if (typeof value !== "string" || !value.trim())
6210
- throw new Error(`"${key}" is required (a non-empty string)`);
6271
+ throw new ToolArgError(`"${key}" is required (a non-empty string)`);
6211
6272
  return value;
6212
6273
  };
6213
6274
  var optional4 = (args, key) => {
@@ -6215,7 +6276,7 @@ var optional4 = (args, key) => {
6215
6276
  if (value === undefined || value === null)
6216
6277
  return;
6217
6278
  if (typeof value !== "string")
6218
- throw new Error(`"${key}" must be a string`);
6279
+ throw new ToolArgError(`"${key}" must be a string`);
6219
6280
  return value.trim() ? value : undefined;
6220
6281
  };
6221
6282
  var strings2 = (args, key) => {
@@ -6223,13 +6284,13 @@ var strings2 = (args, key) => {
6223
6284
  if (value === undefined || value === null)
6224
6285
  return;
6225
6286
  if (!Array.isArray(value) || !value.every((v) => typeof v === "string"))
6226
- throw new Error(`"${key}" must be a list of strings`);
6287
+ throw new ToolArgError(`"${key}" must be a list of strings`);
6227
6288
  return value;
6228
6289
  };
6229
6290
  var oneOf4 = (args, key, allowed) => {
6230
6291
  const value = optional4(args, key);
6231
6292
  if (value !== undefined && !allowed.includes(value))
6232
- throw new Error(`"${key}" must be one of ${allowed.join(", ")}`);
6293
+ throw new ToolArgError(`"${key}" must be one of ${allowed.join(", ")}`);
6233
6294
  return value;
6234
6295
  };
6235
6296
  function dueOf(args) {
@@ -6238,7 +6299,7 @@ function dueOf(args) {
6238
6299
  return;
6239
6300
  const ms = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : Number.NaN;
6240
6301
  if (!Number.isSafeInteger(ms) || ms <= 0)
6241
- throw new Error(`"due" must be an ISO date/time or epoch milliseconds`);
6302
+ throw new ToolArgError(`"due" must be an ISO date/time or epoch milliseconds`);
6242
6303
  return ms;
6243
6304
  }
6244
6305
  var line4 = (i) => `${i.id} ${i.status}${i.progress ? `/${i.progress}` : ""} ${i.priority} r${i.revision} ${sanitizeText(i.title).replace(/\s+/g, " ")}`;
@@ -6325,10 +6386,10 @@ ${intakeText(intake)}`;
6325
6386
  run: async (hub, args, signal) => {
6326
6387
  const status = oneOf4(args, "status", UPDATES);
6327
6388
  if (!status)
6328
- throw new Error(`"status" is required: one of ${UPDATES.join(", ")}`);
6389
+ throw new ToolArgError(`"status" is required: one of ${UPDATES.join(", ")}`);
6329
6390
  const expectedRevision = args.expectedRevision;
6330
6391
  if (expectedRevision !== undefined && expectedRevision !== null && (typeof expectedRevision !== "number" || !Number.isSafeInteger(expectedRevision) || expectedRevision < 0))
6331
- throw new Error(`"expectedRevision" must be the revision you read (or left out)`);
6392
+ throw new ToolArgError(`"expectedRevision" must be the revision you read (or left out)`);
6332
6393
  const reason = optional4(args, "reason");
6333
6394
  const { intake, assignment } = await reportIntakeStatus(hub.teams, need3(args, "team"), need3(args, "intake"), { status, ...typeof expectedRevision === "number" ? { expectedRevision } : {}, ...reason ? { reason } : {} }, { signal });
6334
6395
  return `${intake.id} is ${intake.status}${intake.progress ? ` (${intake.progress})` : ""}, revision ${intake.revision}${assignment ? `; assignment ${assignment.id} ${assignment.status}` : ""}`;
@@ -6421,9 +6482,7 @@ var LAUNCH = {
6421
6482
  required: ["workspace"]
6422
6483
  };
6423
6484
  var SCOPES = "App tokens need scope teams:read (reads) or teams:manage (changes); seats, status and staffing also need sessions:read. Session tokens read only teams they are seated in and never change definitions. Team roles confer no authority.";
6424
-
6425
- class ArgError extends Error {
6426
- }
6485
+ var ArgError = ToolArgError;
6427
6486
  var need4 = (args, key) => {
6428
6487
  const value = args[key];
6429
6488
  if (typeof value !== "string" || !value.trim())
@@ -6790,10 +6849,7 @@ var META_FIELDS = {
6790
6849
  projects: { ...STRINGS2, description: "Project names the profile is meant for (≤ 16)." }
6791
6850
  };
6792
6851
  var SCOPES2 = "App tokens need scope profiles:read (reads) or profiles:manage (changes; profile_version_create with content fields and the skill/command set/remove tools read the latest version first, so they also need profiles:read); pins need profiles:assign and sessions:read (an app also needs a prompt grant on the session). Session tokens never change profiles. A profile confers no authority.";
6793
-
6794
- class ProfileArgError extends Error {
6795
- }
6796
- var ArgError2 = ProfileArgError;
6852
+ var ArgError2 = ToolArgError;
6797
6853
  var need5 = (args, key) => {
6798
6854
  const value = args[key];
6799
6855
  if (typeof value !== "string" || !value.trim())
@@ -7048,7 +7104,7 @@ var STRINGS3 = { type: "array", items: { type: "string" } };
7048
7104
  var need6 = (args, key) => {
7049
7105
  const value = args[key];
7050
7106
  if (typeof value !== "string" || !value.trim())
7051
- throw new Error(`"${key}" is required (a non-empty string)`);
7107
+ throw new ToolArgError(`"${key}" is required (a non-empty string)`);
7052
7108
  return value;
7053
7109
  };
7054
7110
  var flag = (args, key, fallback) => {
@@ -7056,7 +7112,7 @@ var flag = (args, key, fallback) => {
7056
7112
  if (value === undefined || value === null)
7057
7113
  return fallback;
7058
7114
  if (typeof value !== "boolean")
7059
- throw new Error(`"${key}" must be true or false`);
7115
+ throw new ToolArgError(`"${key}" must be true or false`);
7060
7116
  return value;
7061
7117
  };
7062
7118
  var UPDATE_FIELDS = ["autoActive", "activeWithinHours", "usePins", "sessions", "projects", "labels", "muted", "mutedProjects"];
@@ -7088,10 +7144,10 @@ var watchlistTools = [
7088
7144
  run: async (hub, args, signal) => {
7089
7145
  const expected = args.expectedRevision;
7090
7146
  if (typeof expected !== "number" || !Number.isSafeInteger(expected) || expected < 0)
7091
- throw new Error(`"expectedRevision" is required (the revision you read)`);
7147
+ throw new ToolArgError(`"expectedRevision" is required (the revision you read)`);
7092
7148
  const unknown = Object.keys(args).filter((k) => k !== "expectedRevision" && !UPDATE_FIELDS.includes(k));
7093
7149
  if (unknown.length)
7094
- throw new Error(`Unknown argument${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${safe(k).slice(0, 40)}"`).join(", ")}: use ${UPDATE_FIELDS.join(", ")} and expectedRevision`);
7150
+ throw new ToolArgError(`Unknown argument${unknown.length > 1 ? "s" : ""} ${unknown.map((k) => `"${safe(k).slice(0, 40)}"`).join(", ")}: use ${UPDATE_FIELDS.join(", ")} and expectedRevision`);
7095
7151
  const patch = {};
7096
7152
  for (const field of UPDATE_FIELDS)
7097
7153
  if (args[field] !== undefined && args[field] !== null)
@@ -7137,9 +7193,7 @@ ${watchlistConfigText(r.config)}`;
7137
7193
  // packages/cli/src/mcp.ts
7138
7194
  var MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
7139
7195
  var RPC = { parse: -32700, invalidRequest: -32600, methodNotFound: -32601, invalidParams: -32602, internal: -32603 };
7140
-
7141
- class ArgError3 extends Error {
7142
- }
7196
+ var ArgError3 = ToolArgError;
7143
7197
  var need7 = (args, key) => {
7144
7198
  const value = args[key];
7145
7199
  if (typeof value !== "string" || !value.trim())
@@ -8324,7 +8378,7 @@ function createMcpServer(options) {
8324
8378
  } catch (cause) {
8325
8379
  if (controller.signal.aborted)
8326
8380
  return;
8327
- const text = cause instanceof ArgError3 || cause instanceof ProfileArgError ? `invalid: ${cause.message}` : cause instanceof CliError ? errorText(cause) : `${tool.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`;
8381
+ const text = cause instanceof ToolArgError ? `invalid: ${cause.message}` : cause instanceof CliError ? errorText(cause) : `${tool.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`;
8328
8382
  return ok2(id, toolResult(clean(text, token), true));
8329
8383
  } finally {
8330
8384
  if (inFlight.get(key(id)) === controller)
@@ -8761,7 +8815,7 @@ import { arch as osArch, platform as osPlatform } from "node:os";
8761
8815
  // packages/node/package.json
8762
8816
  var package_default2 = {
8763
8817
  name: "@huynle/hub-node",
8764
- version: "0.8.1",
8818
+ version: "0.8.3",
8765
8819
  private: true,
8766
8820
  description: "`hub node`: the per-machine daemon that prepares, collects and cleans up launch workspaces (internal; shipped inside @huynle/hub, not published on its own).",
8767
8821
  license: "MIT",
@@ -9853,7 +9907,7 @@ import path13 from "node:path";
9853
9907
  // packages/runtime-openshell/package.json
9854
9908
  var package_default3 = {
9855
9909
  name: "@huynle/hub-runtime-openshell",
9856
- version: "0.8.1",
9910
+ version: "0.8.3",
9857
9911
  private: true,
9858
9912
  description: "The `openshell` runtime of `hub node`: launches in NVIDIA OpenShell sandboxes, driven through the openshell CLI (internal; shipped inside @huynle/hub, not published on its own).",
9859
9913
  license: "MIT",
@@ -11395,6 +11449,15 @@ function wsURL2(server) {
11395
11449
  var defaultSocket2 = (url, headers) => new WebSocket(url, { headers });
11396
11450
  var message2 = (cause) => cause instanceof Error ? cause.message : String(cause);
11397
11451
 
11452
+ class CodedError extends Error {
11453
+ code;
11454
+ constructor(message, code) {
11455
+ super(message);
11456
+ this.code = code;
11457
+ this.name = "CodedError";
11458
+ }
11459
+ }
11460
+
11398
11461
  class SpokeConnection {
11399
11462
  options;
11400
11463
  state = "off";
@@ -11564,7 +11627,7 @@ class SpokeConnection {
11564
11627
  try {
11565
11628
  reply = { t: "res", id, ok: true, result: await this.options.handle(req) };
11566
11629
  } catch (cause) {
11567
- reply = { t: "res", id, ok: false, error: message2(cause) };
11630
+ reply = { t: "res", id, ok: false, error: message2(cause), ...cause instanceof CodedError ? { code: cause.code } : {} };
11568
11631
  }
11569
11632
  this.send(reply);
11570
11633
  }
@@ -13528,7 +13591,7 @@ import path22 from "node:path";
13528
13591
  // packages/claude-code/package.json
13529
13592
  var package_default4 = {
13530
13593
  name: "@huynle/hub-claude-code",
13531
- version: "0.8.1",
13594
+ version: "0.8.3",
13532
13595
  private: true,
13533
13596
  description: "The Claude Code hooks spoke: per-user daemon, hook bridge, plugin dir, `hub claude` wrapper and daemon-backed MCP server (internal; shipped inside @huynle/hub, not published on its own).",
13534
13597
  license: "MIT",
@@ -13817,7 +13880,7 @@ import path25 from "node:path";
13817
13880
  // packages/preset/package.json
13818
13881
  var package_default5 = {
13819
13882
  name: "@huynle/hub-preset",
13820
- version: "0.8.1",
13883
+ version: "0.8.3",
13821
13884
  private: true,
13822
13885
  description: "The preset spoke: drives a harness without a spoke of its own through a terminal pane (tmux), described by a data-only preset; official presets in presets/ (internal; shipped inside @huynle/hub, not published on its own).",
13823
13886
  license: "MIT",
@@ -14342,7 +14405,7 @@ import { openSync as openSync5, closeSync as closeSync5, readFileSync as readFil
14342
14405
  // packages/codex/package.json
14343
14406
  var package_default6 = {
14344
14407
  name: "@huynle/hub-codex",
14345
- version: "0.8.1",
14408
+ version: "0.8.3",
14346
14409
  private: true,
14347
14410
  description: "The Codex app-server spoke: attaches to Codex's app-server (its own over stdio, and the shared daemon's socket when present), and the `hub codex` wrapper (internal; shipped inside @huynle/hub, not published on its own).",
14348
14411
  license: "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huynle/hub-cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "private": true,
5
5
  "description": "The hub CLI (`hub` / `hubctl`) for agents and scripts (internal; bundled into @huynle/hub, not published on its own).",
6
6
  "license": "MIT",
@@ -7,6 +7,7 @@
7
7
  // (+ artifacts:read) for an app token. Content never travels in requests or job listings, only in the context.
8
8
  import type { ArtifactJob, ArtifactJobContext, ArtifactJobKind, ArtifactJobRequest } from "../../sdk/src/artifact-jobs.ts"
9
9
  import type { Artifacts } from "../../sdk/src/artifacts.ts"
10
+ import { ToolArgError } from "./tool-args"
10
11
 
11
12
  /** UTF-8 bytes of the inline proposal text one tool call may carry. */
12
13
  export const MCP_JOB_TEXT_BYTES = 256 * 1024
@@ -33,27 +34,27 @@ const KINDS: readonly ArtifactJobKind[] = ["ask", "propose-edit", "run-action"]
33
34
 
34
35
  const need = (args: Args, key: string): string => {
35
36
  const value = args[key]
36
- if (typeof value !== "string" || !value.trim()) throw new Error(`"${key}" is required (a non-empty string)`)
37
+ if (typeof value !== "string" || !value.trim()) throw new ToolArgError(`"${key}" is required (a non-empty string)`)
37
38
  return value
38
39
  }
39
40
  const optional = (args: Args, key: string): string | undefined => {
40
41
  const value = args[key]
41
42
  if (value === undefined || value === null) return undefined
42
- if (typeof value !== "string") throw new Error(`"${key}" must be a string`)
43
+ if (typeof value !== "string") throw new ToolArgError(`"${key}" must be a string`)
43
44
  return value.trim() ? value : undefined
44
45
  }
45
46
  const integer = (args: Args, key: string): number => {
46
47
  const value = args[key]
47
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`"${key}" is required (a whole number)`)
48
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new ToolArgError(`"${key}" is required (a whole number)`)
48
49
  return value
49
50
  }
50
51
 
51
52
  function handlerOf(args: Args): ArtifactJobRequest["handler"] {
52
53
  const session = optional(args, "session")
53
54
  const machine = optional(args, "machine")
54
- if (session && machine) throw new Error('give either "session" (an existing session) or "machine" + "repo" (a new handler session), not both')
55
+ if (session && machine) throw new ToolArgError('give either "session" (an existing session) or "machine" + "repo" (a new handler session), not both')
55
56
  if (session) return { session }
56
- if (!machine) throw new Error('"machine" and "repo" are required to launch a handler session (or name an existing one with "session")')
57
+ if (!machine) throw new ToolArgError('"machine" and "repo" are required to launch a handler session (or name an existing one with "session")')
57
58
  const repo = need(args, "repo")
58
59
  const harness = optional(args, "harness")
59
60
  const model = optional(args, "model")
@@ -114,7 +115,7 @@ export const artifactJobTools: readonly ArtifactJobTool[] = [
114
115
  readOnly: false,
115
116
  run: async (hub, args, signal) => {
116
117
  const kind = optional(args, "kind") as ArtifactJobKind | undefined
117
- if (kind && !KINDS.includes(kind)) throw new Error(`"kind" must be one of ${KINDS.join(", ")}`)
118
+ if (kind && !KINDS.includes(kind)) throw new ToolArgError(`"kind" must be one of ${KINDS.join(", ")}`)
118
119
  const action = optional(args, "action")
119
120
  const apply = optional(args, "apply") as "manual" | "auto" | undefined
120
121
  const key = optional(args, "key")
@@ -181,10 +182,10 @@ export const artifactJobTools: readonly ArtifactJobTool[] = [
181
182
  const error = optional(args, "error")
182
183
  const answer = optional(args, "answer")
183
184
  const text = args.text
184
- if (text !== undefined && typeof text !== "string") throw new Error('"text" must be a string')
185
- if (typeof text === "string" && Buffer.byteLength(text, "utf8") > MCP_JOB_TEXT_BYTES) throw new Error(`"text" is larger than ${MCP_JOB_TEXT_BYTES} bytes`)
185
+ if (text !== undefined && typeof text !== "string") throw new ToolArgError('"text" must be a string')
186
+ if (typeof text === "string" && Buffer.byteLength(text, "utf8") > MCP_JOB_TEXT_BYTES) throw new ToolArgError(`"text" is larger than ${MCP_JOB_TEXT_BYTES} bytes`)
186
187
  const keep = args.keep
187
- if (keep !== undefined && (!Array.isArray(keep) || !keep.every((k) => typeof k === "string"))) throw new Error('"keep" must be a list of paths')
188
+ if (keep !== undefined && (!Array.isArray(keep) || !keep.every((k) => typeof k === "string"))) throw new ToolArgError('"keep" must be a list of paths')
188
189
  const path = optional(args, "path")
189
190
  const entry = optional(args, "entry")
190
191
  const result = error
@@ -5,6 +5,7 @@
5
5
  // tool ever reads local files). Needs scope artifacts:read / artifacts:write for an app token; sessions create and
6
6
  // read workspace-shared artifacts only (revising stays with the user in v1).
7
7
  import type { Artifact, ArtifactKind, ArtifactRevision, Artifacts, BlobRef } from "../../sdk/src/artifacts.ts"
8
+ import { ToolArgError } from "./tool-args"
8
9
 
9
10
  /** UTF-8 bytes of inline text one tool call may carry (bigger content: stage it, then pass `blobs`). */
10
11
  export const MCP_INLINE_TEXT_BYTES = 256 * 1024
@@ -45,24 +46,24 @@ const FILE_ARGS = {
45
46
 
46
47
  const need = (args: Args, key: string): string => {
47
48
  const value = args[key]
48
- if (typeof value !== "string" || !value.trim()) throw new Error(`"${key}" is required (a non-empty string)`)
49
+ if (typeof value !== "string" || !value.trim()) throw new ToolArgError(`"${key}" is required (a non-empty string)`)
49
50
  return value
50
51
  }
51
52
  const optional = (args: Args, key: string): string | undefined => {
52
53
  const value = args[key]
53
54
  if (value === undefined || value === null) return undefined
54
- if (typeof value !== "string") throw new Error(`"${key}" must be a string`)
55
+ if (typeof value !== "string") throw new ToolArgError(`"${key}" must be a string`)
55
56
  return value.trim() ? value : undefined
56
57
  }
57
58
 
58
59
  /** The files of a publish/revise: inline text and/or blob references (one of them required). */
59
60
  function files(args: Args, kind: ArtifactKind | undefined): { blobs?: BlobRef[]; upload?: { mediaType: string; data: string; path?: string }[]; entry?: string } {
60
61
  const text = args.text
61
- if (text !== undefined && typeof text !== "string") throw new Error('"text" must be a string')
62
+ if (text !== undefined && typeof text !== "string") throw new ToolArgError('"text" must be a string')
62
63
  const blobs = args.blobs
63
- if (blobs !== undefined && (!Array.isArray(blobs) || !blobs.every((b) => typeof b === "object" && b !== null))) throw new Error('"blobs" must be a list of blob references')
64
- if (text === undefined && (blobs === undefined || blobs.length === 0)) throw new Error('give text or blobs (stage bigger content first and pass its reference)')
65
- if (text !== undefined && Buffer.byteLength(text, "utf8") > MCP_INLINE_TEXT_BYTES) throw new Error(`text is larger than ${MCP_INLINE_TEXT_BYTES} bytes: stage it with uploadBlob and pass blobs`)
64
+ if (blobs !== undefined && (!Array.isArray(blobs) || !blobs.every((b) => typeof b === "object" && b !== null))) throw new ToolArgError('"blobs" must be a list of blob references')
65
+ if (text === undefined && (blobs === undefined || blobs.length === 0)) throw new ToolArgError('give text or blobs (stage bigger content first and pass its reference)')
66
+ if (text !== undefined && Buffer.byteLength(text, "utf8") > MCP_INLINE_TEXT_BYTES) throw new ToolArgError(`text is larger than ${MCP_INLINE_TEXT_BYTES} bytes: stage it with uploadBlob and pass blobs`)
66
67
  const path = optional(args, "path")
67
68
  const entry = optional(args, "entry")
68
69
  const mediaType = optional(args, "mediaType") ?? (kind && DEFAULT_MEDIA[kind]) ?? "text/plain"
@@ -126,7 +127,7 @@ export const artifactTools: readonly ArtifactTool[] = [
126
127
  run: async (hub, args, signal) => {
127
128
  const title = need(args, "title")
128
129
  const kind = need(args, "kind") as ArtifactKind
129
- if (!KINDS.includes(kind)) throw new Error(`"kind" must be one of ${KINDS.join(", ")}`)
130
+ if (!KINDS.includes(kind)) throw new ToolArgError(`"kind" must be one of ${KINDS.join(", ")}`)
130
131
  const body = files(args, kind)
131
132
  const description = optional(args, "description")
132
133
  const visibility = optional(args, "visibility") as "private" | "workspace" | undefined
@@ -26,7 +26,8 @@ import { artifactTools } from "./artifacts-mcp"
26
26
  import { artifactJobTools } from "./artifact-jobs-mcp"
27
27
  import { teamIntakeTools } from "./team-intake-mcp"
28
28
  import { teamTools } from "./teams-mcp"
29
- import { ProfileArgError, profileLifecycleTools } from "./profiles-mcp"
29
+ import { profileLifecycleTools } from "./profiles-mcp"
30
+ import { ToolArgError } from "./tool-args"
30
31
  import { watchlistTools } from "./watchlist"
31
32
 
32
33
  /** The protocol revisions the server speaks, newest first: a client's supported one is echoed, else the newest. */
@@ -48,7 +49,7 @@ type Id = string | number | null
48
49
  type Args = Record<string, unknown>
49
50
 
50
51
  /** A bad tool argument (an isError result, not a protocol error). */
51
- class ArgError extends Error {}
52
+ const ArgError = ToolArgError
52
53
 
53
54
  const need = (args: Args, key: string): string => {
54
55
  const value = args[key]
@@ -1143,7 +1144,7 @@ export function createMcpServer(options: McpServerOptions): McpServer {
1143
1144
  return controller.signal.aborted ? undefined : ok(id, toolResult(clean(text, token), false))
1144
1145
  } catch (cause) {
1145
1146
  if (controller.signal.aborted) return undefined // cancelled: no answer (MCP cancellation)
1146
- const text = cause instanceof ArgError || cause instanceof ProfileArgError ? `invalid: ${cause.message}` : cause instanceof CliError ? errorText(cause) : `${tool.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`
1147
+ const text = cause instanceof ToolArgError ? `invalid: ${cause.message}` : cause instanceof CliError ? errorText(cause) : `${tool.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`
1147
1148
  return ok(id, toolResult(clean(text, token), true))
1148
1149
  } finally {
1149
1150
  if (inFlight.get(key(id)) === controller) inFlight.delete(key(id))
@@ -14,6 +14,7 @@
14
14
  // version can be restored with fromVersion); archive/restore (no new versions or pins) and pins (applied to a running
15
15
  // session) keep the default destructiveHint. Changes send expectedRevision, so repeating one is idempotent.
16
16
  import { sanitizeText } from "../../sdk/src/envelope.ts"
17
+ import { ToolArgError } from "./tool-args"
17
18
  import type { Profiles } from "../../sdk/src/client.ts"
18
19
  import type { ProfileCommand, ProfileContent, ProfileSkill, ProfileVersionSummary, ProfileView } from "../../sdk/src/types.ts"
19
20
 
@@ -53,9 +54,8 @@ const META_FIELDS = {
53
54
  } as const
54
55
  const SCOPES = "App tokens need scope profiles:read (reads) or profiles:manage (changes; profile_version_create with content fields and the skill/command set/remove tools read the latest version first, so they also need profiles:read); pins need profiles:assign and sessions:read (an app also needs a prompt grant on the session). Session tokens never change profiles. A profile confers no authority."
55
56
 
56
- /** A bad tool argument: mcp.ts answers it as `invalid: …` (an isError result). */
57
- export class ProfileArgError extends Error {}
58
- const ArgError = ProfileArgError
57
+ /** A bad tool argument: mcp.ts answers it as `invalid: …` (an isError result; tool-args.ts). */
58
+ const ArgError = ToolArgError
59
59
 
60
60
  const need = (args: Args, key: string): string => {
61
61
  const value = args[key]