@pleri/olam-cli 0.1.208 → 0.1.210

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.
@@ -16387,16 +16387,25 @@ function settingsBackupDir() {
16387
16387
  return override;
16388
16388
  return path27.join(os12.homedir(), ".olam", "state", "settings-backups");
16389
16389
  }
16390
+ function entryKey(entry) {
16391
+ const cmds = commandsInEntry(entry);
16392
+ const cmdKey = cmds.length > 0 ? cmds[0] : "";
16393
+ return `${entry.matcher ?? ""}\0${cmdKey}`;
16394
+ }
16390
16395
  function dedupeByMatcher(entries) {
16391
16396
  const map2 = /* @__PURE__ */ new Map();
16392
16397
  for (const e of entries) {
16393
- const cmds = commandsInEntry(e);
16394
- const cmdKey = cmds.length > 0 ? cmds[0] : "";
16395
- const key = `${e.matcher ?? ""}\0${cmdKey}`;
16396
- map2.set(key, e);
16398
+ map2.set(entryKey(e), e);
16397
16399
  }
16398
16400
  return [...map2.values()];
16399
16401
  }
16402
+ function reclaimLegacyDuplicates(preserved, olamFinal) {
16403
+ if (olamFinal.length === 0)
16404
+ return { kept: preserved, reclaimedCount: 0 };
16405
+ const olamKeys = new Set(olamFinal.map(entryKey));
16406
+ const kept = preserved.filter((e) => !olamKeys.has(entryKey(e)));
16407
+ return { kept, reclaimedCount: preserved.length - kept.length };
16408
+ }
16400
16409
  function commandsInEntry(entry) {
16401
16410
  const cmds = [];
16402
16411
  const direct = entry["command"];
@@ -16497,6 +16506,7 @@ function mergeSettings(input) {
16497
16506
  ]);
16498
16507
  let hooksAdded = 0;
16499
16508
  let dualWriteDeduped = 0;
16509
+ let legacyReclaimed = 0;
16500
16510
  const dualWriteDroppedCommands = [];
16501
16511
  for (const cat of categories) {
16502
16512
  const existing = Array.isArray(existingHooks[cat]) ? existingHooks[cat] : [];
@@ -16506,7 +16516,9 @@ function mergeSettings(input) {
16506
16516
  const { kept: olamFinal, droppedCount, droppedCommands } = applyDualWriteDedup(olamDeduped, preserved);
16507
16517
  dualWriteDeduped += droppedCount;
16508
16518
  dualWriteDroppedCommands.push(...droppedCommands);
16509
- mergedHooks[cat] = [...preserved, ...olamFinal];
16519
+ const { kept: preservedFinal, reclaimedCount } = reclaimLegacyDuplicates(preserved, olamFinal);
16520
+ legacyReclaimed += reclaimedCount;
16521
+ mergedHooks[cat] = [...preservedFinal, ...olamFinal];
16510
16522
  hooksAdded += olamFinal.length;
16511
16523
  }
16512
16524
  const permSet = /* @__PURE__ */ new Set();
@@ -16536,7 +16548,14 @@ function mergeSettings(input) {
16536
16548
  const tmp = `${settingsPath}.tmp-${process.pid}`;
16537
16549
  fs29.writeFileSync(tmp, JSON.stringify(next, null, 2) + "\n", { mode: 420 });
16538
16550
  fs29.renameSync(tmp, settingsPath);
16539
- return { backupPath, hooksAdded, permissionsCount: permSet.size, dualWriteDeduped, dualWriteDroppedCommands };
16551
+ return {
16552
+ backupPath,
16553
+ hooksAdded,
16554
+ permissionsCount: permSet.size,
16555
+ dualWriteDeduped,
16556
+ dualWriteDroppedCommands,
16557
+ legacyReclaimed
16558
+ };
16540
16559
  }
16541
16560
  var OLAM_SKILLS_MARKER, BACKUP_RETENTION, DUAL_WRITE_DEDUP_RULES;
16542
16561
  var init_settings_merger = __esm({
@@ -49076,7 +49095,10 @@ import * as path46 from "node:path";
49076
49095
  import * as os23 from "node:os";
49077
49096
 
49078
49097
  // ../core/dist/kg/hook-template.js
49079
- var KG_HOOK_SENTINEL = "kg-service-v3-classifier-hook";
49098
+ var KG_HOOK_SENTINEL = "kg-service-v4-classifier-hook";
49099
+ var KG_HOOK_SENTINEL_PREFIX = "kg-service-v";
49100
+ var KG_HOOK_PROMPT_SENTINEL = "kg-service-prompt-v4-classifier-hook";
49101
+ var KG_HOOK_PROMPT_SENTINEL_PREFIX = "kg-service-prompt-v";
49080
49102
  function defaultUrl(flavor) {
49081
49103
  if (flavor === "host")
49082
49104
  return "http://127.0.0.1:9997/classify";
@@ -49084,6 +49106,33 @@ function defaultUrl(flavor) {
49084
49106
  return "http://host.docker.internal:9997/classify";
49085
49107
  return "";
49086
49108
  }
49109
+ var TERM_EXTRACT_PY = `python3 -c 'import sys
49110
+ cmd=sys.stdin.read().strip()
49111
+ toks=cmd.split()
49112
+ TOOLS=("grep","rg","ripgrep","find","fd","ack","ag")
49113
+ DIRS=("src","lib","app","test","spec","packages")
49114
+ QUOTES=chr(34)+chr(39)+chr(96)
49115
+ i=0
49116
+ if toks and toks[0]=="git" and len(toks)>1 and toks[1]=="grep": i=2
49117
+ elif toks and toks[0] in TOOLS: i=1
49118
+ term=""
49119
+ for t in toks[i:]:
49120
+ if t.startswith("-"): continue
49121
+ s=t.strip(QUOTES)
49122
+ if not s: continue
49123
+ if "/" in s or s.endswith("/") or s in DIRS or s in (".",".."): continue
49124
+ term=s; break
49125
+ print(term if term else cmd)' 2>/dev/null`;
49126
+ var PROMPT_TERM_EXTRACT_PY = `python3 -c 'import sys,re
49127
+ p=sys.stdin.read().strip()
49128
+ Q=chr(34); B=chr(96); A=chr(39)
49129
+ pats=[B+"([^"+B+"]{3,})"+B, Q+"([^"+Q+"]{3,})"+Q, A+"([^"+A+"]{3,})"+A, "\\\\b[A-Za-z_$][\\\\w$]*(?:\\\\.[A-Za-z_$][\\\\w$]*)+\\\\b", "\\\\b[a-zA-Z_$][\\\\w$]*[A-Z][\\\\w$]*\\\\b", "\\\\b[a-z_][a-z0-9_]*_[a-z0-9_]+\\\\b"]
49130
+ term=""
49131
+ for pat in pats:
49132
+ m=re.search(pat,p)
49133
+ if m:
49134
+ term=(m.group(1) if m.groups() else m.group(0)).strip(); break
49135
+ print(term if term else p)' 2>/dev/null`;
49087
49136
  function buildHookCommand(opts) {
49088
49137
  const url3 = opts.url ?? defaultUrl(opts.flavor);
49089
49138
  const extractCmd = `python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))" 2>/dev/null`;
@@ -49091,20 +49140,26 @@ function buildHookCommand(opts) {
49091
49140
  try:
49092
49141
  d=json.loads(sys.stdin.read())
49093
49142
  route=d.get("route","")
49143
+ qnodes=[]
49144
+ try:
49145
+ qd=json.loads(os.environ.get("KG_QUERY_NODES","") or "{}")
49146
+ if isinstance(qd,dict) and qd.get("ok") and isinstance(qd.get("nodes"),list):
49147
+ qnodes=qd["nodes"]
49148
+ except Exception: qnodes=[]
49149
+ real_n=len(qnodes)
49094
49150
  if route:
49095
49151
  if route in ("kg","both"):
49096
49152
  label=d.get("top_match") or d.get("reason","")
49097
49153
  layer=d.get("layer","?")
49098
- nodes=d.get("nodes_matched",0)
49154
+ nodes=real_n if real_n else d.get("nodes_matched",0)
49099
49155
  saved=(d.get("savings") or {}).get("saved_tokens_est",0)
49100
49156
  saved_k=round(saved/1000)
49101
49157
  sys.stderr.write(f"\\x1b[32m\\u29bf\\u29bf\\x1b[0m KG hit \\u2713 {label} \\u2192 L{layer}/{route} \\u00b7 {nodes} nodes \\u00b7 \\x1b[32m~{saved_k}k tokens saved\\x1b[0m\\n")
49102
- else:
49158
+ elif route=="grep":
49103
49159
  sys.stderr.write("\\U0001f50d Grep used\\n")
49104
- if route and route != "grep":
49160
+ if route in ("kg","both"):
49105
49161
  label=d.get("top_match") or d.get("reason","")
49106
49162
  layer=d.get("layer","?")
49107
- nodes=d.get("nodes_matched",0)
49108
49163
  saved=(d.get("savings") or {}).get("saved_tokens_est",0)
49109
49164
  saved_k=round(saved/1000)
49110
49165
  q=os.environ.get("KG_QUERY","")[:60]
@@ -49114,20 +49169,44 @@ try:
49114
49169
  rel=any(k in ql for k in ("call","caller","import","uses","used","reference","who "))
49115
49170
  g="olam kg mirror graph \\"" + sym + "\\" --workspace <ws>"
49116
49171
  hint=g + (" --relates" if rel else "") + " (where X is defined; add --relates for what calls/imports it; --repo <name> to browse a repo)"
49117
- print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":f"[kg-classifier L{layer}|{route}] {label[:140]}\\n\\u21b3 graph: {hint}"}}))
49172
+ if qnodes:
49173
+ lines=[]
49174
+ for n in qnodes[:5]:
49175
+ s=str(n.get("symbol") or n.get("label") or "?")
49176
+ f=str(n.get("file") or "")
49177
+ loc=str(n.get("loc") or "")
49178
+ lines.append(f" {s} {f}:{loc}" if loc else f" {s} {f}")
49179
+ body="[kg-classifier L"+str(layer)+"|"+route+"] "+label[:140]+"\\n\\u21b3 nodes ("+str(real_n)+"):\\n"+"\\n".join(lines)+"\\n\\u21b3 graph: "+hint
49180
+ else:
49181
+ body="[kg-classifier L"+str(layer)+"|"+route+"] "+label[:140]+"\\n\\u21b3 graph: "+hint
49182
+ print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":body}}))
49118
49183
  except Exception: pass' 2>/dev/null`;
49119
49184
  const isCloud = opts.flavor === "cloud-sandbox";
49120
49185
  const resolvedUrl = isCloud ? '"${OLAM_KG_PROXY_URL:-}/v1/classify"' : url3;
49121
49186
  const authHeader2 = isCloud ? `-H "Authorization: Bearer \${OLAM_KG_PROXY_BEARER:-}"` : "";
49122
49187
  const cloudGuard = isCloud ? `if [ -z "\${OLAM_KG_PROXY_URL:-}" ] || [ -z "\${OLAM_KG_PROXY_BEARER:-}" ]; then exit 0; fi; ` : "";
49123
- const wsField = isCloud ? `,\\"workspace\\":\\"\${OLAM_KG_PROXY_WORKSPACE:-}\\"` : "";
49124
- const curlPost = `RESP=$(curl -s --max-time 1 -X POST -H 'Content-Type: application/json' ${authHeader2} -d "{\\"q\\":\\"$(echo \\"$CMD\\" | head -c 200 | tr '\\"' ' ')\\"${wsField}}" ${resolvedUrl} 2>/dev/null)`;
49125
- const hostRemoteFallback = opts.flavor === "host" ? `; if [ -z "$RESP" ] && [ -r "$HOME/.olam/kg-proxy-url" ] && [ -r "$HOME/.olam/kg-proxy-bearer" ]; then KG_ORIGIN=$(sed 's|\\(https*://[^/]*\\).*|\\1|' "$HOME/.olam/kg-proxy-url" 2>/dev/null); RESP=$(curl -s --max-time 3 -X POST -H 'Content-Type: application/json' -H "Authorization: Bearer $(cat "$HOME/.olam/kg-proxy-bearer")" -d "{\\"q\\":\\"$(echo \\"$CMD\\" | head -c 200 | tr '\\"' ' ')\\"}" "$KG_ORIGIN/v1/classify" 2>/dev/null); fi` : "";
49188
+ const isHostOrWorld = opts.flavor === "host" || opts.flavor === "world";
49189
+ const wsResolve = isHostOrWorld ? `KG_WS="\${OLAM_KG_WORKSPACE:-$(cat "\${OLAM_HOME:-$HOME/.olam}/kg-default-workspace" 2>/dev/null || echo atlas-one)}"; ` : "";
49190
+ const wsField = isCloud ? `,\\"workspace\\":\\"\${OLAM_KG_PROXY_WORKSPACE:-}\\"` : `,\\"workspace\\":\\"$KG_WS\\"`;
49191
+ const termExtract = TERM_EXTRACT_PY;
49192
+ const qTermLocal = `$(echo \\"$KG_TERM\\" | head -c 200 | tr '\\"' ' ')`;
49193
+ const setTerm = `KG_TERM=$(echo "$CMD" | ${termExtract}); `;
49194
+ const curlPost = `${wsResolve}${setTerm}RESP=$(curl -s --max-time 1 -X POST -H 'Content-Type: application/json' ${authHeader2} -d "{\\"q\\":\\"${qTermLocal}\\"${wsField}}" ${resolvedUrl} 2>/dev/null)`;
49195
+ const hostRemoteFallback = opts.flavor === "host" ? `; if [ -r "$HOME/.olam/kg-proxy-url" ] && [ -r "$HOME/.olam/kg-proxy-bearer" ]; then KG_ORIGIN=$(sed 's|\\(https*://[^/]*\\).*|\\1|' "$HOME/.olam/kg-proxy-url" 2>/dev/null); if [ -z "$RESP" ]; then RESP=$(curl -s --max-time 3 -X POST -H 'Content-Type: application/json' -H "Authorization: Bearer $(cat "$HOME/.olam/kg-proxy-bearer")" -d "{\\"q\\":\\"${qTermLocal}\\"${wsField}}" "$KG_ORIGIN/v1/classify" 2>/dev/null); fi; fi` : "";
49196
+ const routeIsKg = `echo "$RESP" | grep -Eq '"route"[[:space:]]*:[[:space:]]*"(kg|both)"'`;
49197
+ const queryBody = `-d "{\\"q\\":\\"${qTermLocal}\\",\\"limit\\":5${wsField}}"`;
49198
+ let queryFollowup = "";
49199
+ if (opts.flavor === "host") {
49200
+ queryFollowup = `; KG_QUERY_NODES=""; if [ -n "$RESP" ] && ${routeIsKg} && [ -n "$KG_ORIGIN" ] && [ -r "$HOME/.olam/kg-proxy-bearer" ]; then KG_QUERY_NODES=$(curl -s --max-time 3 -X POST -H 'Content-Type: application/json' -H "Authorization: Bearer $(cat "$HOME/.olam/kg-proxy-bearer")" ${queryBody} "$KG_ORIGIN/v1/query" 2>/dev/null); fi`;
49201
+ } else if (isCloud) {
49202
+ queryFollowup = `; KG_QUERY_NODES=""; if [ -n "$RESP" ] && ${routeIsKg}; then KG_QUERY_NODES=$(curl -s --max-time 3 -X POST -H 'Content-Type: application/json' ${authHeader2} ${queryBody} "\${OLAM_KG_PROXY_URL:-}/v1/query" 2>/dev/null); fi`;
49203
+ }
49204
+ const emitLine = `export KG_QUERY="$(echo \\"$CMD\\" | head -c 60 | tr '\\"' ' ')"; export KG_QUERY_NODES="\${KG_QUERY_NODES:-}"; echo "$RESP" | ${emitContext}`;
49126
49205
  return [
49127
49206
  `KG_SENTINEL=${KG_HOOK_SENTINEL}`,
49128
49207
  `CMD=$(${extractCmd})`,
49129
- `case "$CMD" in grep\\ *|grep|git\\ grep\\ *|rg\\ *|ripgrep\\ *|find\\ *|fd\\ *|ack\\ *|ag\\ *) ${cloudGuard}${curlPost}${hostRemoteFallback}`,
49130
- `KG_QUERY="$(echo \\"$CMD\\" | head -c 60 | tr '\\"' ' ')" echo "$RESP" | ${emitContext}`,
49208
+ `case "$CMD" in grep\\ *|grep|git\\ grep\\ *|rg\\ *|ripgrep\\ *|find\\ *|fd\\ *|ack\\ *|ag\\ *) ${cloudGuard}${curlPost}${hostRemoteFallback}${queryFollowup}`,
49209
+ emitLine,
49131
49210
  `;; esac`
49132
49211
  ].join("; ");
49133
49212
  }
@@ -49142,6 +49221,77 @@ function buildHookMatcherEntry(opts) {
49142
49221
  ]
49143
49222
  };
49144
49223
  }
49224
+ function buildPromptHookCommand(opts) {
49225
+ const url3 = opts.url ?? defaultUrl(opts.flavor);
49226
+ const isCloud = opts.flavor === "cloud-sandbox";
49227
+ const isHostOrWorld = opts.flavor === "host" || opts.flavor === "world";
49228
+ const extractCmd = `python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('prompt',''))" 2>/dev/null`;
49229
+ const resolvedUrl = isCloud ? '"${OLAM_KG_PROXY_URL:-}/v1/classify"' : url3;
49230
+ const authHeader2 = isCloud ? `-H "Authorization: Bearer \${OLAM_KG_PROXY_BEARER:-}"` : "";
49231
+ const cloudGuard = isCloud ? `if [ -z "\${OLAM_KG_PROXY_URL:-}" ] || [ -z "\${OLAM_KG_PROXY_BEARER:-}" ]; then exit 0; fi; ` : "";
49232
+ const wsResolve = isHostOrWorld ? `KG_WS="\${OLAM_KG_WORKSPACE:-$(cat "\${OLAM_HOME:-$HOME/.olam}/kg-default-workspace" 2>/dev/null || echo atlas-one)}"; ` : "";
49233
+ const wsField = isCloud ? `,\\"workspace\\":\\"\${OLAM_KG_PROXY_WORKSPACE:-}\\"` : `,\\"workspace\\":\\"$KG_WS\\"`;
49234
+ const classifyPost = `${wsResolve}RESP=$(curl -s --max-time 1 -X POST -H 'Content-Type: application/json' ${authHeader2} -d "{\\"q\\":\\"$(echo \\"$PROMPT\\" | head -c 200 | tr '\\"' ' ')\\"${wsField}}" ${resolvedUrl} 2>/dev/null)`;
49235
+ const hostRemoteFallback = opts.flavor === "host" ? `; if [ -r "$HOME/.olam/kg-proxy-url" ] && [ -r "$HOME/.olam/kg-proxy-bearer" ]; then KG_ORIGIN=$(sed 's|\\(https*://[^/]*\\).*|\\1|' "$HOME/.olam/kg-proxy-url" 2>/dev/null); if [ -z "$RESP" ]; then RESP=$(curl -s --max-time 3 -X POST -H 'Content-Type: application/json' -H "Authorization: Bearer $(cat "$HOME/.olam/kg-proxy-bearer")" -d "{\\"q\\":\\"$(echo \\"$PROMPT\\" | head -c 200 | tr '\\"' ' ')\\"${wsField}}" "$KG_ORIGIN/v1/classify" 2>/dev/null); fi; fi` : "";
49236
+ const setTerm = `KG_TERM=$(echo "$PROMPT" | ${PROMPT_TERM_EXTRACT_PY}); `;
49237
+ const qTerm = `$(echo \\"$KG_TERM\\" | head -c 200 | tr '\\"' ' ')`;
49238
+ const routeIsKg = `echo "$RESP" | grep -Eq '"route"[[:space:]]*:[[:space:]]*"(kg|both)"'`;
49239
+ const queryBody = `-d "{\\"q\\":\\"${qTerm}\\",\\"limit\\":5${wsField}}"`;
49240
+ let queryFollowup = "";
49241
+ if (opts.flavor === "host") {
49242
+ queryFollowup = `; KG_QUERY_NODES=""; if [ -n "$RESP" ] && ${routeIsKg} && [ -n "$KG_ORIGIN" ] && [ -r "$HOME/.olam/kg-proxy-bearer" ]; then ${setTerm}KG_QUERY_NODES=$(curl -s --max-time 3 -X POST -H 'Content-Type: application/json' -H "Authorization: Bearer $(cat "$HOME/.olam/kg-proxy-bearer")" ${queryBody} "$KG_ORIGIN/v1/query" 2>/dev/null); fi`;
49243
+ } else if (isCloud) {
49244
+ queryFollowup = `; KG_QUERY_NODES=""; if [ -n "$RESP" ] && ${routeIsKg}; then ${setTerm}KG_QUERY_NODES=$(curl -s --max-time 3 -X POST -H 'Content-Type: application/json' ${authHeader2} ${queryBody} "\${OLAM_KG_PROXY_URL:-}/v1/query" 2>/dev/null); fi`;
49245
+ }
49246
+ const emitContext = `python3 -c 'import json,sys,os
49247
+ try:
49248
+ d=json.loads(sys.stdin.read())
49249
+ route=d.get("route","")
49250
+ if route not in ("kg","both"): sys.exit(0)
49251
+ qnodes=[]
49252
+ try:
49253
+ qd=json.loads(os.environ.get("KG_QUERY_NODES","") or "{}")
49254
+ if isinstance(qd,dict) and qd.get("ok") and isinstance(qd.get("nodes"),list):
49255
+ qnodes=qd["nodes"]
49256
+ except Exception: qnodes=[]
49257
+ layer=d.get("layer","?")
49258
+ cands=d.get("candidate_symbols") or []
49259
+ sym=(cands[0] if cands else "") or "<symbol>"
49260
+ q=os.environ.get("KG_QUERY","")[:60]
49261
+ ql=q.lower()
49262
+ rel=any(k in ql for k in ("call","caller","import","uses","used","reference","who "))
49263
+ g="olam kg mirror graph \\"" + sym + "\\" --workspace <ws>"
49264
+ hint=g + (" --relates" if rel else "") + " (where X is defined; add --relates for what calls/imports it; --repo <name> to browse a repo)"
49265
+ if qnodes:
49266
+ lines=[]
49267
+ for n in qnodes[:5]:
49268
+ s=str(n.get("symbol") or n.get("label") or "?")
49269
+ f=str(n.get("file") or "")
49270
+ loc=str(n.get("loc") or "")
49271
+ lines.append(f" {s} {f}:{loc}" if loc else f" {s} {f}")
49272
+ body="[kg L"+str(layer)+"|"+route+"] this looks symbol-shaped \u2014 the KG already has:\\n\\u21b3 nodes ("+str(len(qnodes))+"):\\n"+"\\n".join(lines)+"\\n\\u21b3 graph: "+hint
49273
+ else:
49274
+ body="[kg L"+str(layer)+"|"+route+"] this looks symbol-shaped \u2014 prefer the KG over grep.\\n\\u21b3 graph: "+hint
49275
+ print(json.dumps({"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":body}}))
49276
+ except Exception: pass' 2>/dev/null`;
49277
+ const emitLine = `export KG_QUERY="$(echo \\"$PROMPT\\" | head -c 60 | tr '\\"' ' ')"; export KG_QUERY_NODES="\${KG_QUERY_NODES:-}"; echo "$RESP" | ${emitContext}`;
49278
+ return [
49279
+ `KG_SENTINEL=${KG_HOOK_PROMPT_SENTINEL}`,
49280
+ `PROMPT=$(${extractCmd})`,
49281
+ // No search-tool gate: classify every non-empty prompt. Empty prompt → no-op.
49282
+ `if [ -n "$PROMPT" ]; then ${cloudGuard}${classifyPost}${hostRemoteFallback}${queryFollowup}; ${emitLine}; fi`
49283
+ ].join("; ");
49284
+ }
49285
+ function buildPromptHookMatcherEntry(opts) {
49286
+ return {
49287
+ hooks: [
49288
+ {
49289
+ type: "command",
49290
+ command: buildPromptHookCommand(opts)
49291
+ }
49292
+ ]
49293
+ };
49294
+ }
49145
49295
 
49146
49296
  // ../skill-runtime/dist/skills/kg-install-hook.js
49147
49297
  init_v3();
@@ -49190,9 +49340,18 @@ function register31(server, _ctx, _initError) {
49190
49340
  ensureHook: {
49191
49341
  stage: "PreToolUse",
49192
49342
  sentinel: KG_HOOK_SENTINEL,
49343
+ staleSentinelPrefix: KG_HOOK_SENTINEL_PREFIX,
49193
49344
  entry: buildHookMatcherEntry({ flavor: "host" })
49194
49345
  }
49195
49346
  });
49347
+ mergeHomeSettingsJson(filePath, {
49348
+ ensureHook: {
49349
+ stage: "UserPromptSubmit",
49350
+ sentinel: KG_HOOK_PROMPT_SENTINEL,
49351
+ staleSentinelPrefix: KG_HOOK_PROMPT_SENTINEL_PREFIX,
49352
+ entry: buildPromptHookMatcherEntry({ flavor: "host" })
49353
+ }
49354
+ });
49196
49355
  if (result.status === "already-present" && backupPath) {
49197
49356
  try {
49198
49357
  fs47.unlinkSync(backupPath);
@@ -49266,13 +49425,13 @@ function settingsPathFor3(scope, projectPath) {
49266
49425
  const root = projectPath ?? process.cwd();
49267
49426
  return path47.join(root, ".claude", "settings.json");
49268
49427
  }
49269
- function dropSentinel(matchers) {
49428
+ function dropSentinel(matchers, sentinelPrefix) {
49270
49429
  let changed = false;
49271
49430
  const out = [];
49272
49431
  for (const matcher of matchers) {
49273
49432
  const innerHooks = matcher.hooks ?? [];
49274
49433
  const keptInner = innerHooks.filter((h) => {
49275
- if (typeof h.command === "string" && h.command.includes(KG_HOOK_SENTINEL)) {
49434
+ if (typeof h.command === "string" && h.command.includes(sentinelPrefix)) {
49276
49435
  changed = true;
49277
49436
  return false;
49278
49437
  }
@@ -49308,13 +49467,16 @@ function register32(server, _ctx, _initError) {
49308
49467
  const raw = fs48.readFileSync(filePath, "utf-8");
49309
49468
  const settings = raw.trim() ? JSON.parse(raw) : {};
49310
49469
  const preToolUse = settings.hooks?.PreToolUse;
49311
- if (!Array.isArray(preToolUse) || preToolUse.length === 0) {
49470
+ const promptSubmit = settings.hooks?.UserPromptSubmit;
49471
+ const hasPre = Array.isArray(preToolUse) && preToolUse.length > 0;
49472
+ const hasPrompt = Array.isArray(promptSubmit) && promptSubmit.length > 0;
49473
+ if (!hasPre && !hasPrompt) {
49312
49474
  return {
49313
49475
  content: [
49314
49476
  {
49315
49477
  type: "text",
49316
49478
  text: JSON.stringify(
49317
- { ok: true, status: "no-hooks", filePath, message: "no PreToolUse hooks present" },
49479
+ { ok: true, status: "no-hooks", filePath, message: "no kg hooks present" },
49318
49480
  null,
49319
49481
  2
49320
49482
  )
@@ -49322,8 +49484,9 @@ function register32(server, _ctx, _initError) {
49322
49484
  ]
49323
49485
  };
49324
49486
  }
49325
- const { matchers, changed } = dropSentinel(preToolUse);
49326
- if (!changed) {
49487
+ const preResult = hasPre ? dropSentinel(preToolUse, KG_HOOK_SENTINEL_PREFIX) : { matchers: [], changed: false };
49488
+ const promptResult = hasPrompt ? dropSentinel(promptSubmit, KG_HOOK_PROMPT_SENTINEL_PREFIX) : { matchers: [], changed: false };
49489
+ if (!preResult.changed && !promptResult.changed) {
49327
49490
  return {
49328
49491
  content: [
49329
49492
  {
@@ -49343,15 +49506,13 @@ function register32(server, _ctx, _initError) {
49343
49506
  fs48.copyFileSync(filePath, backupPath);
49344
49507
  } catch {
49345
49508
  }
49346
- const next = {
49347
- ...settings,
49348
- hooks: { ...settings.hooks, PreToolUse: matchers }
49349
- };
49350
- if (matchers.length === 0) {
49351
- const otherStages = Object.keys(next.hooks ?? {}).filter((k) => k !== "PreToolUse");
49352
- if (otherStages.length === 0) delete next.hooks;
49353
- else delete next.hooks.PreToolUse;
49354
- }
49509
+ const nextHooks = { ...settings.hooks };
49510
+ if (hasPre) nextHooks.PreToolUse = preResult.matchers;
49511
+ if (hasPrompt) nextHooks.UserPromptSubmit = promptResult.matchers;
49512
+ if (hasPre && preResult.matchers.length === 0) delete nextHooks.PreToolUse;
49513
+ if (hasPrompt && promptResult.matchers.length === 0) delete nextHooks.UserPromptSubmit;
49514
+ const next = { ...settings, hooks: nextHooks };
49515
+ if (Object.keys(nextHooks).length === 0) delete next.hooks;
49355
49516
  fs48.writeFileSync(filePath, JSON.stringify(next, null, 2) + "\n");
49356
49517
  return {
49357
49518
  content: [
@@ -57355,6 +57516,13 @@ async function main() {
57355
57516
  const transport = new StdioServerTransport();
57356
57517
  await server.connect(transport);
57357
57518
  logger.info("Olam MCP server running on stdio transport");
57519
+ const shutdown = (reason) => {
57520
+ logger.info("Olam MCP client disconnected; shutting down", { reason });
57521
+ process.exit(0);
57522
+ };
57523
+ process.stdin.once("end", () => shutdown("stdin end"));
57524
+ process.stdin.once("close", () => shutdown("stdin close"));
57525
+ process.stdin.resume();
57358
57526
  }
57359
57527
  main().catch((error51) => {
57360
57528
  logger.error("Fatal error starting Olam MCP server", {
@@ -1,4 +1,4 @@
1
1
  {
2
- "bundledAt": "2026-06-03T04:32:31.735Z",
2
+ "bundledAt": "2026-06-03T07:06:37.421Z",
3
3
  "kgFirstSha": "29a9ccce1b115d049e375c4a90eb5cf7c123e610e2d0590270a4db2cdbc64a28"
4
4
  }
@@ -118,7 +118,7 @@ spec:
118
118
  # k3d), started by `olam upgrade` Step 0.7 — not inside this Pod.
119
119
  containers:
120
120
  - name: olam-host-cp
121
- image: ghcr.io/pleri/olam-host-cp@sha256:ddeb5b82c7a1d72d78070b299ea2266683e71ff832c5b399fe25ba28f2285d6f
121
+ image: ghcr.io/pleri/olam-host-cp@sha256:5d8726cbe667359e64d2abe53150c80c870a2d71960c2f41833a3c8719abee12
122
122
  imagePullPolicy: IfNotPresent
123
123
  securityContext:
124
124
  runAsNonRoot: true
@@ -70,7 +70,7 @@ spec:
70
70
  mountPath: /data
71
71
  containers:
72
72
  - name: olam-auth-service
73
- image: ghcr.io/pleri/olam-auth@sha256:238ef8b58198684f84ea7c2bf91b23a3ac216660a40f8ef6b4db11c83e555b3a
73
+ image: ghcr.io/pleri/olam-auth@sha256:90c00c3d75cc697539a0d6663fdf30d67fac787171ff1c53a3c1fa1755fcecb3
74
74
  imagePullPolicy: IfNotPresent
75
75
  securityContext:
76
76
  runAsNonRoot: true
@@ -61,7 +61,7 @@ spec:
61
61
  mountPath: /data
62
62
  containers:
63
63
  - name: olam-kg-service
64
- image: ghcr.io/pleri/olam-kg-service@sha256:8fa1ef59a27f9295b7f7375ccda8e1efa110c9a309603301c073076e919e35c3
64
+ image: ghcr.io/pleri/olam-kg-service@sha256:829de080754fe0728b0c0368b6083a6679baff33745dd81c68fadc4ccea7bf7d
65
65
  imagePullPolicy: IfNotPresent
66
66
  securityContext:
67
67
  runAsNonRoot: true
@@ -68,7 +68,7 @@ spec:
68
68
  mountPath: /data
69
69
  containers:
70
70
  - name: olam-mcp-auth-service
71
- image: ghcr.io/pleri/olam-mcp-auth@sha256:56964905f2da39c19f462b5e5f5f84e74a930c9a70a4518463606ce91276272b
71
+ image: ghcr.io/pleri/olam-mcp-auth@sha256:c7041e093e23acdad36eb2a611e66e925061b79e41a5b8a3be28bba57d6624c5
72
72
  imagePullPolicy: IfNotPresent
73
73
  securityContext:
74
74
  runAsNonRoot: true
@@ -70,7 +70,7 @@ spec:
70
70
  # bootstrap-placeholder comment + run `npm run refresh:manifest-digests`
71
71
  # once ghcr.io/pleri/olam-memory-service has a real published digest.
72
72
  # bootstrap-placeholder: pre-publish; refresh after first release
73
- image: ghcr.io/pleri/olam-memory-service@sha256:581f763fcdc2e7e056c805883efa5e510ce460c6c2096445a909f35fcb38f04b
73
+ image: ghcr.io/pleri/olam-memory-service@sha256:2b40d835b807fa49316a9db0215ee4ac6b36e2403c8170cfb4a70067115ad292
74
74
  imagePullPolicy: IfNotPresent
75
75
  securityContext:
76
76
  runAsNonRoot: true
@@ -237,6 +237,13 @@ const OLAM_SANDBOX_RUNNER_TOKEN = process.env.OLAM_SANDBOX_RUNNER_TOKEN ?? '';
237
237
  // Basic auth to plan-DO; defaults to Basic auth with operator/<SHOWCASE_PW>.
238
238
  const OLAM_OPSIDE_LONGPOLL = process.env.OLAM_OPSIDE_LONGPOLL === '1';
239
239
  const OLAM_CLOUD_URL = process.env.OLAM_CLOUD_URL ?? '';
240
+ // Chunk sink: the Neon ingest worker is the PROVEN path (runner → ingest →
241
+ // Neon → Electric → SPA). The legacy default (OLAM_CLOUD_URL = plan-DO) buffers
242
+ // chunks in DO SQLite and only reaches Neon if plan-DO's HOST_CP_URL is set —
243
+ // which it is not in cloud mode, so SPA chunk-render silently breaks. Prefer
244
+ // CHUNKS_INGEST_URL when set; fall back to OLAM_CLOUD_URL for back-compat.
245
+ const CHUNKS_INGEST_URL = process.env.CHUNKS_INGEST_URL ?? '';
246
+ const CHUNKS_INGEST_BEARER = process.env.CHUNKS_INGEST_BEARER ?? '';
240
247
  const OLAM_SHOWCASE_PASSWORD = process.env.OLAM_SHOWCASE_PASSWORD ?? '';
241
248
  // Build the Basic auth header from OLAM_SHOWCASE_PASSWORD when not
242
249
  // explicitly overridden. Showcase Basic auth is the v1 mechanism (Decision #7).
@@ -2218,7 +2225,8 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2218
2225
  (typeof parsedBody.workspace === 'string' && parsedBody.workspace.length > 0 ? parsedBody.workspace : '') ||
2219
2226
  hostKgWorkspace;
2220
2227
  const burstHostCpUrl = parsedBody.hostCpUrl || readRemoteHostCpUrl();
2221
- const burstChunksSinkUrl = parsedBody.chunksSinkUrl || OLAM_CLOUD_URL;
2228
+ const burstChunksSinkUrl = parsedBody.chunksSinkUrl || CHUNKS_INGEST_URL || OLAM_CLOUD_URL;
2229
+ const burstChunksSinkBearer = parsedBody.chunksSinkBearer || CHUNKS_INGEST_BEARER || '';
2222
2230
  // Always generate a fresh idempotency key — prevents double-spawn on
2223
2231
  // retried requests (mirrors the cloud-dispatch handler's A3.2 contract).
2224
2232
  const idempotencyKey = crypto.randomUUID();
@@ -2241,6 +2249,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2241
2249
  ...(burstKgWorkspace ? { kgWorkspace: burstKgWorkspace } : {}),
2242
2250
  ...(burstHostCpUrl ? { hostCpUrl: burstHostCpUrl } : {}),
2243
2251
  ...(burstChunksSinkUrl ? { chunksSinkUrl: burstChunksSinkUrl } : {}),
2252
+ ...(burstChunksSinkBearer ? { chunksSinkBearer: burstChunksSinkBearer } : {}),
2244
2253
  // attachments → runner materializes operator-pasted file/image bytes
2245
2254
  // from R2 into /workspace/<sid>/.attachments (plan-chat-spa-attachments
2246
2255
  // Phase A, #1614). The runner reads body.attachments; sandbox-dispatch-client
@@ -3267,10 +3276,14 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
3267
3276
  // operator is watching (live SPA reasoning). body value wins if present.
3268
3277
  const remoteHostCpUrl = readRemoteHostCpUrl();
3269
3278
  if (remoteHostCpUrl && !parsed.hostCpUrl) enrichedObj = { ...(enrichedObj ?? parsed), hostCpUrl: remoteHostCpUrl };
3270
- // Host-cp-independent chunk sink: forward the public plan-DO URL so the
3271
- // runner posts chunks straight to plan-DO /v1/chunks Neon Electric
3272
- // SPA (no host-cp in the chunk path). OLAM_CLOUD_URL is plan-DO's URL.
3273
- if (OLAM_CLOUD_URL && !parsed.chunksSinkUrl) enrichedObj = { ...(enrichedObj ?? parsed), chunksSinkUrl: OLAM_CLOUD_URL };
3279
+ // Host-cp-independent chunk sink: the runner posts chunks to
3280
+ // <chunksSinkUrl>/v1/chunks. Prefer the Neon ingest worker (proven path:
3281
+ // runner ingest Neon Electric → SPA); the legacy OLAM_CLOUD_URL
3282
+ // (plan-DO) only buffers in DO SQLite and never reaches Neon in cloud
3283
+ // mode. body value wins. Pair the sink with its bearer when defaulting.
3284
+ const defaultChunkSink = CHUNKS_INGEST_URL || OLAM_CLOUD_URL;
3285
+ if (defaultChunkSink && !parsed.chunksSinkUrl) enrichedObj = { ...(enrichedObj ?? parsed), chunksSinkUrl: defaultChunkSink };
3286
+ if (CHUNKS_INGEST_BEARER && !parsed.chunksSinkBearer) enrichedObj = { ...(enrichedObj ?? parsed), chunksSinkBearer: CHUNKS_INGEST_BEARER };
3274
3287
  // Always inject idempotencyKey into the body so plan-DO forwards it to the
3275
3288
  // runner even when the Idempotency-Key header is not propagated.
3276
3289
  enrichedObj = { ...(enrichedObj ?? parsed), idempotencyKey };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * agent-memory-gate.mjs — dependency-free ESM gate helper for hooks.
3
+ *
4
+ * Re-implements the SAME tri-state opt-in contract as
5
+ * packages/core/src/memory/gate.ts inline. Hooks are standalone .mjs/.js files
6
+ * distributed to operators' ~/.claude/scripts/hooks/ and cannot import
7
+ * @olam/core at runtime — this module is imported by the agentmemory hooks.
8
+ *
9
+ * # Modes — KEEP IN SYNC WITH gate.ts
10
+ *
11
+ * off — no recall, no capture (default).
12
+ * readonly — recall allowed; capture (write) NOT. Human-driven dev.
13
+ * editor — recall AND capture allowed. Fully agentic dev.
14
+ *
15
+ * # Precedence (fail-closed)
16
+ *
17
+ * 1. env OLAM_AGENT_MEMORY — AUTHORITATIVE when set + non-empty
18
+ * (trimmed, case-insensitive):
19
+ * 'readonly' → readonly · 'editor' → editor
20
+ * '1'|'true'|'on' → readonly (legacy "on" → least-privilege)
21
+ * any other value → off
22
+ * 2. flag file at $OLAM_HOME/agent-memory-enabled (or ~/.olam/…) when env unset:
23
+ * 'readonly' → readonly · 'editor' → editor
24
+ * 'true'|'1' → readonly (legacy enable wrote TRUE)
25
+ * anything else / absent / read error → off
26
+ * 3. default → off
27
+ *
28
+ * FAIL-CLOSED: any error → off. Legacy truthy values resolve to READONLY, never
29
+ * EDITOR — write access must be opted into explicitly.
30
+ *
31
+ * Two implementations, one contract. If you change this logic, update
32
+ * packages/core/src/memory/gate.ts to match (and vice-versa).
33
+ */
34
+
35
+ import { readFileSync } from 'node:fs';
36
+ import { homedir } from 'node:os';
37
+ import { join } from 'node:path';
38
+
39
+ /** @typedef {'off' | 'readonly' | 'editor'} AgentMemoryMode */
40
+
41
+ const ENV_MODE = new Map([
42
+ ['readonly', 'readonly'],
43
+ ['editor', 'editor'],
44
+ ['1', 'readonly'],
45
+ ['true', 'readonly'],
46
+ ['on', 'readonly'],
47
+ ]);
48
+ const FILE_MODE = new Map([
49
+ ['readonly', 'readonly'],
50
+ ['editor', 'editor'],
51
+ ['true', 'readonly'],
52
+ ['1', 'readonly'],
53
+ ]);
54
+
55
+ /**
56
+ * Resolve the agent-memory-enabled flag file path.
57
+ * Mirrors agentMemoryEnabledPath() from @olam/core/src/paths/olam-paths.ts.
58
+ * Re-reads process.env on every call (direnv-safe).
59
+ */
60
+ function agentMemoryEnabledPath() {
61
+ const fromEnv = process.env['OLAM_HOME'];
62
+ const olamHome = (fromEnv && fromEnv.length > 0) ? fromEnv : join(homedir(), '.olam');
63
+ return join(olamHome, 'agent-memory-enabled');
64
+ }
65
+
66
+ /**
67
+ * Resolve the agent-memory access mode. Synchronous. Never throws — any I/O
68
+ * error or unrecognised value → 'off' (fail-closed).
69
+ *
70
+ * @returns {AgentMemoryMode}
71
+ */
72
+ export function agentMemoryMode() {
73
+ const envVal = process.env['OLAM_AGENT_MEMORY'];
74
+ if (envVal !== undefined && envVal.length > 0) {
75
+ return ENV_MODE.get(envVal.trim().toLowerCase()) ?? 'off';
76
+ }
77
+
78
+ try {
79
+ const raw = readFileSync(agentMemoryEnabledPath(), 'utf8').trim().toLowerCase();
80
+ return FILE_MODE.get(raw) ?? 'off';
81
+ } catch {
82
+ return 'off';
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Returns true iff agent-memory RECALL should run (readonly or editor).
88
+ * Compatibility shim for existing read-path call sites.
89
+ *
90
+ * @returns {boolean}
91
+ */
92
+ export function isAgentMemoryEnabled() {
93
+ return agentMemoryMode() !== 'off';
94
+ }
95
+
96
+ /**
97
+ * Returns true iff agent-memory CAPTURE (write) is permitted — editor only.
98
+ *
99
+ * @returns {boolean}
100
+ */
101
+ export function canWriteAgentMemory() {
102
+ return agentMemoryMode() === 'editor';
103
+ }
@@ -33,6 +33,7 @@
33
33
  */
34
34
 
35
35
  import { readFileSync } from 'node:fs';
36
+ import { canWriteAgentMemory } from './agent-memory-gate.mjs';
36
37
 
37
38
  const DEFAULT_URL = 'https://atlas-agent-memory.atlas-kitchen.workers.dev';
38
39
  const MEMORY_PATH_RE = /\/\.claude\/projects\/.*\/memory\/.*\.md$/;
@@ -93,6 +94,9 @@ async function main() {
93
94
  const filePath = payload?.tool_input?.file_path ?? '';
94
95
  if (toolName !== 'Write' || !MEMORY_PATH_RE.test(filePath)) return;
95
96
 
97
+ // Capture is gated to EDITOR mode. off/readonly never write to the store.
98
+ if (!canWriteAgentMemory()) return;
99
+
96
100
  const bearer = (process.env['OLAM_AGENT_MEMORY_BEARER'] ?? '').trim();
97
101
  if (!bearer) return;
98
102
 
@@ -43,21 +43,24 @@ const path = require('path');
43
43
  // ---------------------------------------------------------------------------
44
44
  // Opt-in gate — inline re-implementation of agent-memory-gate.mjs contract
45
45
  // (CJS cannot import ESM at the top level; inline ensures same fail-closed
46
- // semantics without a dynamic import or a build step).
46
+ // semantics without a dynamic import or a build step). This is a RECALL path,
47
+ // so it gates on mode !== off (readonly OR editor); legacy truthy → readonly.
47
48
  // ---------------------------------------------------------------------------
48
- const _GATE_TRUTHY_ENV = new Set(['1', 'true', 'on']);
49
- const _GATE_TRUTHY_FILE = new Set(['true', '1']);
49
+ // Values that enable RECALL (env, then file). Capture (editor-only) is gated
50
+ // separately in agentmemory-save.mjs via canWriteAgentMemory().
51
+ const _GATE_ENABLED_ENV = new Set(['readonly', 'editor', '1', 'true', 'on']);
52
+ const _GATE_ENABLED_FILE = new Set(['readonly', 'editor', 'true', '1']);
50
53
  function _isAgentMemoryEnabled() {
51
54
  const envVal = process.env['OLAM_AGENT_MEMORY'];
52
55
  if (envVal !== undefined && envVal.length > 0) {
53
- return _GATE_TRUTHY_ENV.has(envVal.trim().toLowerCase());
56
+ return _GATE_ENABLED_ENV.has(envVal.trim().toLowerCase());
54
57
  }
55
58
  try {
56
59
  const fromEnv = process.env['OLAM_HOME'];
57
60
  const olamHome = (fromEnv && fromEnv.length > 0) ? fromEnv : path.join(os.homedir(), '.olam');
58
61
  const flagPath = path.join(olamHome, 'agent-memory-enabled');
59
62
  const raw = fs.readFileSync(flagPath, 'utf8').trim().toLowerCase();
60
- return _GATE_TRUTHY_FILE.has(raw);
63
+ return _GATE_ENABLED_FILE.has(raw);
61
64
  } catch {
62
65
  return false;
63
66
  }