@pleri/olam-cli 0.1.209 → 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.
- package/README.md +5 -3
- package/dist/image-digests.json +8 -8
- package/dist/index.js +794 -437
- package/dist/mcp-server.js +193 -32
- package/hermes-bundle/version.json +1 -1
- package/host-cp/k8s/manifests/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +1 -1
- package/host-cp/src/server.mjs +18 -5
- package/memory-hooks/agent-memory-gate.mjs +103 -0
- package/memory-hooks/agentmemory-save.mjs +4 -0
- package/memory-hooks/agentmemory-session-recall.js +8 -5
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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-
|
|
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
|
-
|
|
49158
|
+
elif route=="grep":
|
|
49103
49159
|
sys.stderr.write("\\U0001f50d Grep used\\n")
|
|
49104
|
-
if route
|
|
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
|
-
|
|
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
|
|
49124
|
-
const
|
|
49125
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 }
|
|
49326
|
-
|
|
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
|
|
49347
|
-
|
|
49348
|
-
|
|
49349
|
-
|
|
49350
|
-
if (matchers.length === 0)
|
|
49351
|
-
|
|
49352
|
-
|
|
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: [
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
73
|
+
image: ghcr.io/pleri/olam-memory-service@sha256:2b40d835b807fa49316a9db0215ee4ac6b36e2403c8170cfb4a70067115ad292
|
|
74
74
|
imagePullPolicy: IfNotPresent
|
|
75
75
|
securityContext:
|
|
76
76
|
runAsNonRoot: true
|
package/host-cp/src/server.mjs
CHANGED
|
@@ -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:
|
|
3271
|
-
//
|
|
3272
|
-
//
|
|
3273
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
63
|
+
return _GATE_ENABLED_FILE.has(raw);
|
|
61
64
|
} catch {
|
|
62
65
|
return false;
|
|
63
66
|
}
|