@inetafrica/open-claudia 2.13.1 → 2.14.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.14.0
4
+ - **`/channel add kazee` now finishes cleanly — the validation step was probing the wrong path.** The live adapter mounts every REST route under `{root}/api`, but the setup probe hit `{root}/me`. So the correct host (`https://chat.example.com`) always 404'd at validation, while the only URL that *passed* (`…/api`) then made the adapter build `…/api/api` and break. There was no single URL a user could type to succeed. `kazee-probe` now normalises either form to the root and probes the real `{root}/api/me` (new exported `normalizeKazeeRoot`); the wizard and `setup.js` both store the normalised root. Step 1 now asks for the host and states "I add the /api path myself."
5
+ - **The bot's own user id is no longer thrown away.** Validation already fetches it from `/me`, but the wizard never persisted `KAZEE_BOT_USER_ID` or passed it to the adapter — so a wizard-added channel booted without self-echo filtering or slash-command registration (and `loadChannels` warned on every restart). The wizard and `setup.js` now save it and hand it to the adapter.
6
+ - **Owner is captured by listening, not by pasting a Mongo id (Step 3 redesign).** After URL+token validate, the wizard brings Kazee **live immediately** (owner still blank) and asks the operator to send any message from their Kazee account. A new cross-channel hook — `router.tryCaptureOwner` → `channel-wizard.tryCaptureOwner` — intercepts that inbound Kazee message (it lands on a different channel than the operator's, so the operator-keyed `isAwaiting()` gate can't see it), then asks back on the operator's channel *"Is this you — the owner?"* with **Yes / No, keep listening / Skip** buttons. Yes grabs the sender's Kazee user id; owner recognition then flows through the existing env path (`identity.isConfiguredOwnerChannel` reads `KAZEE_OWNER_USER_ID`). Pasting an id or "skip" still works as a fallback; Cancel now tears the half-added adapter back down and rolls the `.env` edits back.
7
+ - **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically. Purely a UX + correctness fix to the Kazee onboarding wizard — inert for deployments that never run `/channel add kazee`.
8
+
3
9
  ## v2.13.1
4
10
  - **Guardrail no longer mistakes the owner for an external speaker on `/link`'d or env-unified channels (Phase 3 fix).** v2.13.0's relationship classifier resolved ownership through `people.findByHandle` alone — the handles physically listed on the owner's person record — while the queue/state/run-lock resolve it through `identity.canonicalForChannel`, which also honours explicit `/link`s (`identities.json`) and the configured-owner env path. A channel the owner had unified by `/link` or env-config (e.g. a second Kazee handle) was therefore *owner* to the run-lock (shared queue → "Queued.") but *external* to the guardrail (default-deny → the stranger reply), so the bot could greet its own owner as a limited-access outsider. The classifier now resolves ownership through **canonical identity — the same source of truth the rest of the bot uses**: a channel is the owner's when it resolves to the owner canonical **or** its person record is `isOwner`. New `identity.channelIsOwner()`; `relationship.speakerFor` consults it before default-deny. Every guard consumer (external-mode system-prompt block, `io`/`loopback` egress gates, runner turn-guard, CLI guards) funnels through this one function, so the fix lands everywhere at once.
5
11
  - **R1 preserved — a note still can never confer owner.** `channelIsOwner` is driven only by the canonical map, which is written **exclusively by owner/operator actions** (`/link`, `.env`), never by a note; and the owner stays anchored to a real owner record via `ownerCanonical → people.isOwner`. The default-deny posture for genuinely external or unmapped channels is unchanged.
@@ -5,6 +5,13 @@
5
5
  // enters the wizard their next plain-text message becomes the wizard's
6
6
  // input. A Cancel button (payload "chw:cancel") bails out at any step.
7
7
  //
8
+ // Owner capture is cross-channel: once URL+token validate, we bring the
9
+ // Kazee adapter live and listen for an inbound message ON KAZEE, then ask
10
+ // the operator (back on the channel they ran /channel from) to confirm
11
+ // "is this you?". router.tryCaptureOwner() feeds that inbound message in —
12
+ // it arrives on a different channel than the operator's, so isAwaiting()
13
+ // (keyed to the operator) never sees it via handleText().
14
+ //
8
15
  // validateKazee() is exported separately so setup.js can reuse the same
9
16
  // /me probe without going through the chat-driven flow.
10
17
 
@@ -31,22 +38,45 @@ function endSession(canonicalUserId) {
31
38
 
32
39
  const cancelKeyboard = { inline_keyboard: [[{ text: "Cancel", callback_data: "chw:cancel" }]] };
33
40
 
34
- async function start(canonicalUserId, kind) {
41
+ // Prompts issued while listening for (and confirming) the owner's Kazee
42
+ // message must land on the channel the operator ran /channel from — not the
43
+ // Kazee chat a trigger arrived on. Reach it through the stored origin
44
+ // adapter, bypassing the ambient io.send() scope.
45
+ async function sayToOrigin(session, text, opts) {
46
+ try {
47
+ if (session?.origin?.adapter && session.origin.channelId) {
48
+ return await session.origin.adapter.send(session.origin.channelId, text, opts);
49
+ }
50
+ } catch (e) { console.error("channel-wizard sayToOrigin:", e.message); }
51
+ return send(text, opts); // fallback: ambient scope
52
+ }
53
+
54
+ async function start(canonicalUserId, kind, envelope) {
35
55
  if (kind !== "kazee") {
36
56
  await send(`Channel type "${kind}" not supported yet.`);
37
57
  return;
38
58
  }
39
- sessions.set(canonicalUserId, { kind: "kazee", step: "awaiting-url", data: {} });
59
+ const origin = envelope?.adapter && envelope?.channelId
60
+ ? { adapter: envelope.adapter, channelId: envelope.channelId }
61
+ : null;
62
+ sessions.set(canonicalUserId, { kind: "kazee", step: "awaiting-url", data: {}, origin });
40
63
  await send(
41
- "Add a Kazee channel.\n\nStep 1 of 3: Kazee bot API URL (e.g. https://chat.example.com/api).",
64
+ "Add a Kazee channel.\n\nStep 1 of 3: Kazee host URL (e.g. https://chat.example.com). I add the /api path myself.",
42
65
  { keyboard: cancelKeyboard },
43
66
  );
44
67
  }
45
68
 
46
69
  async function cancel(canonicalUserId, { silent } = {}) {
47
- if (!sessions.has(canonicalUserId)) return;
70
+ const session = sessions.get(canonicalUserId);
71
+ if (!session) return;
48
72
  sessions.delete(canonicalUserId);
49
- if (!silent) await send("Channel setup cancelled.");
73
+ // If we already brought the adapter live for owner-capture, tear it back
74
+ // down and roll the .env edits back so a cancel leaves no half-added channel.
75
+ if (session.data?.live) {
76
+ try { await registry.removeAdapter("kazee"); } catch (e) {}
77
+ revertEnv(session.data.previous);
78
+ }
79
+ if (!silent) await sayToOrigin(session, "Channel setup cancelled.");
50
80
  }
51
81
 
52
82
  async function handleText(envelope) {
@@ -59,7 +89,7 @@ async function handleText(envelope) {
59
89
  await send("That doesn't look like a URL. Try again, or hit Cancel.", { keyboard: cancelKeyboard });
60
90
  return;
61
91
  }
62
- session.data.url = text.replace(/\/$/, "");
92
+ session.data.url = text;
63
93
  session.step = "awaiting-token";
64
94
  await send(
65
95
  "Step 2 of 3: Kazee bot token. I'll delete your message after reading.",
@@ -83,83 +113,177 @@ async function handleText(envelope) {
83
113
  await send(`Validation failed: ${check.error}\n\nRun /channel add kazee to try again.`);
84
114
  return;
85
115
  }
116
+ session.data.url = check.url || session.data.url.replace(/\/+$/, "");
86
117
  session.data.botUserId = check.botUserId || "";
87
- session.step = "awaiting-owner";
118
+
119
+ // Bring the adapter live now so it can receive the owner's message.
120
+ const live = await bringLive(session);
121
+ if (!live.ok) {
122
+ sessions.delete(envelope.canonicalUserId);
123
+ await send(`Could not start the Kazee adapter: ${live.error}\n\nReverted .env. Run /channel add kazee to try again.`);
124
+ return;
125
+ }
126
+ session.step = "awaiting-owner-message";
88
127
  await send(
89
- `Step 3 of 3: Kazee user id that should be treated as the owner (or send "skip" to leave blank).`,
128
+ "Connected to Kazee ✅\n\nStep 3 of 3: open Kazee and send me any message I'll grab your account and confirm it here.\n\n(Or send \"skip\" to leave the owner unset.)",
90
129
  { keyboard: cancelKeyboard },
91
130
  );
92
131
  return;
93
132
  }
94
133
 
95
- if (session.step === "awaiting-owner") {
96
- const ownerUserId = text.toLowerCase() === "skip" ? "" : text;
97
- session.data.ownerUserId = ownerUserId;
98
- session.step = "applying";
99
- await applySession(envelope.canonicalUserId, session);
134
+ // Text fallback for step 3 the operator can still type a user id or
135
+ // "skip" on their own channel instead of sending a Kazee message.
136
+ if (session.step === "awaiting-owner-message" || session.step === "confirming-owner") {
137
+ if (text.toLowerCase() === "skip") {
138
+ await finalizeOwner(envelope.canonicalUserId, session, "");
139
+ return;
140
+ }
141
+ if (/^[a-f0-9]{24}$/i.test(text)) {
142
+ await finalizeOwner(envelope.canonicalUserId, session, text);
143
+ return;
144
+ }
145
+ if (session.step === "awaiting-owner-message") {
146
+ await send(
147
+ "Still listening. Send a message from your Kazee account, paste a user id, or send \"skip\".",
148
+ { keyboard: cancelKeyboard },
149
+ );
150
+ }
100
151
  return;
101
152
  }
102
153
  }
103
154
 
155
+ // Fired by router.tryCaptureOwner() for inbound messages arriving ON the
156
+ // freshly-added Kazee adapter while a session is listening. Returns true
157
+ // when it consumed the message (so the router stops normal processing).
158
+ async function tryCaptureOwner(envelope) {
159
+ const adapterId = envelope.adapter?.id;
160
+ if (!adapterId) return false;
161
+ for (const [, session] of sessions) {
162
+ if (session.step !== "awaiting-owner-message") continue;
163
+ if (session.data.adapterId !== adapterId) continue;
164
+ const userId = String(envelope.userId || envelope.from?.id || "");
165
+ if (!userId) return true; // consume malformed input, keep listening
166
+ const name = envelope.from?.name || envelope.from?.username || "";
167
+ session.data.candidate = { userId, name };
168
+ session.step = "confirming-owner";
169
+ // Light ack on the Kazee side so the sender knows it landed.
170
+ try { await envelope.adapter.send(envelope.channelId, "Got your message 👋 — finishing setup on my end."); } catch (e) {}
171
+ const who = name ? `${name} (${userId})` : userId;
172
+ await sayToOrigin(
173
+ session,
174
+ `Got a Kazee message from ${who}.\n\nIs this you — the owner?`,
175
+ { keyboard: { inline_keyboard: [
176
+ [{ text: "Yes, that's me", callback_data: "chw:owner-yes" }],
177
+ [{ text: "No, keep listening", callback_data: "chw:owner-no" }],
178
+ [{ text: "Skip (no owner)", callback_data: "chw:owner-skip" }],
179
+ ] } },
180
+ );
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+
104
186
  async function handleAction(envelope) {
105
187
  const payload = envelope.action?.payload || envelope.action?.buttonId || "";
106
- if (payload !== "chw:cancel") return false;
107
- await cancel(envelope.canonicalUserId);
188
+ if (!String(payload).startsWith("chw:")) return false;
189
+ const session = sessions.get(envelope.canonicalUserId);
190
+
191
+ if (payload === "chw:cancel") {
192
+ await cancel(envelope.canonicalUserId);
193
+ return true;
194
+ }
195
+ if (!session) return true; // stale button — swallow so it doesn't fall through
196
+
197
+ if (payload === "chw:owner-yes") {
198
+ const cand = session.data.candidate;
199
+ if (!cand?.userId) {
200
+ session.step = "awaiting-owner-message";
201
+ await sayToOrigin(session, "Lost that candidate — send another message from Kazee.", { keyboard: cancelKeyboard });
202
+ return true;
203
+ }
204
+ await finalizeOwner(envelope.canonicalUserId, session, cand.userId);
205
+ return true;
206
+ }
207
+ if (payload === "chw:owner-no") {
208
+ session.data.candidate = null;
209
+ session.step = "awaiting-owner-message";
210
+ await sayToOrigin(session, "OK — still listening. Send a message from the owner's Kazee account.", { keyboard: cancelKeyboard });
211
+ return true;
212
+ }
213
+ if (payload === "chw:owner-skip") {
214
+ await finalizeOwner(envelope.canonicalUserId, session, "");
215
+ return true;
216
+ }
108
217
  return true;
109
218
  }
110
219
 
111
- async function applySession(canonicalUserId, session) {
112
- const { url, token, ownerUserId } = session.data;
220
+ // ── env plumbing ────────────────────────────────────────────────────
113
221
 
114
- // Capture rollback state so we can undo .env changes if the adapter
115
- // refuses to start.
116
- const previous = {
222
+ function snapshotEnv() {
223
+ return {
117
224
  KAZEE_URL: config.KAZEE_URL || "",
118
225
  KAZEE_BOT_TOKEN: config.KAZEE_BOT_TOKEN || "",
226
+ KAZEE_BOT_USER_ID: config.KAZEE_BOT_USER_ID || "",
119
227
  KAZEE_OWNER_USER_ID: config.KAZEE_OWNER_USER_ID || "",
120
228
  CHANNELS: config.CHANNELS || "telegram",
121
229
  };
230
+ }
231
+
232
+ function revertEnv(previous) {
233
+ if (!previous) return;
234
+ for (const [k, v] of Object.entries(previous)) {
235
+ saveEnvKey(k, v);
236
+ config[k] = v;
237
+ }
238
+ }
239
+
240
+ // Persist URL/token/botUserId, add "kazee" to CHANNELS, and boot the adapter
241
+ // (owner still blank). Snapshots the prior env for rollback on failure/cancel.
242
+ async function bringLive(session) {
243
+ const { url, token, botUserId } = session.data;
244
+ const previous = snapshotEnv();
245
+ session.data.previous = previous;
122
246
 
123
247
  saveEnvKey("KAZEE_URL", url);
124
248
  saveEnvKey("KAZEE_BOT_TOKEN", token);
125
- saveEnvKey("KAZEE_OWNER_USER_ID", ownerUserId);
126
-
249
+ saveEnvKey("KAZEE_BOT_USER_ID", botUserId);
127
250
  const channels = previous.CHANNELS.split(",").map((s) => s.trim()).filter(Boolean);
128
- if (!channels.some((c) => c === "kazee" || c.startsWith("kazee:"))) {
129
- channels.push("kazee");
130
- }
251
+ if (!channels.some((c) => c === "kazee" || c.startsWith("kazee:"))) channels.push("kazee");
131
252
  const newChannels = channels.join(",");
132
253
  saveEnvKey("CHANNELS", newChannels);
133
254
 
134
- // Refresh in-process view so subsequent loadChannels() sees the new
135
- // values without a restart.
136
255
  config.KAZEE_URL = url;
137
256
  config.KAZEE_BOT_TOKEN = token;
138
- config.KAZEE_OWNER_USER_ID = ownerUserId;
257
+ config.KAZEE_BOT_USER_ID = botUserId;
139
258
  config.CHANNELS = newChannels;
140
259
 
141
260
  const result = await registry.addAdapter(
142
- { id: "kazee", type: "kazee", opts: { url, token, ownerUserId } },
261
+ { id: "kazee", type: "kazee", opts: { url, token, ownerUserId: "", botUserId } },
143
262
  { publicCommands: publicCommands() },
144
263
  );
145
-
146
264
  if (!result.ok) {
147
- // Roll the .env back so the next bot start doesn't crash-loop.
148
- saveEnvKey("KAZEE_URL", previous.KAZEE_URL);
149
- saveEnvKey("KAZEE_BOT_TOKEN", previous.KAZEE_BOT_TOKEN);
150
- saveEnvKey("KAZEE_OWNER_USER_ID", previous.KAZEE_OWNER_USER_ID);
151
- saveEnvKey("CHANNELS", previous.CHANNELS);
152
- config.KAZEE_URL = previous.KAZEE_URL;
153
- config.KAZEE_BOT_TOKEN = previous.KAZEE_BOT_TOKEN;
154
- config.KAZEE_OWNER_USER_ID = previous.KAZEE_OWNER_USER_ID;
155
- config.CHANNELS = previous.CHANNELS;
156
- sessions.delete(canonicalUserId);
157
- await send(`Could not start the Kazee adapter: ${result.error}\n\nReverted .env. Run /channel add kazee to try again.`);
158
- return;
265
+ revertEnv(previous);
266
+ return result;
159
267
  }
268
+ session.data.adapterId = "kazee";
269
+ session.data.live = true;
270
+ return result;
271
+ }
160
272
 
273
+ // Owner decided (id or blank). Persist it, flip the live adapter's owner,
274
+ // and close the session.
275
+ async function finalizeOwner(canonicalUserId, session, ownerUserId) {
276
+ saveEnvKey("KAZEE_OWNER_USER_ID", ownerUserId);
277
+ config.KAZEE_OWNER_USER_ID = ownerUserId;
278
+ const adapter = registry.findAdapter("kazee");
279
+ if (adapter) adapter.ownerUserId = ownerUserId;
161
280
  sessions.delete(canonicalUserId);
162
- await send("Kazee channel added and live.");
281
+ await sayToOrigin(
282
+ session,
283
+ ownerUserId
284
+ ? "Kazee channel added and live. You're set as the owner — message me from Kazee and I'll know it's you. 🎉"
285
+ : "Kazee channel added and live. No owner set — run /channel add kazee again if you want to set one.",
286
+ );
163
287
  }
164
288
 
165
289
  module.exports = {
@@ -170,5 +294,6 @@ module.exports = {
170
294
  cancel,
171
295
  handleText,
172
296
  handleAction,
297
+ tryCaptureOwner,
173
298
  validateKazee,
174
299
  };
package/core/handlers.js CHANGED
@@ -1664,7 +1664,7 @@ register({
1664
1664
  if (addMatch) {
1665
1665
  const kind = addMatch[1].toLowerCase();
1666
1666
  if (kind !== "kazee") return send(`Channel type "${kind}" not supported.`);
1667
- await wizard.start(env.canonicalUserId, "kazee");
1667
+ await wizard.start(env.canonicalUserId, "kazee", env);
1668
1668
  return;
1669
1669
  }
1670
1670
 
@@ -6,10 +6,19 @@ const https = require("https");
6
6
  const http = require("http");
7
7
  const { URL } = require("url");
8
8
 
9
+ // The adapter mounts every REST route under {root}/api and talks socket.io
10
+ // at {root}. Callers may hand us either the bare root host or a URL that
11
+ // already ends in /api — normalise both to the root so we always probe the
12
+ // real {root}/api/me and the caller can store one canonical KAZEE_URL.
13
+ function normalizeKazeeRoot(url) {
14
+ return String(url || "").trim().replace(/\/+$/, "").replace(/\/api$/i, "");
15
+ }
16
+
9
17
  function validateKazee({ url, token }) {
10
18
  return new Promise((resolve) => {
19
+ const root = normalizeKazeeRoot(url);
11
20
  let u;
12
- try { u = new URL(url.replace(/\/$/, "") + "/me"); }
21
+ try { u = new URL(root + "/api/me"); }
13
22
  catch (e) { return resolve({ ok: false, error: `Bad URL: ${e.message}` }); }
14
23
  const lib = u.protocol === "https:" ? https : http;
15
24
  const req = lib.request({
@@ -26,16 +35,16 @@ function validateKazee({ url, token }) {
26
35
  try {
27
36
  const body = chunks ? JSON.parse(chunks) : {};
28
37
  const botUserId = body?.user?._id || body?.user?.id || body?._id || "";
29
- resolve({ ok: true, botUserId });
38
+ resolve({ ok: true, botUserId, url: root });
30
39
  } catch (e) {
31
- resolve({ ok: true, botUserId: "" });
40
+ resolve({ ok: true, botUserId: "", url: root });
32
41
  }
33
42
  } else if (res.statusCode === 401 || res.statusCode === 403) {
34
43
  resolve({ ok: false, error: "Token rejected (401/403). Check that it's a valid Kazee bot token." });
35
44
  } else if (res.statusCode === 404) {
36
- resolve({ ok: false, error: "GET /me returned 404. Check that the URL points at the Kazee bot API root." });
45
+ resolve({ ok: false, error: "GET /api/me returned 404. Check the URL points at your Kazee host (e.g. https://chat.example.com)." });
37
46
  } else {
38
- resolve({ ok: false, error: `GET /me → HTTP ${res.statusCode}` });
47
+ resolve({ ok: false, error: `GET /api/me → HTTP ${res.statusCode}` });
39
48
  }
40
49
  });
41
50
  });
@@ -45,4 +54,4 @@ function validateKazee({ url, token }) {
45
54
  });
46
55
  }
47
56
 
48
- module.exports = { validateKazee };
57
+ module.exports = { validateKazee, normalizeKazeeRoot };
package/core/router.js CHANGED
@@ -63,6 +63,12 @@ async function onMessage(envelope) {
63
63
  // owner's canonical bucket on first contact, before any state is read.
64
64
  try { autoLinkOwnerChannel(envelope.adapter?.type, envelope.channelId); } catch (e) {}
65
65
 
66
+ // A channel wizard listening for the owner's message on a freshly-added
67
+ // adapter consumes that inbound message here: it arrives on a different
68
+ // channel than the operator's, so the isAwaiting() gate below (keyed to
69
+ // the operator) never sees it.
70
+ if (await channelWizard.tryCaptureOwner(envelope)) return;
71
+
66
72
  // While a channel wizard is open, capture text input before slash
67
73
  // dispatch — except for /cancel, which always bails out.
68
74
  if (channelWizard.isAwaiting(envelope.canonicalUserId)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.13.1",
3
+ "version": "2.14.0",
4
4
  "description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
package/setup.js CHANGED
@@ -674,11 +674,11 @@ async function main() {
674
674
  const addKazee = await ask(" Add a Kazee channel now? (y/n) [n]: ");
675
675
  if (addKazee.toLowerCase() === "y") {
676
676
  while (true) {
677
- const url = (await ask(" Kazee bot API URL (e.g. https://chat.example.com/api): ")).trim();
677
+ const url = (await ask(" Kazee host URL (e.g. https://chat.example.com — I add /api myself): ")).trim();
678
678
  if (!/^https?:\/\//i.test(url)) { console.log(" That doesn't look like a URL."); continue; }
679
679
  const token = (await ask(" Kazee bot token: ")).trim();
680
680
  if (!token) { console.log(" Empty token."); continue; }
681
- console.log(" Validating against Kazee /me…");
681
+ console.log(" Validating against Kazee /api/me…");
682
682
  const check = await validateKazee({ url, token });
683
683
  if (!check.ok) {
684
684
  console.log(` Validation failed: ${check.error}`);
@@ -687,7 +687,7 @@ async function main() {
687
687
  continue;
688
688
  }
689
689
  const ownerUserId = (await ask(" Kazee owner user id (blank to skip): ")).trim();
690
- state.data.kazee = { url: url.replace(/\/$/, ""), token, ownerUserId };
690
+ state.data.kazee = { url: check.url || url.replace(/\/+$/, ""), token, ownerUserId, botUserId: check.botUserId || "" };
691
691
  console.log(" Kazee channel configured.");
692
692
  break;
693
693
  }
@@ -756,6 +756,7 @@ async function main() {
756
756
  if (d.kazee) {
757
757
  envLines.push(`KAZEE_URL=${d.kazee.url}`);
758
758
  envLines.push(`KAZEE_BOT_TOKEN=${d.kazee.token}`);
759
+ envLines.push(`KAZEE_BOT_USER_ID=${d.kazee.botUserId || ""}`);
759
760
  envLines.push(`KAZEE_OWNER_USER_ID=${d.kazee.ownerUserId || ""}`);
760
761
  }
761
762
  const env = envLines.join("\n");