slacklocalvibe 0.1.2 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slacklocalvibe",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "SlackLocalVibe: Codex/Claude Code turn notifications and reply→resume bridge for Slack DM (Socket Mode)",
5
5
  "bin": {
6
6
  "slacklocalvibe": "src/cli.js"
@@ -69,12 +69,26 @@ async function runNotify({ tool }) {
69
69
  throw new Error("通知対象のイベントではありません。");
70
70
  }
71
71
  if (input.skip) {
72
- log(LEVELS.WARNING, "notify.skip.empty_input_messages", {
72
+ const reason = input.skip_reason || "unknown";
73
+ log(LEVELS.WARNING, `notify.skip.${reason}`, {
73
74
  meta: input.meta || {},
74
75
  duration_ms: Date.now() - startedAt,
75
76
  });
76
77
  return;
77
78
  }
79
+ if (input.tool === "codex") {
80
+ const meta = input.meta || {};
81
+ log(LEVELS.INFO, "notify.codex_prompt_source", {
82
+ rollout_found: meta.codex_rollout_found,
83
+ rollout_source: meta.codex_rollout_source,
84
+ rollout_path: meta.codex_rollout_path,
85
+ rollout_user_messages: meta.codex_rollout_user_message_count,
86
+ rollout_line_count: meta.codex_rollout_line_count,
87
+ input_messages_len: meta.input_messages_len,
88
+ input_messages_has_content: meta.input_messages_has_content,
89
+ rollout_error: meta.codex_rollout_error,
90
+ });
91
+ }
78
92
 
79
93
  if (!input.session_id) {
80
94
  log(LEVELS.ERROR, "notify.session_missing");
@@ -102,21 +102,76 @@ async function runWizard() {
102
102
  console.log("SlackLocalVibe セットアップを開始します。");
103
103
  if (normalized) {
104
104
  console.log("既存設定が見つかりました。開始方法を選んでください。");
105
- const choice = await promptSelect({
106
- message: "どこから始めますか?",
107
- choices: [
108
- { title: "テストから始める", value: "test" },
109
- { title: "リセットして最初から", value: "reset" },
110
- { title: "終了(保存せず終了)", value: "exit" },
111
- ],
112
- initial: 0,
113
- });
114
- if (choice === "reset") {
115
- resetStoredConfig({ log });
116
- } else if (choice === "test") {
117
- startFromTest = true;
118
- console.log("既存設定を使って通知テストから開始します。");
119
- } else {
105
+ while (true) {
106
+ const choice = await promptSelect({
107
+ message: "どこから始めますか?",
108
+ choices: [
109
+ { title: "アップデートを確認する", value: "update" },
110
+ { title: "テストから始める", value: "test" },
111
+ { title: "リセットして最初からセットアップ", value: "reset" },
112
+ { title: "終了", value: "exit" },
113
+ ],
114
+ initial: 0,
115
+ });
116
+ if (choice === "update") {
117
+ log(LEVELS.INFO, "wizard.update_check_start");
118
+ const versionInfo = await fetchUpdateVersions({ log });
119
+ log(LEVELS.INFO, "wizard.update_versions", {
120
+ installed: versionInfo.installedVersion || "unknown",
121
+ latest: versionInfo.latestVersion || "unknown",
122
+ });
123
+ console.log(formatInfo(`現在: ${formatVersionLabel(versionInfo.installedVersion)}`));
124
+ console.log(formatInfo(`最新: ${formatVersionLabel(versionInfo.latestVersion)}`));
125
+ if (
126
+ versionInfo.installedVersion &&
127
+ versionInfo.latestVersion &&
128
+ versionInfo.installedVersion === versionInfo.latestVersion
129
+ ) {
130
+ log(LEVELS.SUCCRSS, "wizard.update_latest_confirmed");
131
+ console.log(formatSuccess("最新バージョンです。"));
132
+ continue;
133
+ }
134
+
135
+ const updateChoice = await promptSelect({
136
+ message: "アップデートしますか?",
137
+ choices: [
138
+ { title: "アップデートする", value: "update" },
139
+ { title: "戻る", value: "back" },
140
+ { title: "終了", value: "exit" },
141
+ ],
142
+ initial: 0,
143
+ });
144
+ if (updateChoice === "back") {
145
+ log(LEVELS.INFO, "wizard.update_skipped");
146
+ continue;
147
+ }
148
+ if (updateChoice === "exit") {
149
+ throw new UserExit();
150
+ }
151
+
152
+ await ensureGlobalInstall({ log });
153
+ const postUpdate = await fetchUpdateVersions({ log });
154
+ log(LEVELS.SUCCRSS, "wizard.update_applied", {
155
+ installed: postUpdate.installedVersion || "unknown",
156
+ latest: postUpdate.latestVersion || "unknown",
157
+ });
158
+ console.log(formatSuccess("アップデートが完了しました。"));
159
+ console.log(formatInfo(`現在: ${formatVersionLabel(postUpdate.installedVersion)}`));
160
+ console.log(formatInfo(`最新: ${formatVersionLabel(postUpdate.latestVersion)}`));
161
+
162
+ const launchdRecommended = Boolean(normalized?.features?.launchd_enabled);
163
+ await promptLaunchdReinstall({ log, recommended: launchdRecommended });
164
+ continue;
165
+ }
166
+ if (choice === "reset") {
167
+ resetStoredConfig({ log });
168
+ break;
169
+ }
170
+ if (choice === "test") {
171
+ startFromTest = true;
172
+ console.log("既存設定を使って通知テストから開始します。");
173
+ break;
174
+ }
120
175
  throw new UserExit();
121
176
  }
122
177
  }
@@ -147,7 +202,7 @@ async function runWizard() {
147
202
  message: "次の操作を選んでください",
148
203
  choices: [
149
204
  { title: "リセットして最初から", value: "reset" },
150
- { title: "終了(保存せず終了)", value: "exit" },
205
+ { title: "終了", value: "exit" },
151
206
  ],
152
207
  });
153
208
  if (next === "reset") {
@@ -349,7 +404,7 @@ async function stepReplySetup({ log }) {
349
404
  message: "次の操作を選んでください",
350
405
  choices: [
351
406
  { title: "npm i -g slacklocalvibe で登録する(必須)", value: "install" },
352
- { title: "終了(保存せず終了)", value: "exit" },
407
+ { title: "終了", value: "exit" },
353
408
  ],
354
409
  initial: 0,
355
410
  });
@@ -412,7 +467,7 @@ async function stepNotifyTest({ log, botToken, dmConfig }) {
412
467
  choices: [
413
468
  { title: "Bot Token を再入力する", value: "retry_token" },
414
469
  { title: "送信先(DM)を見直す", value: "retry_dm" },
415
- { title: "終了(保存せず終了)", value: "exit" },
470
+ { title: "終了", value: "exit" },
416
471
  ],
417
472
  });
418
473
  if (choice === "retry_token") {
@@ -472,7 +527,7 @@ async function stepCodexConfig({ log }) {
472
527
  message: "Codex設定の更新に失敗しました。次の操作を選んでください",
473
528
  choices: [
474
529
  { title: "再試行", value: "retry" },
475
- { title: "終了(保存せず終了)", value: "exit" },
530
+ { title: "終了", value: "exit" },
476
531
  ],
477
532
  });
478
533
  if (next === "retry") continue;
@@ -506,7 +561,7 @@ async function stepClaudeConfig({ log }) {
506
561
  message: "Claude設定の更新に失敗しました。次の操作を選んでください",
507
562
  choices: [
508
563
  { title: "再試行", value: "retry" },
509
- { title: "終了(保存せず終了)", value: "exit" },
564
+ { title: "終了", value: "exit" },
510
565
  ],
511
566
  });
512
567
  if (next === "retry") continue;
@@ -541,7 +596,7 @@ async function stepDeliveryConfirmation({ log, stage }) {
541
596
  choices: [
542
597
  { title: "届いたので次へ進む", value: "ok" },
543
598
  { title: "届いていないのでログを表示する", value: "logs" },
544
- { title: "終了(保存せず終了)", value: "exit" },
599
+ { title: "終了", value: "exit" },
545
600
  ],
546
601
  });
547
602
  if (choice === "ok") {
@@ -654,7 +709,7 @@ async function runTestCommand({ log, tool }) {
654
709
  message: "次の操作を選んでください",
655
710
  choices: [
656
711
  { title: "再試行", value: "retry" },
657
- { title: "終了(保存せず終了)", value: "exit" },
712
+ { title: "終了", value: "exit" },
658
713
  ],
659
714
  });
660
715
  if (next === "retry") continue;
@@ -672,7 +727,7 @@ async function stepLaunchd({ log }) {
672
727
  choices: [
673
728
  { title: "launchd に登録する(推奨)", value: "install" },
674
729
  { title: "スキップして次へ進む", value: "skip" },
675
- { title: "終了(保存せず終了)", value: "exit" },
730
+ { title: "終了", value: "exit" },
676
731
  ],
677
732
  initial: 0,
678
733
  });
@@ -708,7 +763,7 @@ async function stepLaunchd({ log }) {
708
763
  choices: [
709
764
  { title: "再試行", value: "retry" },
710
765
  { title: "スキップして次へ進む", value: "skip" },
711
- { title: "終了(保存せず終了)", value: "exit" },
766
+ { title: "終了", value: "exit" },
712
767
  ],
713
768
  });
714
769
  if (next === "retry") continue;
@@ -919,7 +974,7 @@ async function copyManifestToClipboard({ log }) {
919
974
  message: "次の操作を選んでください",
920
975
  choices: [
921
976
  { title: "再試行", value: "retry" },
922
- { title: "終了(保存せず終了)", value: "exit" },
977
+ { title: "終了", value: "exit" },
923
978
  ],
924
979
  });
925
980
  if (next === "retry") continue;
@@ -1061,7 +1116,7 @@ async function ensureGlobalInstall({ log }) {
1061
1116
  message: "次の操作を選んでください",
1062
1117
  choices: [
1063
1118
  { title: "再試行", value: "retry" },
1064
- { title: "終了(保存せず終了)", value: "exit" },
1119
+ { title: "終了", value: "exit" },
1065
1120
  ],
1066
1121
  });
1067
1122
  if (next === "retry") continue;
@@ -1070,6 +1125,124 @@ async function ensureGlobalInstall({ log }) {
1070
1125
  }
1071
1126
  }
1072
1127
 
1128
+ async function fetchUpdateVersions({ log }) {
1129
+ const [installedVersion, latestVersion] = await Promise.all([
1130
+ fetchInstalledVersion({ log }),
1131
+ fetchRegistryVersion({ log }),
1132
+ ]);
1133
+ return { installedVersion, latestVersion };
1134
+ }
1135
+
1136
+ async function fetchInstalledVersion({ log }) {
1137
+ try {
1138
+ const binaryPath = resolveBinaryPath();
1139
+ const result = await spawnCommand({
1140
+ command: binaryPath,
1141
+ args: ["--version"],
1142
+ cwd: process.cwd(),
1143
+ });
1144
+ if (result.code === 0) {
1145
+ const version = (result.stdoutText || "").trim().split(/\s+/)[0];
1146
+ if (version) return version;
1147
+ }
1148
+ log(LEVELS.WARNING, "wizard.update_installed_version_empty", {
1149
+ stdout_len: result.stdoutLen,
1150
+ stderr_len: result.stderrLen,
1151
+ });
1152
+ } catch (error) {
1153
+ log(LEVELS.WARNING, "wizard.update_installed_version_failed", {
1154
+ error: safeError(error),
1155
+ });
1156
+ }
1157
+ return "";
1158
+ }
1159
+
1160
+ async function fetchRegistryVersion({ log }) {
1161
+ try {
1162
+ const npmPath = resolveCommandPathStrict("npm");
1163
+ const result = await spawnCommand({
1164
+ command: npmPath,
1165
+ args: ["view", "slacklocalvibe", "version"],
1166
+ cwd: process.cwd(),
1167
+ });
1168
+ if (result.code === 0) {
1169
+ const version = (result.stdoutText || "").trim().split(/\s+/)[0];
1170
+ if (version) return version;
1171
+ }
1172
+ log(LEVELS.WARNING, "wizard.update_registry_version_empty", {
1173
+ stdout_len: result.stdoutLen,
1174
+ stderr_len: result.stderrLen,
1175
+ });
1176
+ } catch (error) {
1177
+ log(LEVELS.WARNING, "wizard.update_registry_version_failed", {
1178
+ error: safeError(error),
1179
+ });
1180
+ }
1181
+ return "";
1182
+ }
1183
+
1184
+ function formatVersionLabel(version) {
1185
+ return version ? version : "不明";
1186
+ }
1187
+
1188
+ async function promptLaunchdReinstall({ log, recommended }) {
1189
+ const title = recommended
1190
+ ? "launchd を再登録する(推奨)"
1191
+ : "launchd を再登録する";
1192
+ while (true) {
1193
+ const choice = await promptSelect({
1194
+ message: "更新後に launchd を再登録します",
1195
+ choices: [
1196
+ { title, value: "install" },
1197
+ { title: "あとでやる", value: "skip" },
1198
+ { title: "終了", value: "exit" },
1199
+ ],
1200
+ initial: 0,
1201
+ });
1202
+ if (choice === "skip") {
1203
+ log(LEVELS.INFO, "wizard.launchd_reinstall_skipped");
1204
+ return;
1205
+ }
1206
+ if (choice === "exit") {
1207
+ throw new UserExit();
1208
+ }
1209
+ try {
1210
+ installLaunchd();
1211
+ log(LEVELS.SUCCRSS, "wizard.launchd_reinstalled");
1212
+ console.log(formatSuccess("launchd を再登録しました。"));
1213
+ return;
1214
+ } catch (error) {
1215
+ log(LEVELS.ERROR, "wizard.launchd_reinstall_failed", {
1216
+ error: safeError(error),
1217
+ });
1218
+ console.log(formatError("launchd の再登録に失敗しました。"));
1219
+ const detail =
1220
+ error?.detail ||
1221
+ error?.stderrText ||
1222
+ error?.stdoutText ||
1223
+ error?.message ||
1224
+ "unknown";
1225
+ if (detail) {
1226
+ console.log(formatError(detail));
1227
+ }
1228
+ const next = await promptSelect({
1229
+ message: "次の操作を選んでください",
1230
+ choices: [
1231
+ { title: "再試行", value: "retry" },
1232
+ { title: "あとでやる", value: "skip" },
1233
+ { title: "終了", value: "exit" },
1234
+ ],
1235
+ });
1236
+ if (next === "retry") continue;
1237
+ if (next === "skip") {
1238
+ log(LEVELS.WARNING, "wizard.launchd_reinstall_skipped_after_error");
1239
+ return;
1240
+ }
1241
+ throw new UserExit();
1242
+ }
1243
+ }
1244
+ }
1245
+
1073
1246
  async function promptPassword({ message, validate }) {
1074
1247
  while (true) {
1075
1248
  const value = await promptVisible({ message });
@@ -1,4 +1,6 @@
1
1
  const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
2
4
 
3
5
  function parseCodexNotify(rawJson) {
4
6
  const payload = JSON.parse(rawJson);
@@ -9,16 +11,19 @@ function parseCodexNotify(rawJson) {
9
11
  const turnId = payload["turn-id"] ? String(payload["turn-id"]) : undefined;
10
12
  const inputMessages = payload["input-messages"];
11
13
  const meta = buildCodexInputMeta(inputMessages);
12
- const hasInputMessages = hasNonEmptyInputMessages(inputMessages);
13
- if (!hasInputMessages) {
14
+ const codexHomeInfo = resolveCodexHomeInfo();
15
+ Object.assign(meta, codexHomeInfo.meta || {});
16
+ if (codexHomeInfo.isDefault) {
14
17
  return {
15
18
  tool: "codex",
16
19
  skip: true,
17
- skip_reason: "empty_input_messages",
20
+ skip_reason: "codex_home_default",
18
21
  meta,
19
22
  };
20
23
  }
21
- const userText = extractUserTextFromCodex(inputMessages);
24
+ const rolloutResult = readCodexUserMessageFromRollout(sessionId, codexHomeInfo.home);
25
+ Object.assign(meta, rolloutResult.meta || {});
26
+ const userText = rolloutResult.userText || "";
22
27
  const assistantText = extractAssistantText(payload["last-assistant-message"]);
23
28
  const cwd = payload?.cwd ? String(payload.cwd) : "";
24
29
 
@@ -33,16 +38,11 @@ function parseCodexNotify(rawJson) {
33
38
  };
34
39
  }
35
40
 
36
- function extractUserTextFromCodex(inputMessages) {
37
- if (!Array.isArray(inputMessages) || inputMessages.length === 0) return "";
38
- const lastMessage = inputMessages[inputMessages.length - 1];
39
- return normalizeContent(lastMessage);
40
- }
41
-
42
41
  function buildCodexInputMeta(inputMessages) {
43
42
  const meta = {
44
43
  input_messages_type: Array.isArray(inputMessages) ? "array" : typeof inputMessages,
45
44
  input_messages_len: Array.isArray(inputMessages) ? inputMessages.length : 0,
45
+ input_messages_has_content: hasNonEmptyInputMessages(inputMessages),
46
46
  input_messages_roles: [],
47
47
  input_messages_last_role: "",
48
48
  input_messages_last_type: "",
@@ -78,6 +78,210 @@ function extractAssistantText(content) {
78
78
  return normalizeContent(content);
79
79
  }
80
80
 
81
+ function readCodexUserMessageFromRollout(sessionId, codexHome) {
82
+ const meta = {
83
+ codex_rollout_found: false,
84
+ codex_rollout_source: "",
85
+ codex_rollout_path: "",
86
+ codex_rollout_mtime_ms: 0,
87
+ codex_rollout_total: 0,
88
+ codex_rollout_user_message_count: 0,
89
+ codex_rollout_line_count: 0,
90
+ codex_rollout_error: "",
91
+ };
92
+ const rolloutPath = findCodexRolloutPath(sessionId, meta, codexHome);
93
+ if (!rolloutPath) {
94
+ return { userText: "", meta };
95
+ }
96
+ try {
97
+ const content = fs.readFileSync(rolloutPath, "utf8");
98
+ const lines = content.split("\n").filter(Boolean);
99
+ meta.codex_rollout_line_count = lines.length;
100
+ let lastUser = "";
101
+ let lastTs = "";
102
+ let count = 0;
103
+ for (const line of lines) {
104
+ let record;
105
+ try {
106
+ record = JSON.parse(line);
107
+ } catch {
108
+ continue;
109
+ }
110
+ if (record?.type !== "event_msg") continue;
111
+ const payload = record?.payload;
112
+ if (payload?.type !== "user_message") continue;
113
+ const text = extractCodexUserMessage(payload);
114
+ if (text) {
115
+ lastUser = text;
116
+ lastTs = record?.timestamp || "";
117
+ }
118
+ count += 1;
119
+ }
120
+ meta.codex_rollout_user_message_count = count;
121
+ if (lastTs) {
122
+ meta.codex_rollout_last_user_ts = lastTs;
123
+ }
124
+ return { userText: lastUser, meta };
125
+ } catch (error) {
126
+ meta.codex_rollout_error = error?.message || "rollout_read_failed";
127
+ return { userText: "", meta };
128
+ }
129
+ }
130
+
131
+ function extractCodexUserMessage(payload) {
132
+ if (!payload || typeof payload !== "object") return "";
133
+ const content =
134
+ payload.message ||
135
+ payload.text ||
136
+ payload.prompt ||
137
+ payload.input ||
138
+ payload.content;
139
+ return normalizeContent(content);
140
+ }
141
+
142
+ function findCodexRolloutPath(sessionId, meta, codexHome) {
143
+ if (!sessionId) {
144
+ meta.codex_rollout_error = "session_id_missing";
145
+ return "";
146
+ }
147
+ const baseHome = codexHome || path.join(os.homedir(), ".codex");
148
+ const sessionsDir = path.join(baseHome, "sessions");
149
+ meta.codex_sessions_dir = sessionsDir;
150
+ if (!fs.existsSync(sessionsDir)) {
151
+ meta.codex_rollout_error = "sessions_dir_missing";
152
+ return "";
153
+ }
154
+
155
+ const files = collectRolloutFiles(sessionsDir);
156
+ meta.codex_rollout_total = files.length;
157
+ const byName = files.filter((item) => item.name.includes(sessionId));
158
+ if (byName.length > 0) {
159
+ const best = pickLatestFile(byName);
160
+ if (best) {
161
+ meta.codex_rollout_found = true;
162
+ meta.codex_rollout_source = "filename";
163
+ meta.codex_rollout_path = best.path;
164
+ meta.codex_rollout_mtime_ms = best.mtimeMs;
165
+ return best.path;
166
+ }
167
+ }
168
+
169
+ const byContent = findRolloutByContent(files, sessionId, meta);
170
+ if (byContent) {
171
+ meta.codex_rollout_found = true;
172
+ meta.codex_rollout_source = "content";
173
+ meta.codex_rollout_path = byContent.path;
174
+ meta.codex_rollout_mtime_ms = byContent.mtimeMs;
175
+ return byContent.path;
176
+ }
177
+
178
+ meta.codex_rollout_error = meta.codex_rollout_error || "rollout_not_found";
179
+ return "";
180
+ }
181
+
182
+ function collectRolloutFiles(rootDir) {
183
+ const results = [];
184
+ const stack = [rootDir];
185
+ while (stack.length > 0) {
186
+ const dir = stack.pop();
187
+ let entries;
188
+ try {
189
+ entries = fs.readdirSync(dir, { withFileTypes: true });
190
+ } catch {
191
+ continue;
192
+ }
193
+ for (const entry of entries) {
194
+ const fullPath = path.join(dir, entry.name);
195
+ if (entry.isDirectory()) {
196
+ stack.push(fullPath);
197
+ continue;
198
+ }
199
+ if (!entry.isFile()) continue;
200
+ if (!entry.name.startsWith("rollout-") || !entry.name.endsWith(".jsonl")) {
201
+ continue;
202
+ }
203
+ let stat;
204
+ try {
205
+ stat = fs.statSync(fullPath);
206
+ } catch {
207
+ continue;
208
+ }
209
+ results.push({
210
+ path: fullPath,
211
+ name: entry.name,
212
+ mtimeMs: stat.mtimeMs || 0,
213
+ });
214
+ }
215
+ }
216
+ return results;
217
+ }
218
+
219
+ function pickLatestFile(files) {
220
+ if (!files || files.length === 0) return null;
221
+ let best = files[0];
222
+ for (const item of files) {
223
+ if (item.mtimeMs > best.mtimeMs) {
224
+ best = item;
225
+ }
226
+ }
227
+ return best;
228
+ }
229
+
230
+ function findRolloutByContent(files, sessionId, meta) {
231
+ if (!files || files.length === 0) {
232
+ meta.codex_rollout_error = "rollout_files_empty";
233
+ return null;
234
+ }
235
+ const sorted = [...files].sort((a, b) => b.mtimeMs - a.mtimeMs);
236
+ const limit = 30;
237
+ for (let i = 0; i < sorted.length && i < limit; i += 1) {
238
+ const file = sorted[i];
239
+ let content = "";
240
+ try {
241
+ content = fs.readFileSync(file.path, "utf8");
242
+ } catch {
243
+ continue;
244
+ }
245
+ if (content.includes(sessionId)) {
246
+ return file;
247
+ }
248
+ }
249
+ meta.codex_rollout_error = "rollout_not_matched_in_recent_files";
250
+ return null;
251
+ }
252
+
253
+ function resolveCodexHomeInfo() {
254
+ const defaultHome = path.join(os.homedir(), ".codex");
255
+ const defaultResolved = path.resolve(defaultHome);
256
+ const envHome = process.env.CODEX_HOME;
257
+ if (!envHome) {
258
+ return {
259
+ home: defaultResolved,
260
+ isDefault: true,
261
+ isSet: false,
262
+ meta: {
263
+ codex_home: defaultResolved,
264
+ codex_home_default: defaultResolved,
265
+ codex_home_is_default: true,
266
+ codex_home_set: false,
267
+ },
268
+ };
269
+ }
270
+ const resolved = path.resolve(envHome);
271
+ const isDefault = resolved === defaultResolved;
272
+ return {
273
+ home: resolved,
274
+ isDefault,
275
+ isSet: true,
276
+ meta: {
277
+ codex_home: resolved,
278
+ codex_home_default: defaultResolved,
279
+ codex_home_is_default: isDefault,
280
+ codex_home_set: true,
281
+ },
282
+ };
283
+ }
284
+
81
285
  function extractTextDeep(value, depth = 0) {
82
286
  if (depth > 6) return "";
83
287
  if (value === null || value === undefined) return "";