@pleri/olam-cli 0.1.205 → 0.1.207
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/image-digests.json +8 -8
- package/dist/index.js +1886 -1386
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +1100 -795
- 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 +30 -1
- package/memory-hooks/agentmemory-recall-trigger.mjs +5 -0
- package/memory-hooks/agentmemory-save.mjs +138 -0
- package/memory-hooks/agentmemory-session-recall.js +4 -0
- package/package.json +1 -1
|
@@ -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:0d1869ed3c823e6ea1b5d885d2f73d7418134c1b82c9dd70719044fd04d74027
|
|
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:9f8195a20727ec844d7393696c298b42e3e83e841fe684787ee9c6d1e301b5e8
|
|
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:0b1f78a566675509f371e4e0636a76859f800c1bec787f06742e3a4f116d7c90
|
|
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:63ecc6bdd5c3def600a78046f0e7ab36938e88bc6fc7d31a446d7cec84a27a3a
|
|
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:673156cc638fc9af096e73fc7cd7c666373f92e63e566a57f51271b691aa9d74
|
|
74
74
|
imagePullPolicy: IfNotPresent
|
|
75
75
|
securityContext:
|
|
76
76
|
runAsNonRoot: true
|
package/host-cp/src/server.mjs
CHANGED
|
@@ -2241,6 +2241,35 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
|
|
|
2241
2241
|
...(burstKgWorkspace ? { kgWorkspace: burstKgWorkspace } : {}),
|
|
2242
2242
|
...(burstHostCpUrl ? { hostCpUrl: burstHostCpUrl } : {}),
|
|
2243
2243
|
...(burstChunksSinkUrl ? { chunksSinkUrl: burstChunksSinkUrl } : {}),
|
|
2244
|
+
// attachments → runner materializes operator-pasted file/image bytes
|
|
2245
|
+
// from R2 into /workspace/<sid>/.attachments (plan-chat-spa-attachments
|
|
2246
|
+
// Phase A, #1614). The runner reads body.attachments; sandbox-dispatch-client
|
|
2247
|
+
// forwards it on the plan-DO path, so cloud-burst must too — else a
|
|
2248
|
+
// sandbox dispatch carrying attachments silently runs without the files
|
|
2249
|
+
// (drift parity with sandbox-dispatch-client — same class as #1605/#1622).
|
|
2250
|
+
...(Array.isArray(parsedBody.attachments) && parsedBody.attachments.length > 0 ? { attachments: parsedBody.attachments } : {}),
|
|
2251
|
+
// Agent-capability fields the runner /spawn handler reads and
|
|
2252
|
+
// sandbox-dispatch-client (cell #4) already forwards. The cloud-burst
|
|
2253
|
+
// path (cell #2) builds an explicit spawnBody, so it must forward them
|
|
2254
|
+
// too or a local-mode→sandbox dispatch carrying any of these silently
|
|
2255
|
+
// loses the capability (same drift class as submodules/attachments).
|
|
2256
|
+
// Body-supplied (the cell #2 SPA send is the only source on this path);
|
|
2257
|
+
// absent → spread to nothing, so today's bare {prompt,session_id,world_id}
|
|
2258
|
+
// burst is byte-for-byte unchanged.
|
|
2259
|
+
// skillSources → runner lays the operator's /100x chain into ~/.claude.
|
|
2260
|
+
// mcpServers → runner writes ~/.claude.json so the agent has MCP tools.
|
|
2261
|
+
// interactive → runner launches a tmux REPL instead of headless -p.
|
|
2262
|
+
// operatorSub → in-container poster stamps chunks with the owner sub
|
|
2263
|
+
// (runner reads body.operatorSub ?? body.operator_sub;
|
|
2264
|
+
// forwarding the camelCase alias satisfies both reads —
|
|
2265
|
+
// operator_sub is a documented EXCLUSIONS alias).
|
|
2266
|
+
// chunksSinkBearer → per-dispatch chunk-sink auth (NEVER logged); runner
|
|
2267
|
+
// prefers it over its deploy-time env.CHUNKS_INGEST_BEARER.
|
|
2268
|
+
...(Array.isArray(parsedBody.skillSources) && parsedBody.skillSources.length > 0 ? { skillSources: parsedBody.skillSources } : {}),
|
|
2269
|
+
...(parsedBody.mcpServers && typeof parsedBody.mcpServers === 'object' && Object.keys(parsedBody.mcpServers).length > 0 ? { mcpServers: parsedBody.mcpServers } : {}),
|
|
2270
|
+
...(parsedBody.interactive ? { interactive: true } : {}),
|
|
2271
|
+
...(parsedBody.operatorSub ? { operatorSub: parsedBody.operatorSub } : {}),
|
|
2272
|
+
...(parsedBody.chunksSinkBearer ? { chunksSinkBearer: parsedBody.chunksSinkBearer } : {}),
|
|
2244
2273
|
idempotencyKey,
|
|
2245
2274
|
...safeOptions,
|
|
2246
2275
|
};
|
|
@@ -4064,7 +4093,7 @@ const _spaCacheByKey = new Map();
|
|
|
4064
4093
|
// audit FAIL. Keep it as the canonical HN/WP-array parity source until
|
|
4065
4094
|
// that audit is repointed at worldFetch.ts directly (follow-up).
|
|
4066
4095
|
// eslint-disable-next-line no-unused-vars -- retained as HN/WP parity-audit source
|
|
4067
|
-
const BOOTSTRAP_SCRIPT = `<script>(function(){function ck(){var m=document.cookie.match(/olam_host_cp_token=([^;]+)/);return m?m[1]:'';}function sw(t){document.cookie='olam_host_cp_token='+t+'; path=/; samesite=strict';}try{var x=new XMLHttpRequest();x.open('GET','/api/bootstrap',false);x.send();if(x.status===200){var d=JSON.parse(x.responseText);sw(d.token);}}catch(e){console.error('[host-cp bootstrap]',e);}var reloading=false;function recover(){if(reloading)return;try{var x=new XMLHttpRequest();x.open('GET','/api/bootstrap',false);x.send();if(x.status===200){var d=JSON.parse(x.responseText);if(d.token&&ck()!==d.token){reloading=true;sw(d.token);console.warn('[host-cp auth recover] token rotated; reloading');location.reload();}}}catch(e){console.error('[host-cp auth recover]',e);}}var HN=['/api/bootstrap','/api/worlds','/api/projects','/api/workspaces','/api/workspaces/match','/api/repos','/api/runbooks','/api/auth','/api/host-stream','/api/plan-chat','/api/plan/agent-runtime','/health'];var WP=['/api/','/session/','/hooks/','/dispatch','/lanes','/codex/','/review/'];function sr(p){if(typeof p!=='string')return false;if(p.startsWith('/api/world/'))return false;for(var i=0;i<HN.length;i++){var n=HN[i];if(p===n||p.startsWith(n+'?')||p.startsWith(n+'/'))return false;}for(var j=0;j<WP.length;j++){var w=WP[j];if(p===w||p===w.replace(/\\/$/,'')||p.startsWith(w)||p.startsWith(w.replace(/\\/$/,'')+'?')||p.startsWith(w.replace(/\\/$/,'')+'/'))return true;}return false;}function wid(){var p=location.pathname;var m=p.match(/^\\/(world|inbox|session)\\/([^/?#]+)/);if(m)return m[2];if(/^\\/(?:worlds?|workspaces?|world|sandbox|session|inbox|plan|design|repos|runbooks|assets|api|health|favicon)($|\\/|\\?)/.test(p))return null;var r=p.match(/^\\/([a-z][a-z0-9-]+)(?:\\/|$|\\?)/);return r?r[1]:null;}function rw(p){var w=wid();return w?'/api/world/'+w+p:p;}var of=window.fetch.bind(window);window.fetch=function(input,init){var pr;if(typeof input==='string'&&sr(input))pr=of(rw(input),init);else if(input&&typeof input.url==='string'&&sr(input.url))pr=of(new Request(rw(input.url),input),init);else pr=of(input,init);return pr.then(function(res){if(res&&res.status===401)recover();return res;});};var OE=window.EventSource;if(OE){window.EventSource=function(u,i){var s=u;if(typeof s==='string'&&sr(s))s=rw(s);var es=new OE(s,i);es.addEventListener('error',function(){if(es.readyState===OE.CLOSED)recover();});return es;};window.EventSource.prototype=OE.prototype;window.EventSource.CONNECTING=OE.CONNECTING;window.EventSource.OPEN=OE.OPEN;window.EventSource.CLOSED=OE.CLOSED;}})();</script>`;
|
|
4096
|
+
const BOOTSTRAP_SCRIPT = `<script>(function(){function ck(){var m=document.cookie.match(/olam_host_cp_token=([^;]+)/);return m?m[1]:'';}function sw(t){document.cookie='olam_host_cp_token='+t+'; path=/; samesite=strict';}try{var x=new XMLHttpRequest();x.open('GET','/api/bootstrap',false);x.send();if(x.status===200){var d=JSON.parse(x.responseText);sw(d.token);}}catch(e){console.error('[host-cp bootstrap]',e);}var reloading=false;function recover(){if(reloading)return;try{var x=new XMLHttpRequest();x.open('GET','/api/bootstrap',false);x.send();if(x.status===200){var d=JSON.parse(x.responseText);if(d.token&&ck()!==d.token){reloading=true;sw(d.token);console.warn('[host-cp auth recover] token rotated; reloading');location.reload();}}}catch(e){console.error('[host-cp auth recover]',e);}}var HN=['/api/bootstrap','/api/whoami','/api/worlds','/api/projects','/api/chats','/api/workspaces','/api/workspaces/match','/api/repos','/api/runbooks','/api/auth','/api/host-stream','/api/plan-chat','/api/plan/agent-runtime','/health'];var WP=['/api/','/session/','/hooks/','/dispatch','/lanes','/codex/','/review/'];function sr(p){if(typeof p!=='string')return false;if(p.startsWith('/api/world/'))return false;for(var i=0;i<HN.length;i++){var n=HN[i];if(p===n||p.startsWith(n+'?')||p.startsWith(n+'/'))return false;}for(var j=0;j<WP.length;j++){var w=WP[j];if(p===w||p===w.replace(/\\/$/,'')||p.startsWith(w)||p.startsWith(w.replace(/\\/$/,'')+'?')||p.startsWith(w.replace(/\\/$/,'')+'/'))return true;}return false;}function wid(){var p=location.pathname;var m=p.match(/^\\/(world|inbox|session)\\/([^/?#]+)/);if(m)return m[2];if(/^\\/(?:worlds?|workspaces?|world|sandbox|session|inbox|plan|design|repos|runbooks|assets|api|health|favicon)($|\\/|\\?)/.test(p))return null;var r=p.match(/^\\/([a-z][a-z0-9-]+)(?:\\/|$|\\?)/);return r?r[1]:null;}function rw(p){var w=wid();return w?'/api/world/'+w+p:p;}var of=window.fetch.bind(window);window.fetch=function(input,init){var pr;if(typeof input==='string'&&sr(input))pr=of(rw(input),init);else if(input&&typeof input.url==='string'&&sr(input.url))pr=of(new Request(rw(input.url),input),init);else pr=of(input,init);return pr.then(function(res){if(res&&res.status===401)recover();return res;});};var OE=window.EventSource;if(OE){window.EventSource=function(u,i){var s=u;if(typeof s==='string'&&sr(s))s=rw(s);var es=new OE(s,i);es.addEventListener('error',function(){if(es.readyState===OE.CLOSED)recover();});return es;};window.EventSource.prototype=OE.prototype;window.EventSource.CONNECTING=OE.CONNECTING;window.EventSource.OPEN=OE.OPEN;window.EventSource.CLOSED=OE.CLOSED;}})();</script>`;
|
|
4068
4097
|
|
|
4069
4098
|
/**
|
|
4070
4099
|
* Build the plan-chat bearer injection script. Reads
|
|
@@ -204,6 +204,11 @@ async function main() {
|
|
|
204
204
|
const additionalContext = formatAdditionalContext(response, prompt);
|
|
205
205
|
if (!additionalContext) return;
|
|
206
206
|
|
|
207
|
+
const count = response?.results?.length ?? 0;
|
|
208
|
+
process.stderr.write(
|
|
209
|
+
`\x1b[34m[🧠⇣ Memory recalled]\x1b[0m (${count} memories · "${prompt.slice(0, 60)}${prompt.length > 60 ? '…' : ''}")\n`
|
|
210
|
+
);
|
|
211
|
+
|
|
207
212
|
process.stdout.write(JSON.stringify({
|
|
208
213
|
hookSpecificOutput: {
|
|
209
214
|
hookEventName: 'PreToolUse',
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agentmemory-save.mjs — PostToolUse hook: persist agent-written memory notes.
|
|
4
|
+
*
|
|
5
|
+
* AM_SENTINEL=olam-agent-memory-hook-v2
|
|
6
|
+
*
|
|
7
|
+
* This is the FILE-BASED port of the legacy inline python SAVE blob that used
|
|
8
|
+
* to live directly in ~/.claude/settings.json under
|
|
9
|
+
* `AM_SENTINEL=olam-agent-memory-hook-v1; python3 -c '…'`. The installer
|
|
10
|
+
* (`olam hooks install-all`, invoked by `olam skills sync`) copies this file to
|
|
11
|
+
* ~/.claude/scripts/hooks/ and registers a settings.json entry pointing at it,
|
|
12
|
+
* REPLACING the inline blob. Keeping the logic in a real source file means it
|
|
13
|
+
* is version-controlled, testable, bundled into @pleri/olam-cli, and owned by
|
|
14
|
+
* sync — not stranded as a string in the operator's settings.
|
|
15
|
+
*
|
|
16
|
+
* Behaviour (byte-faithful port of the v1 blob):
|
|
17
|
+
* - Reads the PostToolUse JSON payload from stdin.
|
|
18
|
+
* - Acts ONLY when tool_name === "Write" AND the written file_path matches
|
|
19
|
+
* /\/\.claude\/projects\/.*\/memory\/.*\.md$/ ; otherwise bails (exit 0).
|
|
20
|
+
* - Requires OLAM_AGENT_MEMORY_BEARER (trimmed, non-empty); else bails.
|
|
21
|
+
* - Parses YAML-ish frontmatter delimited by leading "---":
|
|
22
|
+
* description: → title (default "memory") ← keyed on description, per v1
|
|
23
|
+
* type: → type (default "fact")
|
|
24
|
+
* remainder → body
|
|
25
|
+
* - POSTs {title, content: body, type} to
|
|
26
|
+
* <OLAM_AGENT_MEMORY_URL|default>/agentmemory/remember
|
|
27
|
+
* with Authorization: Bearer <bearer>, ~3s timeout.
|
|
28
|
+
* - On success prints to STDERR the green status line:
|
|
29
|
+
* [🧠⇡ Memory saved] "<title first 60 chars>"
|
|
30
|
+
* - Fail-open: ANY error → silent, exit 0. Never throws to the caller.
|
|
31
|
+
*
|
|
32
|
+
* Node built-ins only (global fetch + AbortController, available on Node ≥18).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { readFileSync } from 'node:fs';
|
|
36
|
+
|
|
37
|
+
const DEFAULT_URL = 'https://atlas-agent-memory.atlas-kitchen.workers.dev';
|
|
38
|
+
const MEMORY_PATH_RE = /\/\.claude\/projects\/.*\/memory\/.*\.md$/;
|
|
39
|
+
const TIMEOUT_MS = 3000;
|
|
40
|
+
|
|
41
|
+
/** Read all of stdin as a UTF-8 string. */
|
|
42
|
+
function readStdin() {
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(0, 'utf8');
|
|
45
|
+
} catch {
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse leading "---"-delimited frontmatter. Returns {title, type, body}.
|
|
52
|
+
* Mirrors the v1 blob: title comes from `description:`, type from `type:`.
|
|
53
|
+
*/
|
|
54
|
+
function parseNote(raw) {
|
|
55
|
+
let title = 'memory';
|
|
56
|
+
let type = 'fact';
|
|
57
|
+
let body = raw;
|
|
58
|
+
if (raw.startsWith('---')) {
|
|
59
|
+
// Split into [before-first-delim, frontmatter, …rest]. JS's limited split
|
|
60
|
+
// truncates the remainder, so slice manually to keep the full body.
|
|
61
|
+
const firstDelimEnd = 3; // length of leading "---"
|
|
62
|
+
const secondDelim = raw.indexOf('---', firstDelimEnd);
|
|
63
|
+
if (secondDelim !== -1) {
|
|
64
|
+
const fm = raw.slice(firstDelimEnd, secondDelim);
|
|
65
|
+
body = raw.slice(secondDelim + 3).trim();
|
|
66
|
+
for (const ln of fm.split('\n')) {
|
|
67
|
+
const s = ln.trim();
|
|
68
|
+
if (s.startsWith('description:')) {
|
|
69
|
+
title = stripQuotes(s.slice(s.indexOf(':') + 1).trim());
|
|
70
|
+
} else if (s.startsWith('type:')) {
|
|
71
|
+
type = stripQuotes(s.slice(s.indexOf(':') + 1).trim());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { title, type, body };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stripQuotes(v) {
|
|
80
|
+
return v.replace(/^["']/, '').replace(/["']$/, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function main() {
|
|
84
|
+
const payloadRaw = readStdin();
|
|
85
|
+
let payload;
|
|
86
|
+
try {
|
|
87
|
+
payload = JSON.parse(payloadRaw);
|
|
88
|
+
} catch {
|
|
89
|
+
return; // not parseable → fail-open
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const toolName = payload?.tool_name ?? '';
|
|
93
|
+
const filePath = payload?.tool_input?.file_path ?? '';
|
|
94
|
+
if (toolName !== 'Write' || !MEMORY_PATH_RE.test(filePath)) return;
|
|
95
|
+
|
|
96
|
+
const bearer = (process.env['OLAM_AGENT_MEMORY_BEARER'] ?? '').trim();
|
|
97
|
+
if (!bearer) return;
|
|
98
|
+
|
|
99
|
+
let raw;
|
|
100
|
+
try {
|
|
101
|
+
raw = readFileSync(filePath, 'utf8');
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { title, type, body } = parseNote(raw);
|
|
107
|
+
const base = (process.env['OLAM_AGENT_MEMORY_URL'] ?? DEFAULT_URL).replace(/\/$/, '');
|
|
108
|
+
const url = `${base}/agentmemory/remember`;
|
|
109
|
+
|
|
110
|
+
const controller = new AbortController();
|
|
111
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
112
|
+
try {
|
|
113
|
+
const resp = await fetch(url, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
Authorization: `Bearer ${bearer}`,
|
|
118
|
+
'User-Agent': 'olam-agent-memory-hook/2',
|
|
119
|
+
},
|
|
120
|
+
body: JSON.stringify({ title, content: body, type }),
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
});
|
|
123
|
+
// Treat any non-throwing response as delivered (the v1 blob did the same —
|
|
124
|
+
// it printed on a non-exception urlopen regardless of status code).
|
|
125
|
+
if (resp) {
|
|
126
|
+
const label = title.slice(0, 60);
|
|
127
|
+
process.stderr.write(`\x1b[32m[\u{1F9E0}\u{21E1} Memory saved]\x1b[0m "${label}"\n`);
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Fail-open: network error / timeout / abort → silent.
|
|
131
|
+
} finally {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main().catch(() => {
|
|
137
|
+
/* fail-open: never propagate to the caller */
|
|
138
|
+
});
|
|
@@ -321,6 +321,10 @@ async function main() {
|
|
|
321
321
|
const additionalContext =
|
|
322
322
|
`## Recalled from agent memory (query: "${query}")\n\n${lines}\n\n${nudge}`;
|
|
323
323
|
|
|
324
|
+
process.stderr.write(
|
|
325
|
+
`\x1b[34m[🧠⇣ Memory recalled]\x1b[0m (${result.results.length} memories · "${query}")\n`
|
|
326
|
+
);
|
|
327
|
+
|
|
324
328
|
process.stdout.write(JSON.stringify({
|
|
325
329
|
hookSpecificOutput: {
|
|
326
330
|
hookEventName: 'SessionStart',
|