@brianli/kimaki 0.4.72-brianli.1
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/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
package/src/opencode.ts
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
// OpenCode server process manager.
|
|
2
|
+
// Spawns and maintains OpenCode API servers per project directory,
|
|
3
|
+
// handles automatic restarts on failure, and provides typed SDK clients.
|
|
4
|
+
// Uses errore for type-safe error handling.
|
|
5
|
+
|
|
6
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import net from 'node:net'
|
|
9
|
+
import os from 'node:os'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
import {
|
|
15
|
+
createOpencodeClient,
|
|
16
|
+
type OpencodeClient,
|
|
17
|
+
type Config as SdkConfig,
|
|
18
|
+
} from '@opencode-ai/sdk/v2'
|
|
19
|
+
import { getBotToken } from './bot-token.js'
|
|
20
|
+
import {
|
|
21
|
+
getDataDir,
|
|
22
|
+
getLockPort,
|
|
23
|
+
getVerboseOpencodeServer,
|
|
24
|
+
} from './config.js'
|
|
25
|
+
import { getHranaUrl } from './hrana-server.js'
|
|
26
|
+
|
|
27
|
+
// SDK Config type is simplified; opencode accepts nested permission objects with path patterns
|
|
28
|
+
type PermissionAction = 'ask' | 'allow' | 'deny'
|
|
29
|
+
type PermissionRule = PermissionAction | Record<string, PermissionAction>
|
|
30
|
+
type Config = Omit<SdkConfig, 'permission'> & {
|
|
31
|
+
permission?: {
|
|
32
|
+
edit?: PermissionRule
|
|
33
|
+
bash?: PermissionRule
|
|
34
|
+
external_directory?: PermissionRule
|
|
35
|
+
webfetch?: PermissionRule
|
|
36
|
+
[key: string]: PermissionRule | undefined
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
import * as errore from 'errore'
|
|
40
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
41
|
+
import { notifyError } from './sentry.js'
|
|
42
|
+
import {
|
|
43
|
+
DirectoryNotAccessibleError,
|
|
44
|
+
ServerStartError,
|
|
45
|
+
ServerNotReadyError,
|
|
46
|
+
FetchError,
|
|
47
|
+
type OpenCodeErrors,
|
|
48
|
+
} from './errors.js'
|
|
49
|
+
|
|
50
|
+
const opencodeLogger = createLogger(LogPrefix.OPENCODE)
|
|
51
|
+
|
|
52
|
+
const STARTUP_STDERR_TAIL_LIMIT = 30
|
|
53
|
+
const STARTUP_STDERR_LINE_MAX_LENGTH = 120
|
|
54
|
+
const STARTUP_ERROR_REASON_MAX_LENGTH = 1500
|
|
55
|
+
const ANSI_ESCAPE_REGEX =
|
|
56
|
+
/[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g
|
|
57
|
+
|
|
58
|
+
function truncateWithEllipsis({
|
|
59
|
+
value,
|
|
60
|
+
maxLength,
|
|
61
|
+
}: {
|
|
62
|
+
value: string
|
|
63
|
+
maxLength: number
|
|
64
|
+
}): string {
|
|
65
|
+
if (maxLength <= 3) {
|
|
66
|
+
return value.slice(0, maxLength)
|
|
67
|
+
}
|
|
68
|
+
if (value.length <= maxLength) {
|
|
69
|
+
return value
|
|
70
|
+
}
|
|
71
|
+
return `${value.slice(0, maxLength - 3)}...`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stripAnsiCodes(value: string): string {
|
|
75
|
+
return value.replaceAll(ANSI_ESCAPE_REGEX, '')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function splitOutputChunkLines(chunk: string): string[] {
|
|
79
|
+
return chunk
|
|
80
|
+
.split(/\r?\n/g)
|
|
81
|
+
.map((line) => stripAnsiCodes(line).trim())
|
|
82
|
+
.filter((line) => line.length > 0)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sanitizeForCodeFence(line: string): string {
|
|
86
|
+
return line.replaceAll('```', '`\u200b``')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pushStartupStderrTail({
|
|
90
|
+
stderrTail,
|
|
91
|
+
chunk,
|
|
92
|
+
}: {
|
|
93
|
+
stderrTail: string[]
|
|
94
|
+
chunk: string
|
|
95
|
+
}): void {
|
|
96
|
+
const incomingLines = splitOutputChunkLines(chunk)
|
|
97
|
+
const truncatedLines = incomingLines.map((line) => {
|
|
98
|
+
const sanitizedLine = sanitizeForCodeFence(line)
|
|
99
|
+
return truncateWithEllipsis({
|
|
100
|
+
value: sanitizedLine,
|
|
101
|
+
maxLength: STARTUP_STDERR_LINE_MAX_LENGTH,
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
stderrTail.push(...truncatedLines)
|
|
105
|
+
if (stderrTail.length > STARTUP_STDERR_TAIL_LIMIT) {
|
|
106
|
+
stderrTail.splice(0, stderrTail.length - STARTUP_STDERR_TAIL_LIMIT)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildStartupTimeoutReason({
|
|
111
|
+
maxAttempts,
|
|
112
|
+
stderrTail,
|
|
113
|
+
}: {
|
|
114
|
+
maxAttempts: number
|
|
115
|
+
stderrTail: string[]
|
|
116
|
+
}): string {
|
|
117
|
+
const baseReason = `Server did not start after ${maxAttempts} seconds`
|
|
118
|
+
if (stderrTail.length === 0) {
|
|
119
|
+
return baseReason
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const formatReason = ({
|
|
123
|
+
lines,
|
|
124
|
+
omitted,
|
|
125
|
+
}: {
|
|
126
|
+
lines: string[]
|
|
127
|
+
omitted: number
|
|
128
|
+
}): string => {
|
|
129
|
+
const omittedLine =
|
|
130
|
+
omitted > 0
|
|
131
|
+
? `[... ${omitted} older stderr lines omitted to fit Discord ...]\n`
|
|
132
|
+
: ''
|
|
133
|
+
const stderrCodeBlock = `${omittedLine}${lines.join('\n')}`
|
|
134
|
+
return `${baseReason}\nLast opencode stderr lines:\n\`\`\`text\n${stderrCodeBlock}\n\`\`\``
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let lines = [...stderrTail]
|
|
138
|
+
let omitted = 0
|
|
139
|
+
let formattedReason = formatReason({ lines, omitted })
|
|
140
|
+
|
|
141
|
+
while (
|
|
142
|
+
formattedReason.length > STARTUP_ERROR_REASON_MAX_LENGTH &&
|
|
143
|
+
lines.length > 0
|
|
144
|
+
) {
|
|
145
|
+
lines = lines.slice(1)
|
|
146
|
+
omitted += 1
|
|
147
|
+
formattedReason = formatReason({ lines, omitted })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return truncateWithEllipsis({
|
|
151
|
+
value: formattedReason,
|
|
152
|
+
maxLength: STARTUP_ERROR_REASON_MAX_LENGTH,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
type ServerInitOptions = { originalRepoDirectory?: string; channelId?: string }
|
|
157
|
+
|
|
158
|
+
const opencodeServers = new Map<
|
|
159
|
+
string,
|
|
160
|
+
{
|
|
161
|
+
process: ChildProcess
|
|
162
|
+
client: OpencodeClient
|
|
163
|
+
port: number
|
|
164
|
+
/** Original options used to spawn this server, reused on auto-restart */
|
|
165
|
+
initOptions?: ServerInitOptions
|
|
166
|
+
}
|
|
167
|
+
>()
|
|
168
|
+
|
|
169
|
+
const serverRetryCount = new Map<string, number>()
|
|
170
|
+
|
|
171
|
+
async function getOpenPort(): Promise<number> {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const server = net.createServer()
|
|
174
|
+
server.listen(0, () => {
|
|
175
|
+
const address = server.address()
|
|
176
|
+
if (address && typeof address === 'object') {
|
|
177
|
+
const port = address.port
|
|
178
|
+
server.close(() => {
|
|
179
|
+
resolve(port)
|
|
180
|
+
})
|
|
181
|
+
} else {
|
|
182
|
+
reject(new Error('Failed to get port'))
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
server.on('error', reject)
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function waitForServer({
|
|
190
|
+
port,
|
|
191
|
+
maxAttempts = 30,
|
|
192
|
+
startupStderrTail,
|
|
193
|
+
}: {
|
|
194
|
+
port: number
|
|
195
|
+
maxAttempts?: number
|
|
196
|
+
startupStderrTail: string[]
|
|
197
|
+
}): Promise<ServerStartError | true> {
|
|
198
|
+
const endpoint = `http://127.0.0.1:${port}/api/health`
|
|
199
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
200
|
+
const response = await errore.tryAsync({
|
|
201
|
+
try: () => fetch(endpoint),
|
|
202
|
+
catch: (e) => new FetchError({ url: endpoint, cause: e }),
|
|
203
|
+
})
|
|
204
|
+
if (response instanceof Error) {
|
|
205
|
+
// Connection refused or other transient errors - continue polling
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if (response.status < 500) {
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
const body = await response.text()
|
|
213
|
+
// Fatal errors that won't resolve with retrying
|
|
214
|
+
if (body.includes('BunInstallFailedError')) {
|
|
215
|
+
return new ServerStartError({ port, reason: body.slice(0, 200) })
|
|
216
|
+
}
|
|
217
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
218
|
+
}
|
|
219
|
+
return new ServerStartError({
|
|
220
|
+
port,
|
|
221
|
+
reason: buildStartupTimeoutReason({
|
|
222
|
+
maxAttempts,
|
|
223
|
+
stderrTail: startupStderrTail,
|
|
224
|
+
}),
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Initialize OpenCode server for a directory.
|
|
230
|
+
* @param directory - The directory to run the server in (cwd)
|
|
231
|
+
* @param options.originalRepoDirectory - For worktrees: the original repo directory to allow access to
|
|
232
|
+
*/
|
|
233
|
+
export async function initializeOpencodeForDirectory(
|
|
234
|
+
directory: string,
|
|
235
|
+
options?: { originalRepoDirectory?: string; channelId?: string },
|
|
236
|
+
): Promise<OpenCodeErrors | (() => OpencodeClient)> {
|
|
237
|
+
const existing = opencodeServers.get(directory)
|
|
238
|
+
if (existing && !existing.process.killed) {
|
|
239
|
+
opencodeLogger.log(
|
|
240
|
+
`Reusing existing server on port ${existing.port} for directory: ${directory}`,
|
|
241
|
+
)
|
|
242
|
+
return () => {
|
|
243
|
+
const entry = opencodeServers.get(directory)
|
|
244
|
+
if (!entry?.client) {
|
|
245
|
+
throw new ServerNotReadyError({ directory })
|
|
246
|
+
}
|
|
247
|
+
return entry.client
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Verify directory exists and is accessible before spawning
|
|
252
|
+
const accessCheck = errore.tryFn({
|
|
253
|
+
try: () => {
|
|
254
|
+
fs.accessSync(directory, fs.constants.R_OK | fs.constants.X_OK)
|
|
255
|
+
},
|
|
256
|
+
catch: () => new DirectoryNotAccessibleError({ directory }),
|
|
257
|
+
})
|
|
258
|
+
if (accessCheck instanceof Error) {
|
|
259
|
+
return accessCheck
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const port = await getOpenPort()
|
|
263
|
+
|
|
264
|
+
const opencodeCommand = process.env.OPENCODE_PATH || 'opencode'
|
|
265
|
+
|
|
266
|
+
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
|
|
267
|
+
const tmpdir = os.tmpdir().replaceAll('\\', '/')
|
|
268
|
+
const originalRepo = options?.originalRepoDirectory?.replaceAll('\\', '/')
|
|
269
|
+
const normalizedDirectory = directory.replaceAll('\\', '/')
|
|
270
|
+
|
|
271
|
+
// Build external_directory permissions, optionally including original repo for worktrees.
|
|
272
|
+
const externalDirectoryPermissions: Record<string, PermissionAction> = {
|
|
273
|
+
'*': 'ask',
|
|
274
|
+
'/tmp': 'allow',
|
|
275
|
+
'/tmp/*': 'allow',
|
|
276
|
+
'/private/tmp': 'allow',
|
|
277
|
+
'/private/tmp/*': 'allow',
|
|
278
|
+
[tmpdir]: 'allow',
|
|
279
|
+
[`${tmpdir}/*`]: 'allow',
|
|
280
|
+
[normalizedDirectory]: 'allow',
|
|
281
|
+
[`${normalizedDirectory}/*`]: 'allow',
|
|
282
|
+
}
|
|
283
|
+
// Allow ~/.config/opencode so the agent doesn't get permission prompts when
|
|
284
|
+
// it tries to read the global AGENTS.md or opencode config (the path is
|
|
285
|
+
// visible in the system prompt, so models sometimes try to read it).
|
|
286
|
+
const opencodeConfigDir = path
|
|
287
|
+
.join(os.homedir(), '.config', 'opencode')
|
|
288
|
+
.replaceAll('\\', '/')
|
|
289
|
+
externalDirectoryPermissions[opencodeConfigDir] = 'allow'
|
|
290
|
+
externalDirectoryPermissions[`${opencodeConfigDir}/*`] = 'allow'
|
|
291
|
+
|
|
292
|
+
if (originalRepo) {
|
|
293
|
+
externalDirectoryPermissions[originalRepo] = 'allow'
|
|
294
|
+
externalDirectoryPermissions[`${originalRepo}/*`] = 'allow'
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Get bot token for plugin to use Discord API (env first, DB fallback)
|
|
298
|
+
const kimakiBotToken = getBotToken()?.token
|
|
299
|
+
|
|
300
|
+
const serveArgs = ['serve', '--port', port.toString()]
|
|
301
|
+
if (getVerboseOpencodeServer()) {
|
|
302
|
+
serveArgs.push('--print-logs', '--log-level', 'DEBUG')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const serverProcess = spawn(
|
|
306
|
+
opencodeCommand,
|
|
307
|
+
serveArgs,
|
|
308
|
+
{
|
|
309
|
+
stdio: 'pipe',
|
|
310
|
+
detached: false,
|
|
311
|
+
cwd: directory,
|
|
312
|
+
shell: true, // Required for .cmd files on Windows
|
|
313
|
+
env: {
|
|
314
|
+
...process.env,
|
|
315
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
316
|
+
$schema: 'https://opencode.ai/config.json',
|
|
317
|
+
lsp: false,
|
|
318
|
+
formatter: false,
|
|
319
|
+
plugin: [new URL('../src/opencode-plugin.ts', import.meta.url).href],
|
|
320
|
+
permission: {
|
|
321
|
+
edit: 'allow',
|
|
322
|
+
bash: 'allow',
|
|
323
|
+
external_directory: externalDirectoryPermissions,
|
|
324
|
+
webfetch: 'allow',
|
|
325
|
+
},
|
|
326
|
+
agent: {
|
|
327
|
+
explore: {
|
|
328
|
+
permission: {
|
|
329
|
+
'*': 'deny',
|
|
330
|
+
grep: 'allow',
|
|
331
|
+
glob: 'allow',
|
|
332
|
+
list: 'allow',
|
|
333
|
+
read: {
|
|
334
|
+
'*': 'allow',
|
|
335
|
+
'*.env': 'deny',
|
|
336
|
+
'*.env.*': 'deny',
|
|
337
|
+
'*.env.example': 'allow',
|
|
338
|
+
},
|
|
339
|
+
webfetch: 'allow',
|
|
340
|
+
websearch: 'allow',
|
|
341
|
+
codesearch: 'allow',
|
|
342
|
+
external_directory: externalDirectoryPermissions,
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
skills: {
|
|
347
|
+
paths: [path.resolve(__dirname, '..', 'skills')],
|
|
348
|
+
},
|
|
349
|
+
} satisfies Config),
|
|
350
|
+
OPENCODE_PORT: port.toString(),
|
|
351
|
+
KIMAKI_DATA_DIR: getDataDir(),
|
|
352
|
+
KIMAKI_LOCK_PORT: getLockPort().toString(),
|
|
353
|
+
...(kimakiBotToken && { KIMAKI_BOT_TOKEN: kimakiBotToken }),
|
|
354
|
+
|
|
355
|
+
...(getHranaUrl() && { KIMAKI_DB_URL: getHranaUrl()! }),
|
|
356
|
+
...(process.env.KIMAKI_SENTRY_DSN && {
|
|
357
|
+
KIMAKI_SENTRY_DSN: process.env.KIMAKI_SENTRY_DSN,
|
|
358
|
+
}),
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
// Buffer logs until we know if server started successfully.
|
|
364
|
+
// Once ready, switch to forwarding if --verbose-opencode-server is set.
|
|
365
|
+
const logBuffer: string[] = []
|
|
366
|
+
const startupStderrTail: string[] = []
|
|
367
|
+
let serverReady = false
|
|
368
|
+
const shortDir = path.basename(directory)
|
|
369
|
+
|
|
370
|
+
logBuffer.push(
|
|
371
|
+
`Spawned opencode serve --port ${port} in ${directory} (pid: ${serverProcess.pid})`,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
serverProcess.stdout?.on('data', (data) => {
|
|
375
|
+
try {
|
|
376
|
+
const chunk = data.toString()
|
|
377
|
+
const lines = splitOutputChunkLines(chunk)
|
|
378
|
+
if (!serverReady) {
|
|
379
|
+
logBuffer.push(...lines.map((line) => `[stdout] ${line}`))
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
if (getVerboseOpencodeServer()) {
|
|
383
|
+
for (const line of lines) {
|
|
384
|
+
opencodeLogger.log(`[${shortDir}:${port}] ${line}`)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
logBuffer.push(`Failed to process stdout startup logs: ${error}`)
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
serverProcess.stderr?.on('data', (data) => {
|
|
393
|
+
try {
|
|
394
|
+
const chunk = data.toString()
|
|
395
|
+
const lines = splitOutputChunkLines(chunk)
|
|
396
|
+
if (!serverReady) {
|
|
397
|
+
logBuffer.push(...lines.map((line) => `[stderr] ${line}`))
|
|
398
|
+
pushStartupStderrTail({ stderrTail: startupStderrTail, chunk })
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
if (getVerboseOpencodeServer()) {
|
|
402
|
+
for (const line of lines) {
|
|
403
|
+
opencodeLogger.error(`[${shortDir}:${port}] ${line}`)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch (error) {
|
|
407
|
+
logBuffer.push(`Failed to process stderr startup logs: ${error}`)
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
serverProcess.on('error', (error) => {
|
|
412
|
+
logBuffer.push(`Failed to start server on port ${port}: ${error}`)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
serverProcess.on('exit', (code) => {
|
|
416
|
+
opencodeLogger.log(
|
|
417
|
+
`Opencode server on ${directory} exited with code:`,
|
|
418
|
+
code,
|
|
419
|
+
)
|
|
420
|
+
// Capture init options before deleting the entry so auto-restart preserves
|
|
421
|
+
// worktree repo access.
|
|
422
|
+
const storedInitOptions = opencodeServers.get(directory)?.initOptions
|
|
423
|
+
opencodeServers.delete(directory)
|
|
424
|
+
if (code !== 0) {
|
|
425
|
+
const retryCount = serverRetryCount.get(directory) || 0
|
|
426
|
+
if (retryCount < 5) {
|
|
427
|
+
serverRetryCount.set(directory, retryCount + 1)
|
|
428
|
+
opencodeLogger.log(
|
|
429
|
+
`Restarting server for directory: ${directory} (attempt ${retryCount + 1}/5)`,
|
|
430
|
+
)
|
|
431
|
+
initializeOpencodeForDirectory(directory, storedInitOptions).then(
|
|
432
|
+
(result) => {
|
|
433
|
+
if (result instanceof Error) {
|
|
434
|
+
opencodeLogger.error(`Failed to restart opencode server:`, result)
|
|
435
|
+
void notifyError(result, `OpenCode server restart failed for ${directory}`)
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
)
|
|
439
|
+
} else {
|
|
440
|
+
const crashError = new Error(
|
|
441
|
+
`Server for ${directory} crashed too many times (5), not restarting`,
|
|
442
|
+
)
|
|
443
|
+
opencodeLogger.error(crashError.message)
|
|
444
|
+
void notifyError(crashError, `OpenCode server crash loop exhausted`)
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
serverRetryCount.delete(directory)
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
const waitResult = await waitForServer({
|
|
452
|
+
port,
|
|
453
|
+
startupStderrTail,
|
|
454
|
+
})
|
|
455
|
+
if (waitResult instanceof Error) {
|
|
456
|
+
// Dump buffered logs on failure
|
|
457
|
+
opencodeLogger.error(`Server failed to start for ${directory}:`)
|
|
458
|
+
for (const line of logBuffer) {
|
|
459
|
+
opencodeLogger.error(` ${line}`)
|
|
460
|
+
}
|
|
461
|
+
return waitResult
|
|
462
|
+
}
|
|
463
|
+
serverReady = true
|
|
464
|
+
opencodeLogger.log(`Server ready on port ${port}`)
|
|
465
|
+
|
|
466
|
+
// When verbose mode is enabled, also dump startup logs so plugin loading
|
|
467
|
+
// errors and other startup output are visible in kimaki.log.
|
|
468
|
+
if (getVerboseOpencodeServer()) {
|
|
469
|
+
for (const line of logBuffer) {
|
|
470
|
+
opencodeLogger.log(`[${shortDir}:${port}:startup] ${line}`)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const baseUrl = `http://127.0.0.1:${port}`
|
|
475
|
+
const fetchWithTimeout = (request: Request) =>
|
|
476
|
+
fetch(request, {
|
|
477
|
+
// @ts-ignore
|
|
478
|
+
timeout: false,
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
const client = createOpencodeClient({
|
|
482
|
+
baseUrl,
|
|
483
|
+
fetch: fetchWithTimeout as typeof fetch,
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
opencodeServers.set(directory, {
|
|
487
|
+
process: serverProcess,
|
|
488
|
+
client,
|
|
489
|
+
port,
|
|
490
|
+
initOptions: options,
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
return () => {
|
|
494
|
+
const entry = opencodeServers.get(directory)
|
|
495
|
+
if (!entry?.client) {
|
|
496
|
+
throw new ServerNotReadyError({ directory })
|
|
497
|
+
}
|
|
498
|
+
return entry.client
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function getOpencodeServers() {
|
|
503
|
+
return opencodeServers
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function getOpencodeServerPort(directory: string): number | null {
|
|
507
|
+
const entry = opencodeServers.get(directory)
|
|
508
|
+
return entry?.port ?? null
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function getOpencodeClient(directory: string): OpencodeClient | null {
|
|
512
|
+
const entry = opencodeServers.get(directory)
|
|
513
|
+
return entry?.client ?? null
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Restart the opencode server for a directory.
|
|
518
|
+
* Kills the existing process and reinitializes a new one.
|
|
519
|
+
* Used for resolving opencode state issues, refreshing auth, plugins, etc.
|
|
520
|
+
*/
|
|
521
|
+
export async function restartOpencodeServer(
|
|
522
|
+
directory: string,
|
|
523
|
+
): Promise<OpenCodeErrors | true> {
|
|
524
|
+
const existing = opencodeServers.get(directory)
|
|
525
|
+
// Preserve init options (originalRepoDirectory) so the restarted
|
|
526
|
+
// server retains worktree access.
|
|
527
|
+
const storedInitOptions = existing?.initOptions
|
|
528
|
+
|
|
529
|
+
if (existing) {
|
|
530
|
+
opencodeLogger.log(
|
|
531
|
+
`Killing existing server for directory: ${directory} (pid: ${existing.process.pid})`,
|
|
532
|
+
)
|
|
533
|
+
// Reset retry count so the exit handler doesn't auto-restart
|
|
534
|
+
serverRetryCount.set(directory, 999)
|
|
535
|
+
existing.process.kill('SIGTERM')
|
|
536
|
+
opencodeServers.delete(directory)
|
|
537
|
+
// Give the process time to fully terminate
|
|
538
|
+
await new Promise((resolve) => {
|
|
539
|
+
setTimeout(resolve, 1000)
|
|
540
|
+
})
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Reset retry count for the fresh start
|
|
544
|
+
serverRetryCount.delete(directory)
|
|
545
|
+
|
|
546
|
+
const result = await initializeOpencodeForDirectory(
|
|
547
|
+
directory,
|
|
548
|
+
storedInitOptions,
|
|
549
|
+
)
|
|
550
|
+
if (result instanceof Error) {
|
|
551
|
+
return result
|
|
552
|
+
}
|
|
553
|
+
return true
|
|
554
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Sensitive data redaction helpers for logs and telemetry payloads.
|
|
2
|
+
// Redacts common secrets, identifiers, emails, and can optionally redact paths.
|
|
3
|
+
|
|
4
|
+
const CORE_SENSITIVE_REPLACEMENTS: Array<{
|
|
5
|
+
pattern: RegExp
|
|
6
|
+
replacement: string
|
|
7
|
+
}> = [
|
|
8
|
+
{
|
|
9
|
+
pattern: /\bBearer\s+[A-Za-z0-9._-]{10,}\b/gi,
|
|
10
|
+
replacement: 'Bearer [REDACTED]',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
pattern: /\bsk-[A-Za-z0-9]{16,}\b/g,
|
|
14
|
+
replacement: '[REDACTED_OPENAI_KEY]',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
pattern: /\bAIza[0-9A-Za-z_-]{20,}\b/g,
|
|
18
|
+
replacement: '[REDACTED_GOOGLE_KEY]',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
22
|
+
replacement: '[REDACTED_GITHUB_TOKEN]',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern:
|
|
26
|
+
/([?&](?:token|api[_-]?key|key|secret|password|authorization)=)[^&\s]+/gi,
|
|
27
|
+
replacement: '$1[REDACTED]',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
pattern:
|
|
31
|
+
/(\b(?:token|api[_-]?key|secret|password|authorization)\b\s*[:=]\s*")([^"]+)(")/gi,
|
|
32
|
+
replacement: '$1[REDACTED]$3',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
pattern:
|
|
36
|
+
/(\b(?:token|api[_-]?key|secret|password|authorization)\b\s*[:=]\s*)([^\s,;]+)/gi,
|
|
37
|
+
replacement: '$1[REDACTED]',
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const PATH_REPLACEMENTS: Array<{
|
|
42
|
+
pattern: RegExp
|
|
43
|
+
replacement: string
|
|
44
|
+
}> = [
|
|
45
|
+
{
|
|
46
|
+
pattern: /\/(?:Users|home)\/[^/\s]+\/[^\s'"`)]*/g,
|
|
47
|
+
replacement: '[REDACTED_PATH]',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
pattern: /[A-Za-z]:\\[^\s'"`)]*/g,
|
|
51
|
+
replacement: '[REDACTED_PATH]',
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
export function sanitizeSensitiveText(
|
|
56
|
+
value: string,
|
|
57
|
+
{ redactPaths = false }: { redactPaths?: boolean } = {},
|
|
58
|
+
): string {
|
|
59
|
+
const replacements = redactPaths
|
|
60
|
+
? [...CORE_SENSITIVE_REPLACEMENTS, ...PATH_REPLACEMENTS]
|
|
61
|
+
: CORE_SENSITIVE_REPLACEMENTS
|
|
62
|
+
return replacements.reduce((current, entry) => {
|
|
63
|
+
return current.replace(entry.pattern, entry.replacement)
|
|
64
|
+
}, value)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function sanitizeUnknownValue(
|
|
68
|
+
value: unknown,
|
|
69
|
+
{
|
|
70
|
+
depth = 0,
|
|
71
|
+
seen = new WeakSet<object>(),
|
|
72
|
+
redactPaths = false,
|
|
73
|
+
}: {
|
|
74
|
+
depth?: number
|
|
75
|
+
seen?: WeakSet<object>
|
|
76
|
+
redactPaths?: boolean
|
|
77
|
+
} = {},
|
|
78
|
+
): unknown {
|
|
79
|
+
if (depth > 8) {
|
|
80
|
+
return '[REDACTED_DEPTH_LIMIT]'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof value === 'string') {
|
|
84
|
+
return sanitizeSensitiveText(value, { redactPaths })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
typeof value === 'number' ||
|
|
89
|
+
typeof value === 'boolean' ||
|
|
90
|
+
value === null ||
|
|
91
|
+
value === undefined
|
|
92
|
+
) {
|
|
93
|
+
return value
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (value instanceof Date) {
|
|
97
|
+
return value.toISOString()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (value instanceof Error) {
|
|
101
|
+
const sanitizedStack = value.stack
|
|
102
|
+
? sanitizeSensitiveText(value.stack, { redactPaths })
|
|
103
|
+
: undefined
|
|
104
|
+
return {
|
|
105
|
+
name: value.name,
|
|
106
|
+
message: sanitizeSensitiveText(value.message, { redactPaths }),
|
|
107
|
+
stack: sanitizedStack,
|
|
108
|
+
cause: sanitizeUnknownValue(value.cause, {
|
|
109
|
+
depth: depth + 1,
|
|
110
|
+
seen,
|
|
111
|
+
redactPaths,
|
|
112
|
+
}),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
return value.map((item) => {
|
|
118
|
+
return sanitizeUnknownValue(item, { depth: depth + 1, seen, redactPaths })
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof value === 'object') {
|
|
123
|
+
if (seen.has(value)) {
|
|
124
|
+
return '[REDACTED_CIRCULAR]'
|
|
125
|
+
}
|
|
126
|
+
seen.add(value)
|
|
127
|
+
|
|
128
|
+
const sanitizedEntries = Object.entries(value).map(([key, entryValue]) => {
|
|
129
|
+
return [
|
|
130
|
+
key,
|
|
131
|
+
sanitizeUnknownValue(entryValue, {
|
|
132
|
+
depth: depth + 1,
|
|
133
|
+
seen,
|
|
134
|
+
redactPaths,
|
|
135
|
+
}),
|
|
136
|
+
]
|
|
137
|
+
})
|
|
138
|
+
return Object.fromEntries(sanitizedEntries)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return sanitizeSensitiveText(String(value), { redactPaths })
|
|
142
|
+
}
|