@hasna/mementos 0.17.3 → 0.17.5

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/cli/index.js CHANGED
@@ -7019,18 +7019,55 @@ var init_memories = __esm(() => {
7019
7019
  ]);
7020
7020
  });
7021
7021
 
7022
+ // src/lib/agent-name.ts
7023
+ function trimControlBytes(value) {
7024
+ let name = value;
7025
+ for (;; ) {
7026
+ const next = name.trim().replace(CONTROL_BYTE_EDGES, "");
7027
+ if (next === name)
7028
+ return name;
7029
+ name = next;
7030
+ }
7031
+ }
7032
+ function isValidAgentName(name) {
7033
+ return name.length > 0 && name.length <= AGENT_NAME_MAX_LENGTH && /^[A-Za-z0-9._-]+$/.test(name) && /[A-Za-z0-9_-]/.test(name);
7034
+ }
7035
+ function isUrlDotSegment(value) {
7036
+ const segment = value.toLowerCase();
7037
+ return segment === "." || segment === ".." || segment === "%2e" || segment === ".%2e" || segment === "%2e." || segment === "%2e%2e";
7038
+ }
7039
+ function agentPathSegment(name) {
7040
+ if (isUrlDotSegment(name)) {
7041
+ throw new Error(`Refusing to build an agent URL from ${JSON.stringify(name)}: the URL parser collapses a dot segment, ` + `so the request path would not be the one built here.`);
7042
+ }
7043
+ return encodeURIComponent(name);
7044
+ }
7045
+ function sanitizeAgentName(raw) {
7046
+ if (typeof raw !== "string")
7047
+ return null;
7048
+ const name = trimControlBytes(raw);
7049
+ if (CONTROL_BYTE.test(name))
7050
+ return null;
7051
+ return isValidAgentName(name) ? name : null;
7052
+ }
7053
+ var AGENT_NAME_MAX_LENGTH = 128, CONTROL_BYTE, CONTROL_BYTE_EDGES;
7054
+ var init_agent_name = __esm(() => {
7055
+ CONTROL_BYTE = /[\u0000-\u001f\u007f]/;
7056
+ CONTROL_BYTE_EDGES = /^[\u0000-\u001f\u007f]+|[\u0000-\u001f\u007f]+$/g;
7057
+ });
7058
+
7022
7059
  // src/db/agents.ts
7023
7060
  import { homedir as homedir3 } from "os";
7024
7061
  import { join as join6 } from "path";
7025
7062
  import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
7026
7063
  function resolveWritingAgentName() {
7027
- const envName = process.env["MEMENTOS_AGENT"]?.trim();
7064
+ const envName = sanitizeAgentName(process.env["MEMENTOS_AGENT"]);
7028
7065
  if (envName)
7029
7066
  return envName;
7030
7067
  try {
7031
7068
  const path = join6(homedir3(), ".hasna", "conversations", "agent-id");
7032
7069
  if (existsSync5(path)) {
7033
- const fileAgent = readFileSync2(path, "utf8").trim();
7070
+ const fileAgent = sanitizeAgentName(readFileSync2(path, "utf8"));
7034
7071
  if (fileAgent)
7035
7072
  return fileAgent;
7036
7073
  }
@@ -7111,7 +7148,7 @@ function registerAgent(name, sessionId, description, role, projectId, db) {
7111
7148
  }
7112
7149
  function getAgent(idOrName, db) {
7113
7150
  if (!db && isApiMode()) {
7114
- const { status, data } = apiJson("GET", `/agents/${encodeURIComponent(idOrName)}`, undefined, { allow404: true });
7151
+ const { status, data } = apiJson("GET", `/agents/${agentPathSegment(idOrName)}`, undefined, { allow404: true });
7115
7152
  if (status === 404 || !data)
7116
7153
  return null;
7117
7154
  return data;
@@ -7175,7 +7212,7 @@ function touchAgent(idOrName, db) {
7175
7212
  const agent2 = getAgent(idOrName);
7176
7213
  if (!agent2)
7177
7214
  return;
7178
- apiJson("PATCH", `/agents/${encodeURIComponent(agent2.id)}`, {});
7215
+ apiJson("PATCH", `/agents/${agentPathSegment(agent2.id)}`, {});
7179
7216
  return;
7180
7217
  }
7181
7218
  const d = db || getDatabase();
@@ -7186,7 +7223,7 @@ function touchAgent(idOrName, db) {
7186
7223
  }
7187
7224
  function updateAgent(id, updates, db) {
7188
7225
  if (!db && isApiMode()) {
7189
- const { status, data } = apiJson("PATCH", `/agents/${encodeURIComponent(id)}`, updates, { allow404: true });
7226
+ const { status, data } = apiJson("PATCH", `/agents/${agentPathSegment(id)}`, updates, { allow404: true });
7190
7227
  if (status === 404 || !data)
7191
7228
  return null;
7192
7229
  return data;
@@ -7232,6 +7269,7 @@ function updateAgent(id, updates, db) {
7232
7269
  var CONFLICT_WINDOW_MS, UNBOUNDED_AGENT_LIST_LIMIT;
7233
7270
  var init_agents = __esm(() => {
7234
7271
  init_types();
7272
+ init_agent_name();
7235
7273
  init_database();
7236
7274
  init_api_mode();
7237
7275
  CONFLICT_WINDOW_MS = 30 * 60 * 1000;
@@ -13680,7 +13718,7 @@ import {
13680
13718
  ClientTransportConfigurationError as ClientTransportConfigurationError2
13681
13719
  } from "@hasna/contracts/client";
13682
13720
  function sdkProtocolError(operation, detail) {
13683
- return new MementosError(`mementos ${operation} returned a malformed 2xx response: ${detail}`, 502);
13721
+ return new MementosError(`mementos ${operation} returned a malformed 2xx response: ${detail}`, 502, undefined, "MEMENTOS_RESPONSE_CONTRACT");
13684
13722
  }
13685
13723
  function sessionProtocolError(detail) {
13686
13724
  return sdkProtocolError("session jobs", detail);
@@ -14135,11 +14173,12 @@ class MementosClient {
14135
14173
  body: body !== undefined ? JSON.stringify(body) : undefined
14136
14174
  });
14137
14175
  if (!res.ok) {
14138
- let errBody = {};
14176
+ let errBody;
14139
14177
  try {
14140
14178
  errBody = await res.json();
14141
14179
  } catch {}
14142
- throw new MementosError(errBody.error ?? `HTTP ${res.status}`, res.status, errBody.details);
14180
+ const errorObject = errBody && typeof errBody === "object" && !Array.isArray(errBody) ? errBody : undefined;
14181
+ throw new MementosError(typeof errorObject?.["error"] === "string" ? errorObject["error"] : `HTTP ${res.status}`, res.status, errorObject?.["details"]);
14143
14182
  }
14144
14183
  if (res.status === 204)
14145
14184
  return;
@@ -14341,10 +14380,10 @@ class MementosClient {
14341
14380
  return this.post("/api/agents", input);
14342
14381
  }
14343
14382
  getAgent(idOrName) {
14344
- return this.get(`/api/agents/${idOrName}`);
14383
+ return this.get(`/api/agents/${agentPathSegment(idOrName)}`);
14345
14384
  }
14346
14385
  updateAgent(idOrName, updates) {
14347
- return this.patch(`/api/agents/${idOrName}`, updates);
14386
+ return this.patch(`/api/agents/${agentPathSegment(idOrName)}`, updates);
14348
14387
  }
14349
14388
  listAgentsByProject(projectId) {
14350
14389
  return this.get(`/api/agents`, { project_id: projectId });
@@ -14887,14 +14926,17 @@ var MEMENTOS_MACHINE_REGISTRATION_CONTRACT = "mementos.machine-registration.v1",
14887
14926
  var init_sdk = __esm(() => {
14888
14927
  init_audit_contract();
14889
14928
  init_local_opt_in();
14929
+ init_agent_name();
14890
14930
  init_decisions();
14891
14931
  MementosError = class MementosError extends Error {
14892
14932
  status;
14893
14933
  details;
14894
- constructor(message, status, details) {
14934
+ code;
14935
+ constructor(message, status, details, code) {
14895
14936
  super(message);
14896
14937
  this.status = status;
14897
14938
  this.details = details;
14939
+ this.code = code;
14898
14940
  this.name = "MementosError";
14899
14941
  }
14900
14942
  };
@@ -14919,9 +14961,35 @@ __export(exports_claude_stop_hook, {
14919
14961
  });
14920
14962
  import { constants, openSync, closeSync, fstatSync, readSync } from "fs";
14921
14963
  import { isAbsolute } from "path";
14964
+ function failureCategory(error, phase) {
14965
+ if (error instanceof HookFailure)
14966
+ return { phase, reason: error.reason };
14967
+ if (error instanceof MementosConfigError)
14968
+ return { phase: "configuration", reason: "configuration_unavailable" };
14969
+ if (error instanceof MementosError) {
14970
+ return { phase: "response", reason: error.code === "MEMENTOS_RESPONSE_CONTRACT" ? "response_contract" : "http_error" };
14971
+ }
14972
+ if (error instanceof SyntaxError) {
14973
+ return { phase: phase === "request" ? "response" : phase, reason: "invalid_json" };
14974
+ }
14975
+ if (phase === "input")
14976
+ return { phase, reason: "input_unreadable" };
14977
+ if (phase === "transcript") {
14978
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
14979
+ const reason = code === "ENOENT" ? "file_not_found" : code === "EACCES" || code === "EPERM" ? "file_access_denied" : code === "ELOOP" ? "file_symlink_refused" : "file_read_failed";
14980
+ return { phase, reason };
14981
+ }
14982
+ if (phase === "configuration")
14983
+ return { phase, reason: "configuration_unavailable" };
14984
+ if (error instanceof Error && error.name === "TimeoutError")
14985
+ return { phase, reason: "timeout" };
14986
+ if (error instanceof Error && error.name === "AbortError")
14987
+ return { phase, reason: "aborted" };
14988
+ return { phase, reason: phase === "response" ? "response_unreadable" : error instanceof TypeError ? "network_error" : "request_failed" };
14989
+ }
14922
14990
  function object2(value) {
14923
14991
  if (!value || typeof value !== "object" || Array.isArray(value))
14924
- throw new Error("invalid input");
14992
+ throw new HookFailure("invalid_shape", "invalid input");
14925
14993
  return value;
14926
14994
  }
14927
14995
  function textContent(value) {
@@ -14938,13 +15006,15 @@ function textContent(value) {
14938
15006
  function claudeTranscript(context) {
14939
15007
  const path = context["transcript_path"];
14940
15008
  if (typeof path !== "string" || !isAbsolute(path))
14941
- throw new Error("transcript path required");
15009
+ throw new HookFailure("transcript_path_required", "transcript path required");
14942
15010
  const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
14943
15011
  let raw;
14944
15012
  try {
14945
15013
  const stat = fstatSync(fd);
14946
- if (!stat.isFile() || stat.size > MAX_TRANSCRIPT_BYTES)
14947
- throw new Error("invalid transcript file");
15014
+ if (!stat.isFile())
15015
+ throw new HookFailure("transcript_not_regular", "invalid transcript file");
15016
+ if (stat.size > MAX_TRANSCRIPT_BYTES)
15017
+ throw new HookFailure("transcript_too_large", "invalid transcript file");
14948
15018
  const buffer = Buffer.alloc(MAX_TRANSCRIPT_BYTES + 1);
14949
15019
  let read = 0;
14950
15020
  while (read < buffer.length) {
@@ -14954,7 +15024,7 @@ function claudeTranscript(context) {
14954
15024
  read += count;
14955
15025
  }
14956
15026
  if (read > MAX_TRANSCRIPT_BYTES)
14957
- throw new Error("transcript grew beyond bound");
15027
+ throw new HookFailure("transcript_too_large", "transcript grew beyond bound");
14958
15028
  raw = buffer.subarray(0, read).toString("utf8");
14959
15029
  } finally {
14960
15030
  closeSync(fd);
@@ -14969,7 +15039,7 @@ function claudeTranscript(context) {
14969
15039
  if (row["type"] !== "user" && row["type"] !== "assistant")
14970
15040
  continue;
14971
15041
  if (row["sessionId"] !== undefined && row["sessionId"] !== context["session_id"])
14972
- throw new Error("transcript session mismatch");
15042
+ throw new HookFailure("transcript_session_mismatch", "transcript session mismatch");
14973
15043
  const message = object2(row["message"]);
14974
15044
  const text = textContent(message["content"]);
14975
15045
  if (!text.trim())
@@ -14994,54 +15064,72 @@ async function runClaudeStopHook(options = {}) {
14994
15064
  const report = options.stderr ?? ((text) => {
14995
15065
  process.stderr.write(text);
14996
15066
  });
15067
+ let phase = "input";
14997
15068
  try {
14998
15069
  const chunks = [];
14999
15070
  let bytes = 0;
15000
15071
  for await (const chunk of options.stdin ?? Bun.stdin.stream()) {
15001
15072
  bytes += chunk.byteLength;
15002
15073
  if (bytes > MAX_CONTEXT_BYTES)
15003
- throw new Error("context too large");
15074
+ throw new HookFailure("context_too_large", "context too large");
15004
15075
  chunks.push(chunk);
15005
15076
  }
15006
15077
  const context = object2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
15007
15078
  if (context["hook_event_name"] !== "Stop")
15008
- throw new Error("not a Stop event");
15079
+ throw new HookFailure("invalid_event", "not a Stop event");
15009
15080
  if (context["stop_hook_active"] === true)
15010
15081
  return 0;
15011
15082
  const session = context["session_id"];
15012
15083
  if (typeof session !== "string" || !session.trim() || session.length > 256)
15013
- throw new Error("session required");
15084
+ throw new HookFailure("invalid_session", "session required");
15085
+ phase = "transcript";
15014
15086
  const transcript = claudeTranscript(context);
15015
15087
  if (!transcript.trim())
15016
15088
  return 0;
15089
+ phase = "configuration";
15017
15090
  const client = options.client ?? new MementosClient({
15018
- fetch: (input, init) => fetch(input, { ...init, redirect: "error", signal: AbortSignal.timeout(5000) })
15091
+ fetch: async (input, init) => {
15092
+ const response = await fetch(input, { ...init, redirect: "error", signal: AbortSignal.timeout(5000) });
15093
+ phase = "response";
15094
+ return response;
15095
+ }
15019
15096
  });
15020
15097
  const authority = new URL(client.apiUrl);
15021
15098
  if (authority.protocol !== "https:" || !authority.pathname.endsWith("/v1"))
15022
- throw new Error("hosted v1 authority required");
15099
+ throw new HookFailure("hosted_authority_required", "hosted v1 authority required");
15100
+ const agentId = sanitizeAgentName(process.env["MEMENTOS_AGENT"]);
15101
+ phase = "request";
15023
15102
  await client.ingestSession({
15024
15103
  transcript,
15025
15104
  session_id: session,
15026
15105
  source: "claude-code",
15027
- ...process.env["MEMENTOS_AGENT"] ? { agent_id: process.env["MEMENTOS_AGENT"] } : {}
15106
+ ...agentId ? { agent_id: agentId } : {}
15028
15107
  });
15029
15108
  report(`[mementos] Session queued for hosted memory extraction.
15030
15109
  `);
15031
15110
  return 0;
15032
15111
  } catch (error) {
15033
- const status = error instanceof MementosError && Number.isInteger(error.status) ? ` (HTTP ${error.status})` : "";
15034
- report(`[mementos] Hosted session ingest failed${status}; no local fallback or automatic retry.
15112
+ const failure = failureCategory(error, phase);
15113
+ const status = error instanceof MementosError && error.code !== "MEMENTOS_RESPONSE_CONTRACT" && Number.isInteger(error.status) && error.status >= 100 && error.status <= 599 ? ` (HTTP ${error.status})` : "";
15114
+ report(`[mementos] Hosted session ingest failed${status} [phase=${failure.phase}; reason=${failure.reason}]; no local fallback or automatic retry.
15035
15115
  `);
15036
15116
  return 1;
15037
15117
  }
15038
15118
  }
15039
- var MAX_CONTEXT_BYTES, MAX_TRANSCRIPT_BYTES;
15119
+ var MAX_CONTEXT_BYTES, MAX_TRANSCRIPT_BYTES, HookFailure;
15040
15120
  var init_claude_stop_hook = __esm(() => {
15041
15121
  init_sdk();
15122
+ init_agent_name();
15042
15123
  init_redact();
15043
15124
  MAX_CONTEXT_BYTES = 64 * 1024;
15044
15125
  MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
15126
+ HookFailure = class HookFailure extends Error {
15127
+ reason;
15128
+ constructor(reason, message) {
15129
+ super(message);
15130
+ this.reason = reason;
15131
+ }
15132
+ };
15045
15133
  });
15046
15134
 
15047
15135
  // src/db/session-jobs.ts
@@ -16109,7 +16197,8 @@ function planClaudeHook(home, command = claudeHookCommand()) {
16109
16197
  const changed = !before || JSON.stringify(settings) !== JSON.stringify(JSON.parse(next));
16110
16198
  return { settingsPath, resolvedSettingsPath, legacyPath, directory, directory_sha256: directory.sha256, before, next, changed, settings_sha256: before ? digest2(before) : "absent", legacy_hook_sha256: legacyHash, command };
16111
16199
  }
16112
- function installClaudeHook(home, expected, command = claudeHookCommand()) {
16200
+ async function installClaudeHook(home, expected, command = claudeHookCommand(), options = {}) {
16201
+ const { planClaudeStopHookUpdate, applyAgentIntegration } = await import("@hasna/skills");
16113
16202
  const plan = planClaudeHook(home, command);
16114
16203
  if (plan.directory.alias && !expected.directorySha256)
16115
16204
  throw new Error("MEMENTOS_HOOK_DIRECTORY_PREIMAGE_REQUIRED");
@@ -16117,8 +16206,18 @@ function installClaudeHook(home, expected, command = claudeHookCommand()) {
16117
16206
  throw new Error("MEMENTOS_HOOK_DIRECTORY_PREIMAGE_CHANGED");
16118
16207
  if (plan.settings_sha256 !== expected.settingsSha256 || (plan.legacy_hook_sha256 ?? undefined) !== expected.legacyHookSha256)
16119
16208
  throw new Error("MEMENTOS_HOOK_PREIMAGE_CHANGED");
16120
- if (!plan.changed)
16209
+ const integrationOptions = {
16210
+ home,
16211
+ dataDir: options.skillsDataDir,
16212
+ expectedSettingsSha256: plan.settings_sha256,
16213
+ replacement: plan.changed ? plan.next : plan.before.toString("utf8")
16214
+ };
16215
+ const integrationPlan = planClaudeStopHookUpdate(integrationOptions);
16216
+ if (!plan.changed) {
16217
+ if (integrationPlan)
16218
+ applyAgentIntegration(integrationPlan);
16121
16219
  return { changed: false, settings_sha256: plan.settings_sha256 };
16220
+ }
16122
16221
  const directory = plan.directory.path;
16123
16222
  mkdirSync5(directory, { recursive: true, mode: 448 });
16124
16223
  const claimedDirectory = claudeDirectory(home);
@@ -16136,13 +16235,19 @@ function installClaudeHook(home, expected, command = claudeHookCommand()) {
16136
16235
  writeFileSync4(backup, plan.before, { flag: "wx", mode: 384 });
16137
16236
  if (backup && digest2(readRegular(backup)) !== plan.settings_sha256)
16138
16237
  throw new Error("MEMENTOS_HOOK_BACKUP_CONFLICT");
16139
- writeFileSync4(temporary, plan.next, { flag: "wx", mode: 384 });
16140
16238
  const current = planClaudeHook(home, command);
16141
16239
  if (current.directory_sha256 !== claimedDirectory.sha256)
16142
16240
  throw new Error("MEMENTOS_HOOK_DIRECTORY_PREIMAGE_CHANGED");
16143
16241
  if (current.settings_sha256 !== plan.settings_sha256 || current.legacy_hook_sha256 !== plan.legacy_hook_sha256)
16144
16242
  throw new Error("MEMENTOS_HOOK_PREIMAGE_CHANGED");
16145
- renameSync(temporary, plan.resolvedSettingsPath);
16243
+ if (integrationPlan) {
16244
+ applyAgentIntegration(integrationPlan);
16245
+ } else {
16246
+ if (planClaudeStopHookUpdate(integrationOptions))
16247
+ throw new Error("MEMENTOS_HOOK_SKILLS_POLICY_CHANGED");
16248
+ writeFileSync4(temporary, plan.next, { flag: "wx", mode: 384 });
16249
+ renameSync(temporary, plan.resolvedSettingsPath);
16250
+ }
16146
16251
  if (claudeDirectory(home).sha256 !== claimedDirectory.sha256 || digest2(readRegular(plan.resolvedSettingsPath)) !== digest2(plan.next))
16147
16252
  throw new Error("MEMENTOS_HOOK_READBACK_FAILED");
16148
16253
  } finally {
@@ -74685,7 +74790,7 @@ session_end = "bun ${script}"`);
74685
74790
  if (opts.apply) {
74686
74791
  if (!opts.expectSettingsSha256)
74687
74792
  throw new Error("Preview first; --apply requires --expect-settings-sha256");
74688
- outputJson(installClaudeHook2(homedir6(), { settingsSha256: opts.expectSettingsSha256, legacyHookSha256: opts.expectHookSha256, directorySha256: opts.expectDirectorySha256 }));
74793
+ outputJson(await installClaudeHook2(homedir6(), { settingsSha256: opts.expectSettingsSha256, legacyHookSha256: opts.expectHookSha256, directorySha256: opts.expectDirectorySha256 }));
74689
74794
  } else {
74690
74795
  const plan = planClaudeHook2(homedir6());
74691
74796
  outputJson({ changed: plan.changed, settings_path: plan.settingsPath, resolved_settings_path: plan.resolvedSettingsPath, directory_sha256: plan.directory_sha256, directory_alias: plan.directory.alias, command: plan.command, settings_sha256: plan.settings_sha256, legacy_hook_sha256: plan.legacy_hook_sha256 });
@@ -76144,7 +76249,7 @@ function registerInitCommand(program2) {
76144
76249
  try {
76145
76250
  const { planClaudeHook: planClaudeHook2, installClaudeHook: installClaudeHook2 } = await Promise.resolve().then(() => (init_claude_hook_install(), exports_claude_hook_install));
76146
76251
  const plan = planClaudeHook2(home);
76147
- const result = installClaudeHook2(home, { settingsSha256: plan.settings_sha256, legacyHookSha256: plan.legacy_hook_sha256 ?? undefined, directorySha256: plan.directory_sha256 });
76252
+ const result = await installClaudeHook2(home, { settingsSha256: plan.settings_sha256, legacyHookSha256: plan.legacy_hook_sha256 ?? undefined, directorySha256: plan.directory_sha256 });
76148
76253
  console.log(chalk41.green(result.changed ? " \u2713 Hosted Stop hook installed" : " \xB7 Hosted Stop hook already installed"));
76149
76254
  } catch {
76150
76255
  console.error(chalk41.red(" \u2717 Stop hook installation refused; run mementos session setup-hook --claude to inspect the preconditions"));
@@ -16,6 +16,13 @@ export interface AgentListFilter {
16
16
  * Returns null when neither source yields a non-empty name. An unreadable or
17
17
  * missing identity file is treated as absent — a write must never fail merely
18
18
  * because the identity file was mid-write.
19
+ *
20
+ * BOTH sources are sanitized: control-byte padding is stripped (station07's
21
+ * file held `autumn-bea\0\0`, and the padded value reached
22
+ * `/agents/autumn-bea%00%00` as a hosted 400) and the remainder must be a
23
+ * usable agent name. A value that is not usable — an error string written by a
24
+ * failed claim, say — is treated as absent rather than used, so the caller's
25
+ * own refusal path owns the outcome.
19
26
  */
20
27
  export declare function resolveWritingAgentName(): string | null;
21
28
  export declare function registerAgent(name: string, sessionId?: string, description?: string, role?: string, projectId?: string, db?: Database): Agent;
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/db/agents.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAQ/C,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAcvD;AAgBD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,EAClB,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,CA0EP;AAED,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,GAAG,IAAI,CA6Bd;AA+CD,wBAAgB,UAAU,CAAC,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAAC;AACnD,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAAC;AAsB5E,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAahE;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAAC;AAC/E,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,EACvB,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,EAAE,CAAC;AA0BX,wBAAgB,WAAW,CACzB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACtI,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,GAAG,IAAI,CAsDd"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/db/agents.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAS/C,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAcvD;AAgBD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,EAClB,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,CA0EP;AAED,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,GAAG,IAAI,CA6Bd;AA+CD,wBAAgB,UAAU,CAAC,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAAC;AACnD,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAAC;AAsB5E,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAahE;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAE,CAAC;AAC/E,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,EACvB,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,EAAE,CAAC;AA0BX,wBAAgB,WAAW,CACzB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACtI,EAAE,CAAC,EAAE,QAAQ,GACZ,KAAK,GAAG,IAAI,CAsDd"}
package/dist/index.js CHANGED
@@ -55476,6 +55476,18 @@ function getMemoryProjectLinkReceipt(memoryId, receiptId, identity = linkAuthori
55476
55476
  }
55477
55477
  // src/memory-project-link/index.ts
55478
55478
  init_schema();
55479
+ // src/lib/agent-name.ts
55480
+ function isUrlDotSegment(value) {
55481
+ const segment = value.toLowerCase();
55482
+ return segment === "." || segment === ".." || segment === "%2e" || segment === ".%2e" || segment === "%2e." || segment === "%2e%2e";
55483
+ }
55484
+ function agentPathSegment(name) {
55485
+ if (isUrlDotSegment(name)) {
55486
+ throw new Error(`Refusing to build an agent URL from ${JSON.stringify(name)}: the URL parser collapses a dot segment, ` + `so the request path would not be the one built here.`);
55487
+ }
55488
+ return encodeURIComponent(name);
55489
+ }
55490
+
55479
55491
  // src/db/agents.ts
55480
55492
  init_database();
55481
55493
  init_api_mode();
@@ -55555,7 +55567,7 @@ function registerAgent(name, sessionId, description, role, projectId, db) {
55555
55567
  }
55556
55568
  function getAgent(idOrName, db) {
55557
55569
  if (!db && isApiMode()) {
55558
- const { status, data } = apiJson("GET", `/agents/${encodeURIComponent(idOrName)}`, undefined, { allow404: true });
55570
+ const { status, data } = apiJson("GET", `/agents/${agentPathSegment(idOrName)}`, undefined, { allow404: true });
55559
55571
  if (status === 404 || !data)
55560
55572
  return null;
55561
55573
  return data;
@@ -55619,7 +55631,7 @@ function touchAgent(idOrName, db) {
55619
55631
  const agent2 = getAgent(idOrName);
55620
55632
  if (!agent2)
55621
55633
  return;
55622
- apiJson("PATCH", `/agents/${encodeURIComponent(agent2.id)}`, {});
55634
+ apiJson("PATCH", `/agents/${agentPathSegment(agent2.id)}`, {});
55623
55635
  return;
55624
55636
  }
55625
55637
  const d = db || getDatabase();
@@ -55645,7 +55657,7 @@ function listAgentsByProject(projectId, filterOrDb = {}, db) {
55645
55657
  }
55646
55658
  function updateAgent(id, updates, db) {
55647
55659
  if (!db && isApiMode()) {
55648
- const { status, data } = apiJson("PATCH", `/agents/${encodeURIComponent(id)}`, updates, { allow404: true });
55660
+ const { status, data } = apiJson("PATCH", `/agents/${agentPathSegment(id)}`, updates, { allow404: true });
55649
55661
  if (status === 404 || !data)
55650
55662
  return null;
55651
55663
  return data;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Hygiene for agent identity VALUES, shared by every reader in this package.
3
+ *
4
+ * WHY THIS EXISTS. On 2026-09-18 the fleet's machine identity surface
5
+ * (`~/.hasna/conversations/agent-id`, written by `conversations agents
6
+ * register`) was found holding two corrupt values: station07's was
7
+ * `autumn-bea\0\0` (NUL padded) and station01's was an error string with a
8
+ * literal newline, written by a failed claim. `resolveWritingAgentName()` read
9
+ * both at face value, and the padded name was interpolated into
10
+ * `/agents/autumn-bea%00%00` — a 400 from the hosted API, on a path that only
11
+ * ever receives a name.
12
+ *
13
+ * The rules are deliberately the same three the conversations CLI applies:
14
+ * control-byte PADDING is stripped, the remainder is trimmed, and what is left
15
+ * must be ONE token of `[A-Za-z0-9._-]` (at most 128 characters). A value that
16
+ * fails is refused, never "repaired" into something nobody wrote: an error
17
+ * message that landed in a name field is not an identity.
18
+ *
19
+ * This module has NO imports on purpose — it is reached from the db layer, the
20
+ * CLI, the MCP tools and the SDK, and must not drag any of them into a bundle.
21
+ */
22
+ /**
23
+ * Longest agent name this build will accept. Fleet names are short
24
+ * (`autumn-bear`, `chief-engineering-officer`); anything longer is a message
25
+ * that landed in a name field, not a name.
26
+ */
27
+ export declare const AGENT_NAME_MAX_LENGTH = 128;
28
+ /** Remove every control byte from a raw value. Pure. */
29
+ export declare function stripControlBytes(value: string): string;
30
+ /**
31
+ * Strip control-byte PADDING and surrounding whitespace from one raw value.
32
+ *
33
+ * This is the shape the fleet actually saw: station07's file held
34
+ * `autumn-bea\0\0`, and the padded value reached the hosted API as
35
+ * `/agents/autumn-bea%00%00`. The padding is never part of a name, so the
36
+ * readable bytes under it are kept.
37
+ *
38
+ * Only the EDGES are stripped. A control byte that survives in the middle is
39
+ * corruption that cannot be repaired by deleting it — joining `autumn\0bear`
40
+ * into `autumnbear` would mint a name nobody wrote — so the sanitizer below
41
+ * refuses such a value instead. Pure.
42
+ */
43
+ export declare function trimControlBytes(value: string): string;
44
+ /**
45
+ * Whether a value can be used as an agent name at all.
46
+ *
47
+ * A name is interpolated into URL paths and into attribution columns, so it
48
+ * must be one token — no spaces, no colons, no slashes — and contain at least
49
+ * one character that is not a dot. The last rule is what refuses `.` and `..`:
50
+ * a pure dot segment is collapsed by the URL parser, so `/api/agents/..`
51
+ * resolves to `/v1/` and the request silently changes shape (measured in review
52
+ * of hasna/apps#2619, todos A3-00715). `Error: already-claimed` fails the
53
+ * charset and is refused instead of being registered as an agent.
54
+ */
55
+ export declare function isValidAgentName(name: string): boolean;
56
+ /**
57
+ * The segments a URL parser collapses while resolving a path: `.` and `..`,
58
+ * plus their percent-encoded spellings (the WHATWG URL spec's "single-dot" and
59
+ * "double-dot path segment").
60
+ *
61
+ * `encodeURIComponent` does NOT touch a dot, so encoding alone cannot
62
+ * neutralize these: `/api/agents/..` is resolved by the fetch URL parser to
63
+ * `/v1/` and the request silently changes shape — measured in review of
64
+ * hasna/apps#2619 (todos A3-00715), where the SDK's `getAgent("..")` issued
65
+ * `GET /v1/`. This predicate is the rule for the URL BOUNDARY below, where a
66
+ * value can be an explicit argument rather than a validated name;
67
+ * {@link isValidAgentName} applies the name-side equivalent (at least one
68
+ * non-dot character). Pure.
69
+ */
70
+ export declare function isUrlDotSegment(value: string): boolean;
71
+ /**
72
+ * Encode one agent name for an `/agents/<name>` path segment, refusing a value
73
+ * the URL parser would collapse to a different path.
74
+ *
75
+ * This is the boundary rule for callers that take a name straight from argv, an
76
+ * MCP argument, a store row or an SDK call — places where
77
+ * {@link isValidAgentName} is not on the path. Everything else is
78
+ * percent-encoded; a dot segment is refused rather than encoded, because
79
+ * encoding cannot express it (see {@link isUrlDotSegment}).
80
+ *
81
+ * @throws {Error} when `name` is a URL dot segment.
82
+ */
83
+ export declare function agentPathSegment(name: string): string;
84
+ /**
85
+ * Read one raw value into a usable agent name, or `null` when nothing usable
86
+ * remains. Control-byte padding is stripped and the result is trimmed before
87
+ * validation, so `autumn-bea\0\0\n` reads back as `autumn-bea`; a value with
88
+ * interior corruption, or one that is not a single token, is refused.
89
+ *
90
+ * `null` is the only failure value: callers decide whether an unusable name is
91
+ * absent (the file fallback in `resolveWritingAgentName`) or a refusal. Pure.
92
+ */
93
+ export declare function sanitizeAgentName(raw: string | null | undefined): string | null;
94
+ //# sourceMappingURL=agent-name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-name.d.ts","sourceRoot":"","sources":["../../src/lib/agent-name.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAWzC,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOtD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAUtD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQrD;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAO/E"}
@@ -21,7 +21,9 @@ export declare function installClaudeHook(home: string, expected: {
21
21
  settingsSha256: string;
22
22
  legacyHookSha256?: string;
23
23
  directorySha256?: string;
24
- }, command?: string): {
24
+ }, command?: string, options?: {
25
+ skillsDataDir?: string;
26
+ }): Promise<{
25
27
  changed: boolean;
26
28
  settings_sha256: string;
27
29
  backup?: undefined;
@@ -29,5 +31,5 @@ export declare function installClaudeHook(home: string, expected: {
29
31
  changed: boolean;
30
32
  settings_sha256: string;
31
33
  backup: string | null;
32
- };
34
+ }>;
33
35
  //# sourceMappingURL=claude-hook-install.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"claude-hook-install.d.ts","sourceRoot":"","sources":["../../src/lib/claude-hook-install.ts"],"names":[],"mappings":"AAYA,wBAAgB,iBAAiB,IAAI,MAAM,CAI1C;AAuBD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAsB;;;;;;;;;;;;;;;;;EAmCzE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,SAAsB;;;;;;;;EA+BvK"}
1
+ {"version":3,"file":"claude-hook-install.d.ts","sourceRoot":"","sources":["../../src/lib/claude-hook-install.ts"],"names":[],"mappings":"AAYA,wBAAgB,iBAAiB,IAAI,MAAM,CAI1C;AAuBD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAsB;;;;;;;;;;;;;;;;;EAmCzE;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,SAAsB,EAAE,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO;;;;;;;;GAiDvN"}
@@ -1 +1 @@
1
- {"version":3,"file":"claude-stop-hook.d.ts","sourceRoot":"","sources":["../../src/lib/claude-stop-hook.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAiB,MAAM,iBAAiB,CAAC;AAoBhE,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAuCzE;AAED,wBAAsB,iBAAiB,CAAC,OAAO,GAAE;IAC/C,KAAK,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,CAAC,EAAE,cAAc,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsCvB"}
1
+ {"version":3,"file":"claude-stop-hook.d.ts","sourceRoot":"","sources":["../../src/lib/claude-stop-hook.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAsC,MAAM,iBAAiB,CAAC;AA0DrF,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAwCzE;AAED,wBAAsB,iBAAiB,CAAC,OAAO,GAAE;IAC/C,KAAK,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,CAAC,EAAE,cAAc,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmDvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"open-sessions-connector.d.ts","sourceRoot":"","sources":["../../src/lib/open-sessions-connector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,2BAA2B;IAC1C,iCAAiC;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,8CAA8C;IAC9C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAA8B;gBAEhC,MAAM,EAAE,2BAA2B;IAI/C;;OAEG;IACG,aAAa,CACjB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GACrD,OAAO,CAAC,YAAY,CAAC;IAwCxB;;OAEG;IACG,kBAAkB,CAAC,OAAO,GAAE;QAChC,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;YAelB,sBAAsB;YAiBtB,oBAAoB;CAqBnC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,qBAAqB,CAWxD"}
1
+ {"version":3,"file":"open-sessions-connector.d.ts","sourceRoot":"","sources":["../../src/lib/open-sessions-connector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,2BAA2B;IAC1C,iCAAiC;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,8CAA8C;IAC9C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAA8B;gBAEhC,MAAM,EAAE,2BAA2B;IAI/C;;OAEG;IACG,aAAa,CACjB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GACrD,OAAO,CAAC,YAAY,CAAC;IAwCxB;;OAEG;IACG,kBAAkB,CAAC,OAAO,GAAE;QAChC,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;YAelB,sBAAsB;YAiBtB,oBAAoB;CAqBnC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,qBAAqB,CAaxD"}
package/dist/mcp/index.js CHANGED
@@ -61981,6 +61981,20 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
61981
61981
 
61982
61982
  // src/db/agents.ts
61983
61983
  init_types();
61984
+
61985
+ // src/lib/agent-name.ts
61986
+ function isUrlDotSegment(value) {
61987
+ const segment = value.toLowerCase();
61988
+ return segment === "." || segment === ".." || segment === "%2e" || segment === ".%2e" || segment === "%2e." || segment === "%2e%2e";
61989
+ }
61990
+ function agentPathSegment(name) {
61991
+ if (isUrlDotSegment(name)) {
61992
+ throw new Error(`Refusing to build an agent URL from ${JSON.stringify(name)}: the URL parser collapses a dot segment, ` + `so the request path would not be the one built here.`);
61993
+ }
61994
+ return encodeURIComponent(name);
61995
+ }
61996
+
61997
+ // src/db/agents.ts
61984
61998
  init_database();
61985
61999
  init_api_mode();
61986
62000
  var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
@@ -62059,7 +62073,7 @@ function registerAgent(name, sessionId, description, role, projectId, db) {
62059
62073
  }
62060
62074
  function getAgent(idOrName, db) {
62061
62075
  if (!db && isApiMode()) {
62062
- const { status, data } = apiJson("GET", `/agents/${encodeURIComponent(idOrName)}`, undefined, { allow404: true });
62076
+ const { status, data } = apiJson("GET", `/agents/${agentPathSegment(idOrName)}`, undefined, { allow404: true });
62063
62077
  if (status === 404 || !data)
62064
62078
  return null;
62065
62079
  return data;
@@ -62123,7 +62137,7 @@ function touchAgent(idOrName, db) {
62123
62137
  const agent2 = getAgent(idOrName);
62124
62138
  if (!agent2)
62125
62139
  return;
62126
- apiJson("PATCH", `/agents/${encodeURIComponent(agent2.id)}`, {});
62140
+ apiJson("PATCH", `/agents/${agentPathSegment(agent2.id)}`, {});
62127
62141
  return;
62128
62142
  }
62129
62143
  const d = db || getDatabase();
@@ -62149,7 +62163,7 @@ function listAgentsByProject(projectId, filterOrDb = {}, db) {
62149
62163
  }
62150
62164
  function updateAgent(id, updates, db) {
62151
62165
  if (!db && isApiMode()) {
62152
- const { status, data } = apiJson("PATCH", `/agents/${encodeURIComponent(id)}`, updates, { allow404: true });
62166
+ const { status, data } = apiJson("PATCH", `/agents/${agentPathSegment(id)}`, updates, { allow404: true });
62153
62167
  if (status === 404 || !data)
62154
62168
  return null;
62155
62169
  return data;
@@ -581,7 +581,8 @@ export interface MementosClientConfig {
581
581
  export declare class MementosError extends Error {
582
582
  readonly status: number;
583
583
  readonly details?: unknown | undefined;
584
- constructor(message: string, status: number, details?: unknown | undefined);
584
+ readonly code?: "MEMENTOS_RESPONSE_CONTRACT" | undefined;
585
+ constructor(message: string, status: number, details?: unknown | undefined, code?: "MEMENTOS_RESPONSE_CONTRACT" | undefined);
585
586
  }
586
587
  /**
587
588
  * Thrown when the SDK is asked to reach the memory store and NOTHING resolves: