@optimuslabs/harness-map-staging 1.5.16

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.
Files changed (88) hide show
  1. package/README.md +46 -0
  2. package/dist/apply_deferred_vscdb.js +38 -0
  3. package/dist/bootstrap_constants.js +15 -0
  4. package/dist/cli/bash_script_generator.js +95 -0
  5. package/dist/cli.js +144 -0
  6. package/dist/cli_invocation_match.js +30 -0
  7. package/dist/compliance_check_runner.js +57 -0
  8. package/dist/compliance_prompt_gate.js +396 -0
  9. package/dist/dialog_prefs_cli.js +109 -0
  10. package/dist/endpoint_client/compress_payload.js +25 -0
  11. package/dist/endpoint_client/http_transport.js +90 -0
  12. package/dist/endpoint_client/index.js +3 -0
  13. package/dist/endpoint_client/registry_api.js +41 -0
  14. package/dist/endpoint_client/startup_api.js +44 -0
  15. package/dist/endpoint_client/types.js +4 -0
  16. package/dist/execute_trusted_restarts.js +60 -0
  17. package/dist/log_config_files/auth/auth_flow.js +22 -0
  18. package/dist/log_config_files/auth/auth_key_store.js +14 -0
  19. package/dist/log_config_files/collection/claude_desktop_extensions_collector.js +248 -0
  20. package/dist/log_config_files/collection/claude_known_projects_collector.js +42 -0
  21. package/dist/log_config_files/collection/claude_token_usage_collector.js +203 -0
  22. package/dist/log_config_files/collection/codex_token_usage_collector.js +342 -0
  23. package/dist/log_config_files/collection/config_collector.js +245 -0
  24. package/dist/log_config_files/collection/copilot_token_usage_collector.js +167 -0
  25. package/dist/log_config_files/collection/cowork_desktop_version_collector.js +60 -0
  26. package/dist/log_config_files/collection/cowork_session_whitelist.js +89 -0
  27. package/dist/log_config_files/collection/cursor_project_mcp_collector.js +230 -0
  28. package/dist/log_config_files/collection/cursor_project_workspace_activity.js +73 -0
  29. package/dist/log_config_files/collection/cursor_token_usage_collector.js +192 -0
  30. package/dist/log_config_files/collection/directory_collector.js +126 -0
  31. package/dist/log_config_files/collection/enrichment_helpers.js +53 -0
  32. package/dist/log_config_files/collection/ensure_cursor_user_settings_snapshot.js +50 -0
  33. package/dist/log_config_files/collection/file_type_rules.js +47 -0
  34. package/dist/log_config_files/collection/grok_cli_version_collector.js +53 -0
  35. package/dist/log_config_files/collection/hermes_token_usage_collector.js +241 -0
  36. package/dist/log_config_files/collection/mcp_tool_collector.js +106 -0
  37. package/dist/log_config_files/collection/metadata_merge.js +43 -0
  38. package/dist/log_config_files/collection/openclaw_helpers.js +55 -0
  39. package/dist/log_config_files/collection/openclaw_token_usage_collector.js +223 -0
  40. package/dist/log_config_files/collection/opencode_token_usage_collector.js +177 -0
  41. package/dist/log_config_files/collection/pi_token_usage_collector.js +239 -0
  42. package/dist/log_config_files/collection/plugin_collector.js +223 -0
  43. package/dist/log_config_files/collection/plugin_version_helpers.js +37 -0
  44. package/dist/log_config_files/collection/skills_cli_collector.js +162 -0
  45. package/dist/log_config_files/index.js +19 -0
  46. package/dist/log_config_files/paths/path_constants_helpers.js +71 -0
  47. package/dist/log_config_files/paths/pattern_resolver.js +326 -0
  48. package/dist/log_config_files/readers/composer_shadow_merge.js +67 -0
  49. package/dist/log_config_files/readers/cursor_shadow_merge_ruleset.js +46 -0
  50. package/dist/log_config_files/readers/file_readers.js +196 -0
  51. package/dist/log_config_files/readers/vscdb_config_builder.js +148 -0
  52. package/dist/log_config_files/readers/vscdb_reactive_storage.js +62 -0
  53. package/dist/log_config_files/readers/vscdb_reader.js +337 -0
  54. package/dist/log_config_files/runtime/client_event_reporter.js +347 -0
  55. package/dist/log_config_files/runtime/compliance_check.js +1013 -0
  56. package/dist/log_config_files/runtime/compliance_session_log.js +378 -0
  57. package/dist/log_config_files/runtime/dialog_preferences.js +31 -0
  58. package/dist/log_config_files/runtime/hardware_uuid.js +36 -0
  59. package/dist/log_config_files/runtime/hook_logger.js +267 -0
  60. package/dist/log_config_files/runtime/hook_type_for_request.js +30 -0
  61. package/dist/log_config_files/runtime/log_metadata.js +122 -0
  62. package/dist/log_config_files/runtime/main_runner.js +376 -0
  63. package/dist/log_config_files/runtime/management_storage.js +144 -0
  64. package/dist/log_config_files/runtime/ops_target_path.js +18 -0
  65. package/dist/log_config_files/runtime/remediation_apply_tracking.js +152 -0
  66. package/dist/log_config_files/runtime/remediation_config_path.js +140 -0
  67. package/dist/log_config_files/runtime/remediation_sync.js +1705 -0
  68. package/dist/log_config_files/runtime/secret_regex_scan.js +126 -0
  69. package/dist/log_config_files/runtime/sqlite_binary.js +124 -0
  70. package/dist/log_config_files/runtime/trusted_restarts.js +113 -0
  71. package/dist/log_config_files/runtime/workspace_repo.js +170 -0
  72. package/dist/log_config_files/runtime/worktree_absent.js +50 -0
  73. package/dist/log_config_files/runtime/worktree_scanner.js +137 -0
  74. package/dist/log_config_files/sender/batch_sender.js +283 -0
  75. package/dist/log_config_files/sender/endpoint_config.js +24 -0
  76. package/dist/log_config_files/sender/signing.js +1 -0
  77. package/dist/log_sensitive_paths_audit.js +89 -0
  78. package/dist/log_uuid/auth_key_store.js +37 -0
  79. package/dist/log_uuid/hardware_uuid.js +35 -0
  80. package/dist/log_uuid/index.js +11 -0
  81. package/dist/log_uuid/log_uuid_helper.js +30 -0
  82. package/dist/log_uuid/startup_sender.js +131 -0
  83. package/dist/log_uuid/user_profile.js +192 -0
  84. package/dist/remediation_change_preview.js +182 -0
  85. package/dist/tofu.js +22 -0
  86. package/dist/tofu_environment.js +36 -0
  87. package/dist/types/config_file_types.js +1 -0
  88. package/package.json +67 -0
@@ -0,0 +1,267 @@
1
+ import { existsSync, mkdirSync, appendFileSync, writeFileSync, statSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { OPTIMUSLABS_LOGS_REL, LOG_GROUP_HOOK, LOG_GROUP_COMPLIANCE, } from '../../bootstrap_constants.js';
4
+ import { getActiveComplianceLogPath } from './compliance_session_log.js';
5
+ import { buildLogMetadataBlock, formatErrorBlock } from './log_metadata.js';
6
+ const HOOK_LOG_FILENAME = 'hook_log.txt';
7
+ const COMPLIANCE_RUNNER_LOG_FILENAME = 'compliance_runner.log';
8
+ const COMPLIANCE_FALLBACK_TIER = 'noop';
9
+ /** Hard cap so a single upload/sync session cannot grow hook_log.txt without bound. */
10
+ const MAX_HOOK_LOG_BYTES = 2 * 1024 * 1024;
11
+ function getHookLogPath() {
12
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
13
+ if (!homeDir)
14
+ return null;
15
+ return path.join(homeDir, OPTIMUSLABS_LOGS_REL, LOG_GROUP_HOOK, HOOK_LOG_FILENAME);
16
+ }
17
+ function getComplianceRunnerLogPath() {
18
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
19
+ if (!homeDir)
20
+ return null;
21
+ return path.join(homeDir, OPTIMUSLABS_LOGS_REL, LOG_GROUP_COMPLIANCE, COMPLIANCE_FALLBACK_TIER, COMPLIANCE_RUNNER_LOG_FILENAME);
22
+ }
23
+ /**
24
+ * Append one ISO-timestamped line to the active compliance session log.
25
+ * If no session is active, use compliance/noop/compliance_runner.log as a legacy fallback.
26
+ * `level` examples: INFO, RUNNER, RESTART, FAIL
27
+ */
28
+ function appendComplianceRunnerLine(level, message) {
29
+ const sessionPath = getActiveComplianceLogPath();
30
+ if (sessionPath) {
31
+ try {
32
+ appendFileSync(sessionPath, `${new Date().toISOString()} [${level}] pid=${process.pid} ${message}\n`, 'utf8');
33
+ }
34
+ catch {
35
+ // best-effort
36
+ }
37
+ return;
38
+ }
39
+ const logPath = getComplianceRunnerLogPath();
40
+ if (!logPath)
41
+ return;
42
+ try {
43
+ const dir = path.dirname(logPath);
44
+ if (!existsSync(dir))
45
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
46
+ const ts = new Date().toISOString();
47
+ appendFileSync(logPath, `${ts} [${level}] pid=${process.pid} ${message}\n`, 'utf8');
48
+ }
49
+ catch {
50
+ // best-effort
51
+ }
52
+ }
53
+ /**
54
+ * Append-only diagnostics for remediation sync / compliance (structured).
55
+ */
56
+ function complianceRunnerDiag(message) {
57
+ appendComplianceRunnerLine('INFO', message);
58
+ }
59
+ /**
60
+ * Background compliance_check_runner should use this so entries match the same format.
61
+ */
62
+ function complianceRunnerRunnerLine(message) {
63
+ appendComplianceRunnerLine('RUNNER', message);
64
+ }
65
+ /**
66
+ * Loud record when a remediation cannot be verified or applied. Writes the block to the active
67
+ * compliance session log (which finalizes into the failures/ tier), and appends a single summary
68
+ * line to hook_log.txt for the current session. No separate permanent ledger — the failure detail
69
+ * lives in the per-session compliance log.
70
+ */
71
+ function logRemediationApplyFailure(context, fields) {
72
+ const ts = new Date().toISOString();
73
+ const bodyLines = Object.entries(fields)
74
+ .filter(([, v]) => v !== undefined && v !== null && String(v) !== '')
75
+ .map(([k, v]) => ` ${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`);
76
+ const block = [
77
+ '',
78
+ '='.repeat(72),
79
+ `${ts} REMEDIATION APPLY FAILURE — ${context}`,
80
+ ...bodyLines,
81
+ '='.repeat(72),
82
+ '',
83
+ ].join('\n');
84
+ const summary = fields.reason != null
85
+ ? String(fields.reason)
86
+ : fields.message != null
87
+ ? String(fields.message)
88
+ : 'see fields above';
89
+ const writeFailAppend = (filePath) => {
90
+ const dir = path.dirname(filePath);
91
+ if (!existsSync(dir))
92
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
93
+ appendFileSync(filePath, block, 'utf8');
94
+ };
95
+ try {
96
+ const activeCompliancePath = getActiveComplianceLogPath();
97
+ if (activeCompliancePath) {
98
+ writeFailAppend(activeCompliancePath);
99
+ }
100
+ else {
101
+ const crPath = getComplianceRunnerLogPath();
102
+ if (crPath) {
103
+ const dir = path.dirname(crPath);
104
+ if (!existsSync(dir))
105
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
106
+ appendFileSync(crPath, block, 'utf8');
107
+ }
108
+ }
109
+ }
110
+ catch {
111
+ /* best-effort */
112
+ }
113
+ try {
114
+ hookRunLog(`REMEDIATION_APPLY_FAILURE [${context}] ${fields.uuid != null ? `uuid=${fields.uuid} ` : ''}${summary}`);
115
+ }
116
+ catch {
117
+ /* best-effort */
118
+ }
119
+ appendComplianceRunnerLine('FAIL', `${context} — ${summary}`);
120
+ }
121
+ function ensureHookLogUnderCap() {
122
+ const logPath = getHookLogPath();
123
+ if (!logPath || !existsSync(logPath))
124
+ return;
125
+ try {
126
+ if (statSync(logPath).size <= MAX_HOOK_LOG_BYTES)
127
+ return;
128
+ const ts = new Date().toISOString();
129
+ writeFileSync(logPath, `${'='.repeat(72)}\n${ts} hook_log.txt truncated (exceeded ${MAX_HOOK_LOG_BYTES} bytes)\n${'='.repeat(72)}\n`, 'utf8');
130
+ }
131
+ catch {
132
+ // best-effort
133
+ }
134
+ }
135
+ /**
136
+ * Start a new hook_log.txt session (replaces the file). Use once per logical run
137
+ * (e.g. compliance_prompt_gate, log_config_files upload via hookLogReplace).
138
+ */
139
+ function hookLogSessionBanner(label) {
140
+ const logPath = getHookLogPath();
141
+ if (!logPath)
142
+ return;
143
+ try {
144
+ const dir = path.dirname(logPath);
145
+ if (!existsSync(dir))
146
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
147
+ const ts = new Date().toISOString();
148
+ const banner = `\n${'='.repeat(72)}\n${ts} session: ${label}\n${'='.repeat(72)}\n`;
149
+ writeFileSync(logPath, banner + buildLogMetadataBlock(), 'utf8');
150
+ }
151
+ catch {
152
+ // best-effort
153
+ }
154
+ }
155
+ /**
156
+ * Record a structured error block to the active compliance session log (if any) or
157
+ * hook_log.txt. Prefer this over silent best-effort catches so failures carry the
158
+ * error name/message/stack needed to diagnose a customer report.
159
+ */
160
+ function hookLogError(context, err, fields) {
161
+ const block = formatErrorBlock(context, err, fields);
162
+ const compliancePath = getActiveComplianceLogPath();
163
+ const logPath = compliancePath ?? getHookLogPath();
164
+ if (!logPath)
165
+ return;
166
+ try {
167
+ const dir = path.dirname(logPath);
168
+ if (!existsSync(dir))
169
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
170
+ if (!compliancePath)
171
+ ensureHookLogUnderCap();
172
+ appendFileSync(logPath, block, 'utf8');
173
+ }
174
+ catch {
175
+ // best-effort
176
+ }
177
+ }
178
+ /**
179
+ * Append a subsection after an existing session (e.g. compliance_check_runner after the gate)
180
+ * without replacing hook_log.txt.
181
+ */
182
+ function hookLogAppendSection(label) {
183
+ const compliancePath = getActiveComplianceLogPath();
184
+ if (compliancePath) {
185
+ try {
186
+ const ts = new Date().toISOString();
187
+ const banner = `\n${'-'.repeat(72)}\n${ts} section: ${label}\n${'-'.repeat(72)}\n`;
188
+ appendFileSync(compliancePath, banner, 'utf8');
189
+ }
190
+ catch {
191
+ // best-effort
192
+ }
193
+ return;
194
+ }
195
+ const logPath = getHookLogPath();
196
+ if (!logPath)
197
+ return;
198
+ try {
199
+ ensureHookLogUnderCap();
200
+ const dir = path.dirname(logPath);
201
+ if (!existsSync(dir))
202
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
203
+ const ts = new Date().toISOString();
204
+ const banner = `\n${'-'.repeat(72)}\n${ts} section: ${label}\n${'-'.repeat(72)}\n`;
205
+ appendFileSync(logPath, banner, 'utf8');
206
+ }
207
+ catch {
208
+ // best-effort
209
+ }
210
+ }
211
+ /** Begins a log_config_files upload session (replaces hook_log.txt, same as hookLogSessionBanner). */
212
+ function hookLogReplace() {
213
+ hookLogSessionBanner('log_config_files (config upload)');
214
+ }
215
+ /** Append a timestamped line to hook_log.txt or the active compliance session log. */
216
+ function hookRunLog(message) {
217
+ const compliancePath = getActiveComplianceLogPath();
218
+ if (compliancePath) {
219
+ try {
220
+ const ts = new Date().toISOString();
221
+ appendFileSync(compliancePath, `${ts} ${message}\n`, 'utf8');
222
+ }
223
+ catch {
224
+ // best-effort
225
+ }
226
+ return;
227
+ }
228
+ const logPath = getHookLogPath();
229
+ if (!logPath)
230
+ return;
231
+ try {
232
+ ensureHookLogUnderCap();
233
+ const ts = new Date().toISOString();
234
+ appendFileSync(logPath, `${ts} ${message}\n`, 'utf8');
235
+ }
236
+ catch {
237
+ // best-effort; don't break the run
238
+ }
239
+ }
240
+ /** Append a line without timestamp (for multi-line blocks like manifests). */
241
+ function hookLogLine(message) {
242
+ const logPath = getHookLogPath();
243
+ if (!logPath)
244
+ return;
245
+ try {
246
+ ensureHookLogUnderCap();
247
+ appendFileSync(logPath, `${message}\n`, 'utf8');
248
+ }
249
+ catch {
250
+ // best-effort
251
+ }
252
+ }
253
+ /** Read the current hook_log.txt content. Returns empty string if not found or unreadable. */
254
+ function readHookLog() {
255
+ const logPath = getHookLogPath();
256
+ if (!logPath)
257
+ return '';
258
+ try {
259
+ if (!existsSync(logPath))
260
+ return '';
261
+ return readFileSync(logPath, 'utf8');
262
+ }
263
+ catch {
264
+ return '';
265
+ }
266
+ }
267
+ export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookLogAppendSection, hookRunLog, hookLogError, hookLogLine, complianceRunnerDiag, complianceRunnerRunnerLine, appendComplianceRunnerLine, logRemediationApplyFailure, readHookLog, };
@@ -0,0 +1,30 @@
1
+ function normalizeToken(raw) {
2
+ if (!raw?.trim())
3
+ return '';
4
+ const s = raw.trim().toLowerCase().replace(/-/g, '_');
5
+ if (s === 'claude_desktop')
6
+ return 'claude';
7
+ if (s === 'github_copilot')
8
+ return 'copilot';
9
+ if (s === 'cursor' || s === 'claude' || s === 'copilot' || s === 'opencode' || s === 'codex')
10
+ return s;
11
+ // Legacy hooks set OPTIMUS_AGENT=Cursor (display casing)
12
+ if (raw.trim() === 'Cursor')
13
+ return 'cursor';
14
+ return s;
15
+ }
16
+ /** Resolve hook_request.hook_type from hook wrapper env (OPTIMUS_HOOK_TYPE, then OPTIMUS_AGENT). */
17
+ export function resolveHookTypeFromEnv(env = process.env) {
18
+ const fromHook = normalizeToken(env.OPTIMUS_HOOK_TYPE);
19
+ const fromAgent = normalizeToken(env.OPTIMUS_AGENT);
20
+ const token = fromHook || fromAgent || 'claude';
21
+ if (token === 'cursor')
22
+ return 'cursor';
23
+ if (token === 'copilot')
24
+ return 'copilot';
25
+ if (token === 'opencode')
26
+ return 'opencode';
27
+ if (token === 'codex')
28
+ return 'codex';
29
+ return 'claude';
30
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Shared log enrichment helpers: a host/run-context metadata block appended to
3
+ * session banners, and a structured error block. Kept dependency-free of
4
+ * hook_logger / compliance_session_log so both can import it without cycles.
5
+ */
6
+ import os from 'node:os';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { OPTIMUSLABS_MANAGEMENT_REL } from '../../bootstrap_constants.js';
11
+ /** This package's name + version as "<name>: <version>", read from its own package.json. */
12
+ function readPackageVersion() {
13
+ try {
14
+ const pkgPath = fileURLToPath(new URL('../../../package.json', import.meta.url));
15
+ const parsed = JSON.parse(readFileSync(pkgPath, 'utf8'));
16
+ const name = typeof parsed?.name === 'string' ? parsed.name : '';
17
+ const version = typeof parsed?.version === 'string' ? parsed.version : '';
18
+ if (name && version)
19
+ return `${name}: ${version}`;
20
+ return version || name || 'unknown';
21
+ }
22
+ catch {
23
+ return 'unknown';
24
+ }
25
+ }
26
+ /** Deployed bundle version from release/bundle-version.json, or 'unknown'. */
27
+ function readBundleVersion() {
28
+ const home = process.env.HOME || process.env.USERPROFILE;
29
+ if (!home)
30
+ return 'unknown';
31
+ const p = path.join(home, OPTIMUSLABS_MANAGEMENT_REL, 'release', 'bundle-version.json');
32
+ try {
33
+ if (!existsSync(p))
34
+ return 'unknown';
35
+ const parsed = JSON.parse(readFileSync(p, 'utf8'));
36
+ return typeof parsed?.version === 'string' ? parsed.version : 'unknown';
37
+ }
38
+ catch {
39
+ return 'unknown';
40
+ }
41
+ }
42
+ /**
43
+ * Object form of the host/run-context metadata, for JSON telemetry sent to the backend
44
+ * (client-event ingest). Same fields as {@link buildLogMetadataBlock}, minus empty values.
45
+ */
46
+ export function buildLogMetadataObject() {
47
+ const out = {};
48
+ const add = (key, value) => {
49
+ if (value !== undefined && value !== '')
50
+ out[key] = value;
51
+ };
52
+ add('host', os.hostname());
53
+ add('platform', `${process.platform}/${process.arch}`);
54
+ add('os_release', os.release());
55
+ add('node', process.version);
56
+ add('pid', String(process.pid));
57
+ add('hook_type', process.env.OPTIMUS_HOOK_TYPE);
58
+ add('hook_script_version', process.env.OPTIMUS_HOOK_SCRIPT_VERSION);
59
+ add('reporter_version', readPackageVersion());
60
+ add('bundle_version', readBundleVersion());
61
+ add('endpoint', process.env.OPTIMUS_ENDPOINT);
62
+ add('cwd', process.cwd());
63
+ return out;
64
+ }
65
+ /**
66
+ * Indented, multi-line metadata describing the host and run context. Appended to each
67
+ * session banner so a single log file is self-describing during customer support triage.
68
+ * Lines with no value are omitted.
69
+ */
70
+ export function buildLogMetadataBlock() {
71
+ const lines = ['meta:'];
72
+ const add = (key, value) => {
73
+ if (value !== undefined && value !== '')
74
+ lines.push(` ${key}=${value}`);
75
+ };
76
+ add('host', os.hostname());
77
+ add('platform', `${process.platform}/${process.arch}`);
78
+ add('os_release', os.release());
79
+ add('node', process.version);
80
+ add('pid', String(process.pid));
81
+ add('hook_type', process.env.OPTIMUS_HOOK_TYPE);
82
+ add('hook_script_version', process.env.OPTIMUS_HOOK_SCRIPT_VERSION);
83
+ add('reporter_version', readPackageVersion());
84
+ add('bundle_version', readBundleVersion());
85
+ add('endpoint', process.env.OPTIMUS_ENDPOINT);
86
+ add('cwd', process.cwd());
87
+ // Only the banner line + meta: header would be useless on its own; require ≥1 field.
88
+ return lines.length > 1 ? lines.join('\n') + '\n' : '';
89
+ }
90
+ /**
91
+ * Structured, multi-line error block: name/message, a trimmed stack, and any extra
92
+ * context fields. Used by log writers so errors carry enough detail to diagnose
93
+ * without re-running, instead of being swallowed by best-effort catches.
94
+ */
95
+ export function formatErrorBlock(context, err, fields) {
96
+ const ts = new Date().toISOString();
97
+ const lines = ['', '-'.repeat(72), `${ts} ERROR — ${context}`];
98
+ if (err instanceof Error) {
99
+ lines.push(` error: ${err.name}: ${err.message}`);
100
+ if (err.stack) {
101
+ const frames = err.stack
102
+ .split('\n')
103
+ .slice(1, 6)
104
+ .map((l) => ` ${l.trim()}`)
105
+ .filter((l) => l.trim().length > 0);
106
+ if (frames.length)
107
+ lines.push(' stack:', ...frames);
108
+ }
109
+ }
110
+ else {
111
+ lines.push(` error: ${String(err)}`);
112
+ }
113
+ if (fields) {
114
+ for (const [k, v] of Object.entries(fields)) {
115
+ if (v !== undefined && v !== null && String(v) !== '') {
116
+ lines.push(` ${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`);
117
+ }
118
+ }
119
+ }
120
+ lines.push('-'.repeat(72), '');
121
+ return lines.join('\n');
122
+ }