@otto-assistant/bridge 0.4.92
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/agent-model.e2e.test.js +755 -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/anthropic-auth-plugin.js +728 -0
- package/dist/anthropic-auth-plugin.test.js +125 -0
- package/dist/anthropic-auth-state.js +231 -0
- package/dist/bin.js +90 -0
- package/dist/channel-management.js +227 -0
- package/dist/cli-parsing.test.js +137 -0
- package/dist/cli-send-thread.e2e.test.js +356 -0
- package/dist/cli.js +3276 -0
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +335 -0
- package/dist/commands/ask-question.js +274 -0
- package/dist/commands/btw.js +116 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +130 -0
- package/dist/commands/diff.js +63 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +220 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +885 -0
- package/dist/commands/mcp.js +239 -0
- package/dist/commands/memory-snapshot.js +24 -0
- package/dist/commands/mention-mode.js +44 -0
- package/dist/commands/merge-worktree.js +159 -0
- package/dist/commands/model-variant.js +364 -0
- package/dist/commands/model.js +776 -0
- package/dist/commands/new-worktree.js +366 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +206 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +127 -0
- package/dist/commands/resume.js +149 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/screenshare.js +303 -0
- package/dist/commands/screenshare.test.js +20 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +176 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/tasks.js +205 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +138 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +410 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +94 -0
- package/dist/context-awareness-plugin.js +363 -0
- package/dist/context-awareness-plugin.test.js +124 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1310 -0
- package/dist/db.js +251 -0
- package/dist/db.test.js +138 -0
- package/dist/debounce-timeout.js +28 -0
- package/dist/debounced-process-flush.js +77 -0
- package/dist/discord-bot.js +1008 -0
- package/dist/discord-command-registration.js +524 -0
- package/dist/discord-urls.js +81 -0
- package/dist/discord-utils.js +591 -0
- package/dist/discord-utils.test.js +134 -0
- package/dist/errors.js +157 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/event-stream-real-capture.e2e.test.js +533 -0
- package/dist/eventsource-parser.test.js +327 -0
- package/dist/exec-async.js +26 -0
- package/dist/external-opencode-sync.js +480 -0
- package/dist/format-tables.js +302 -0
- package/dist/format-tables.test.js +308 -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 +113 -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-proxy-reconnect.e2e.test.js +394 -0
- package/dist/gateway-proxy.e2e.test.js +483 -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 +37 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +52 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +253 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +223 -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_events.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 +122 -0
- package/dist/hrana-server.js +263 -0
- package/dist/hrana-server.test.js +370 -0
- package/dist/html-actions.js +123 -0
- package/dist/html-actions.test.js +70 -0
- package/dist/html-components.js +117 -0
- package/dist/html-components.test.js +34 -0
- package/dist/image-optimizer-plugin.js +153 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +397 -0
- package/dist/ipc-polling.js +252 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/kimaki-digital-twin.e2e.test.js +161 -0
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
- package/dist/kimaki-opencode-plugin.js +17 -0
- package/dist/kimaki-opencode-plugin.test.js +98 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +165 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +257 -0
- package/dist/message-finish-field.e2e.test.js +165 -0
- package/dist/message-formatting.js +413 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/message-preprocessing.js +330 -0
- package/dist/onboarding-tutorial.js +172 -0
- package/dist/onboarding-welcome.js +37 -0
- package/dist/openai-realtime.js +224 -0
- package/dist/opencode-command-detection.js +65 -0
- package/dist/opencode-command-detection.test.js +240 -0
- package/dist/opencode-command.js +129 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +361 -0
- package/dist/opencode-interrupt-plugin.test.js +458 -0
- package/dist/opencode.js +861 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +59 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/queue-advanced-abort.e2e.test.js +293 -0
- package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
- package/dist/queue-advanced-e2e-setup.js +786 -0
- package/dist/queue-advanced-footer.e2e.test.js +472 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
- package/dist/queue-advanced-question.e2e.test.js +261 -0
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
- package/dist/queue-advanced-typing.e2e.test.js +153 -0
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
- package/dist/queue-interrupt-drain.e2e.test.js +135 -0
- package/dist/queue-question-select-drain.e2e.test.js +120 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +508 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +420 -0
- package/dist/session-handler/event-stream-state.test.js +563 -0
- package/dist/session-handler/model-utils.js +124 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +104 -0
- package/dist/session-handler/thread-session-runtime.js +3258 -0
- package/dist/session-handler.js +9 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/session-title-rename.test.js +80 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +17 -0
- package/dist/system-message.js +613 -0
- package/dist/system-message.test.js +602 -0
- package/dist/task-runner.js +295 -0
- package/dist/task-schedule.js +209 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +299 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +999 -0
- package/dist/tools.js +357 -0
- package/dist/undo-redo.e2e.test.js +161 -0
- package/dist/unnest-code-blocks.js +146 -0
- package/dist/unnest-code-blocks.test.js +673 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +144 -0
- package/dist/voice-attachment.js +34 -0
- package/dist/voice-handler.js +646 -0
- package/dist/voice-message.e2e.test.js +1021 -0
- package/dist/voice.js +447 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +94 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +308 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +929 -0
- package/dist/worktrees.test.js +189 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +98 -0
- package/schema.prisma +295 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +112 -0
- package/skills/egaki/SKILL.md +100 -0
- package/skills/errore/SKILL.md +647 -0
- package/skills/event-sourcing-state/SKILL.md +252 -0
- package/skills/gitchamber/SKILL.md +93 -0
- package/skills/goke/SKILL.md +644 -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/lintcn/SKILL.md +873 -0
- package/skills/new-skill/SKILL.md +211 -0
- package/skills/npm-package/SKILL.md +239 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +14 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +112 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +976 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +283 -0
- package/src/ai-tool.ts +39 -0
- package/src/anthropic-auth-plugin.test.ts +159 -0
- package/src/anthropic-auth-plugin.ts +861 -0
- package/src/anthropic-auth-state.ts +282 -0
- package/src/bin.ts +111 -0
- package/src/channel-management.ts +334 -0
- package/src/cli-parsing.test.ts +195 -0
- package/src/cli-send-thread.e2e.test.ts +464 -0
- package/src/cli.ts +4581 -0
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +473 -0
- package/src/commands/ask-question.ts +390 -0
- package/src/commands/btw.ts +164 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +190 -0
- package/src/commands/diff.ts +91 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +321 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1173 -0
- package/src/commands/mcp.ts +307 -0
- package/src/commands/memory-snapshot.ts +30 -0
- package/src/commands/mention-mode.ts +68 -0
- package/src/commands/merge-worktree.ts +223 -0
- package/src/commands/model-variant.ts +483 -0
- package/src/commands/model.ts +1053 -0
- package/src/commands/new-worktree.ts +510 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +271 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/screenshare.test.ts +30 -0
- package/src/commands/screenshare.ts +366 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +227 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/tasks.ts +293 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +173 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +552 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +111 -0
- package/src/context-awareness-plugin.test.ts +142 -0
- package/src/context-awareness-plugin.ts +510 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1876 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +286 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1330 -0
- package/src/discord-command-registration.ts +693 -0
- package/src/discord-urls.ts +88 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +800 -0
- package/src/errors.ts +201 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/event-stream-real-capture.e2e.test.ts +692 -0
- package/src/eventsource-parser.test.ts +351 -0
- package/src/exec-async.ts +35 -0
- package/src/external-opencode-sync.ts +685 -0
- package/src/format-tables.test.ts +335 -0
- package/src/format-tables.ts +445 -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 +172 -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/gateway-proxy-reconnect.e2e.test.ts +523 -0
- package/src/gateway-proxy.e2e.test.ts +640 -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 +114 -0
- package/src/generated/client.ts +138 -0
- package/src/generated/commonInputTypes.ts +736 -0
- package/src/generated/enums.ts +88 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2386 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1656 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +1859 -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 +1228 -0
- package/src/generated/models/channel_worktrees.ts +1300 -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_events.ts +1439 -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 +1781 -0
- package/src/generated/models/thread_worktrees.ts +1356 -0
- package/src/generated/models.ts +30 -0
- package/src/heap-monitor.ts +152 -0
- package/src/hrana-server.test.ts +434 -0
- package/src/hrana-server.ts +314 -0
- package/src/html-actions.test.ts +87 -0
- package/src/html-actions.ts +174 -0
- package/src/html-components.test.ts +38 -0
- package/src/html-components.ts +181 -0
- package/src/image-optimizer-plugin.ts +194 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +576 -0
- package/src/ipc-polling.ts +326 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/kimaki-digital-twin.e2e.test.ts +199 -0
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
- package/src/kimaki-opencode-plugin.test.ts +108 -0
- package/src/kimaki-opencode-plugin.ts +18 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +208 -0
- package/src/markdown.test.ts +308 -0
- package/src/markdown.ts +410 -0
- package/src/message-finish-field.e2e.test.ts +192 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +533 -0
- package/src/message-preprocessing.ts +455 -0
- package/src/onboarding-tutorial.ts +176 -0
- package/src/onboarding-welcome.ts +49 -0
- package/src/openai-realtime.ts +358 -0
- package/src/opencode-command-detection.test.ts +307 -0
- package/src/opencode-command-detection.ts +76 -0
- package/src/opencode-command.test.ts +70 -0
- package/src/opencode-command.ts +188 -0
- package/src/opencode-interrupt-plugin.test.ts +677 -0
- package/src/opencode-interrupt-plugin.ts +477 -0
- package/src/opencode.ts +1110 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.ts +22 -0
- package/src/parse-permission-rules.test.ts +127 -0
- package/src/patch-text-parser.ts +107 -0
- package/src/plugin-logger.ts +68 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/queue-advanced-abort.e2e.test.ts +382 -0
- package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
- package/src/queue-advanced-e2e-setup.ts +873 -0
- package/src/queue-advanced-footer.e2e.test.ts +576 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
- package/src/queue-advanced-question.e2e.test.ts +316 -0
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
- package/src/queue-advanced-typing.e2e.test.ts +199 -0
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
- package/src/queue-interrupt-drain.e2e.test.ts +166 -0
- package/src/queue-question-select-drain.e2e.test.ts +152 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +641 -0
- package/src/schema.sql +173 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +97 -0
- package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
- package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
- package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
- package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
- package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
- package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
- package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
- package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
- package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
- package/src/session-handler/event-stream-state.test.ts +645 -0
- package/src/session-handler/event-stream-state.ts +608 -0
- package/src/session-handler/model-utils.ts +183 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +212 -0
- package/src/session-handler/thread-session-runtime.ts +4281 -0
- package/src/session-handler.ts +15 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/session-title-rename.test.ts +112 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +122 -0
- package/src/system-message.test.ts +612 -0
- package/src/system-message.ts +723 -0
- package/src/task-runner.ts +421 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +311 -0
- package/src/test-utils.ts +435 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1219 -0
- package/src/tools.ts +430 -0
- package/src/undici.d.ts +12 -0
- package/src/undo-redo.e2e.test.ts +209 -0
- package/src/unnest-code-blocks.test.ts +713 -0
- package/src/unnest-code-blocks.ts +185 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +212 -0
- package/src/voice-attachment.ts +51 -0
- package/src/voice-handler.ts +908 -0
- package/src/voice-message.e2e.test.ts +1255 -0
- package/src/voice.test.ts +281 -0
- package/src/voice.ts +627 -0
- package/src/wait-session.ts +147 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +391 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +223 -0
- package/src/worktrees.ts +1294 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lintcn
|
|
3
|
+
description: |
|
|
4
|
+
Type-aware TypeScript lint rules in .lintcn/ Go files. Only load this skill when creating, editing, or debugging rule files.
|
|
5
|
+
|
|
6
|
+
To just run the linter: `npx lintcn lint` (or `--fix`, `--tsconfig <path>`). Finds .lintcn/ by walking up from cwd. First build ~30s, cached ~1s. In monorepos, run from each package folder, not the root.
|
|
7
|
+
|
|
8
|
+
Warnings don't fail CI and only show for git-changed files by default. Use `--all-warnings` to see them across the entire codebase.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# lintcn — Writing Custom tsgolint Lint Rules
|
|
12
|
+
|
|
13
|
+
tsgolint rules are Go functions that listen for TypeScript AST nodes and use the
|
|
14
|
+
TypeScript type checker for type-aware analysis. Each rule lives in its own
|
|
15
|
+
subfolder under `.lintcn/` and is compiled into a custom tsgolint binary.
|
|
16
|
+
|
|
17
|
+
**Every rule MUST be in a subfolder** — flat `.go` files in `.lintcn/` root are
|
|
18
|
+
not supported. The subfolder name = Go package name = rule identity.
|
|
19
|
+
|
|
20
|
+
Always run `go build ./...` inside `.lintcn/` to validate rules compile.
|
|
21
|
+
Always run `go test -v ./...` inside `.lintcn/` to run tests.
|
|
22
|
+
|
|
23
|
+
## Directory Layout
|
|
24
|
+
|
|
25
|
+
Each rule is a subfolder. The Go package name must match the folder name:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
.lintcn/
|
|
29
|
+
no_floating_promises/
|
|
30
|
+
no_floating_promises.go ← rule source (committed)
|
|
31
|
+
no_floating_promises_test.go ← tests (committed)
|
|
32
|
+
options.go ← rule options struct
|
|
33
|
+
await_thenable/
|
|
34
|
+
await_thenable.go
|
|
35
|
+
await_thenable_test.go
|
|
36
|
+
my_custom_rule/
|
|
37
|
+
my_custom_rule.go
|
|
38
|
+
.gitignore ← ignores generated Go files
|
|
39
|
+
go.mod ← generated
|
|
40
|
+
go.work ← generated
|
|
41
|
+
.tsgolint/ ← symlink to cached source (gitignored)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Adding Rules
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Add a rule folder from tsgolint
|
|
48
|
+
npx lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises
|
|
49
|
+
|
|
50
|
+
# Add by file URL (auto-fetches the whole folder)
|
|
51
|
+
npx lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go
|
|
52
|
+
|
|
53
|
+
# List installed rules
|
|
54
|
+
npx lintcn list
|
|
55
|
+
|
|
56
|
+
# Remove a rule (deletes the whole subfolder)
|
|
57
|
+
npx lintcn remove no-floating-promises
|
|
58
|
+
|
|
59
|
+
# Lint your project
|
|
60
|
+
npx lintcn lint
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Rule Anatomy
|
|
64
|
+
|
|
65
|
+
Every rule is a `rule.Rule` struct with a `Name` and a `Run` function.
|
|
66
|
+
`Run` receives a `RuleContext` and returns a `RuleListeners` map — a map from
|
|
67
|
+
`ast.Kind` to callback functions. The linter walks the AST and calls your
|
|
68
|
+
callback when it encounters a node of that kind.
|
|
69
|
+
|
|
70
|
+
```go
|
|
71
|
+
// .lintcn/my_rule/my_rule.go
|
|
72
|
+
package my_rule
|
|
73
|
+
|
|
74
|
+
import (
|
|
75
|
+
"github.com/microsoft/typescript-go/shim/ast"
|
|
76
|
+
"github.com/typescript-eslint/tsgolint/internal/rule"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
var MyRule = rule.Rule{
|
|
80
|
+
Name: "my-rule",
|
|
81
|
+
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
|
|
82
|
+
return rule.RuleListeners{
|
|
83
|
+
ast.KindCallExpression: func(node *ast.Node) {
|
|
84
|
+
call := node.AsCallExpression()
|
|
85
|
+
// analyze the call...
|
|
86
|
+
ctx.ReportNode(node, rule.RuleMessage{
|
|
87
|
+
Id: "myError",
|
|
88
|
+
Description: "Something is wrong here.",
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Metadata Comments
|
|
97
|
+
|
|
98
|
+
Add `// lintcn:` comments at the top for CLI metadata:
|
|
99
|
+
|
|
100
|
+
```go
|
|
101
|
+
// lintcn:name my-rule
|
|
102
|
+
// lintcn:severity warn
|
|
103
|
+
// lintcn:description Disallow doing X without checking Y
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Available directives:
|
|
107
|
+
|
|
108
|
+
| Directive | Values | Default | Description |
|
|
109
|
+
| -------------------- | --------------- | ----------- | -------------------- |
|
|
110
|
+
| `lintcn:name` | kebab-case | folder name | Rule display name |
|
|
111
|
+
| `lintcn:severity` | `error`, `warn` | `error` | Severity level |
|
|
112
|
+
| `lintcn:description` | text | empty | One-line description |
|
|
113
|
+
| `lintcn:source` | URL | empty | Original source URL |
|
|
114
|
+
|
|
115
|
+
### Warning Severity
|
|
116
|
+
|
|
117
|
+
Rules with `// lintcn:severity warn`:
|
|
118
|
+
|
|
119
|
+
- Don't fail CI (exit code 0)
|
|
120
|
+
- Only show for git-changed/untracked files — unchanged files are skipped
|
|
121
|
+
- Use `--all-warnings` to see warnings across the whole codebase
|
|
122
|
+
|
|
123
|
+
Warnings are for rules that guide agents writing new code without flooding
|
|
124
|
+
the output with violations from the rest of the codebase. Examples:
|
|
125
|
+
|
|
126
|
+
- "Remove `as any`, the actual type is `string`"
|
|
127
|
+
- "This `||` fallback is unreachable, the left side is never nullish"
|
|
128
|
+
- "Unhandled Error return value, assign to a variable and check it"
|
|
129
|
+
|
|
130
|
+
### Package Name
|
|
131
|
+
|
|
132
|
+
Each rule subfolder has its own Go package. The package name must match the
|
|
133
|
+
folder name (e.g. `package no_floating_promises` in folder `no_floating_promises/`).
|
|
134
|
+
The exported variable name must match the pattern `var XxxRule = rule.Rule{...}`.
|
|
135
|
+
|
|
136
|
+
## RuleContext
|
|
137
|
+
|
|
138
|
+
`ctx rule.RuleContext` provides:
|
|
139
|
+
|
|
140
|
+
| Field | Type | Description |
|
|
141
|
+
| --------------------------- | -------------------------- | -------------------------- |
|
|
142
|
+
| `SourceFile` | `*ast.SourceFile` | Current file being linted |
|
|
143
|
+
| `Program` | `*compiler.Program` | Full TypeScript program |
|
|
144
|
+
| `TypeChecker` | `*checker.Checker` | TypeScript type checker |
|
|
145
|
+
| `ReportNode` | `func(node, msg)` | Report error on a node |
|
|
146
|
+
| `ReportNodeWithFixes` | `func(node, msg, fixesFn)` | Report with auto-fixes |
|
|
147
|
+
| `ReportNodeWithSuggestions` | `func(node, msg, suggFn)` | Report with suggestions |
|
|
148
|
+
| `ReportRange` | `func(range, msg)` | Report on a text range |
|
|
149
|
+
| `ReportDiagnostic` | `func(diagnostic)` | Report with labeled ranges |
|
|
150
|
+
|
|
151
|
+
## AST Node Listeners
|
|
152
|
+
|
|
153
|
+
### Most Useful ast.Kind Values
|
|
154
|
+
|
|
155
|
+
```go
|
|
156
|
+
// Statements
|
|
157
|
+
ast.KindExpressionStatement // bare expression: `foo();`
|
|
158
|
+
ast.KindReturnStatement // `return x`
|
|
159
|
+
ast.KindThrowStatement // `throw x`
|
|
160
|
+
ast.KindIfStatement // `if (x) { ... }`
|
|
161
|
+
ast.KindVariableDeclaration // `const x = ...`
|
|
162
|
+
ast.KindForInStatement // `for (x in y)`
|
|
163
|
+
|
|
164
|
+
// Expressions
|
|
165
|
+
ast.KindCallExpression // `foo()` — most commonly listened
|
|
166
|
+
ast.KindNewExpression // `new Foo()`
|
|
167
|
+
ast.KindBinaryExpression // `a + b`, `a === b`, `a = b`
|
|
168
|
+
ast.KindPropertyAccessExpression // `obj.prop`
|
|
169
|
+
ast.KindElementAccessExpression // `obj[key]`
|
|
170
|
+
ast.KindAwaitExpression // `await x`
|
|
171
|
+
ast.KindConditionalExpression // `a ? b : c`
|
|
172
|
+
ast.KindPrefixUnaryExpression // `!x`, `-x`, `typeof x`
|
|
173
|
+
ast.KindTemplateExpression // `hello ${name}`
|
|
174
|
+
ast.KindDeleteExpression // `delete obj.x`
|
|
175
|
+
ast.KindVoidExpression // `void x`
|
|
176
|
+
|
|
177
|
+
// Declarations
|
|
178
|
+
ast.KindFunctionDeclaration
|
|
179
|
+
ast.KindArrowFunction
|
|
180
|
+
ast.KindMethodDeclaration
|
|
181
|
+
ast.KindClassDeclaration
|
|
182
|
+
ast.KindEnumDeclaration
|
|
183
|
+
|
|
184
|
+
// Types
|
|
185
|
+
ast.KindUnionType // `A | B`
|
|
186
|
+
ast.KindIntersectionType // `A & B`
|
|
187
|
+
ast.KindAsExpression // `x as T`
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Enter and Exit Listeners
|
|
191
|
+
|
|
192
|
+
By default, listeners fire when the AST walker **enters** a node.
|
|
193
|
+
Use `rule.ListenerOnExit(kind)` to fire when the walker **exits** — useful
|
|
194
|
+
for scope tracking:
|
|
195
|
+
|
|
196
|
+
```go
|
|
197
|
+
return rule.RuleListeners{
|
|
198
|
+
// enter function — push scope
|
|
199
|
+
ast.KindFunctionDeclaration: func(node *ast.Node) {
|
|
200
|
+
currentScope = &scopeInfo{upper: currentScope}
|
|
201
|
+
},
|
|
202
|
+
// exit function — pop scope and check
|
|
203
|
+
rule.ListenerOnExit(ast.KindFunctionDeclaration): func(node *ast.Node) {
|
|
204
|
+
if !currentScope.hasAwait {
|
|
205
|
+
ctx.ReportNode(node, msg)
|
|
206
|
+
}
|
|
207
|
+
currentScope = currentScope.upper
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Used by require_await, return_await, consistent_return, prefer_readonly for
|
|
213
|
+
tracking state across function bodies with a scope stack.
|
|
214
|
+
|
|
215
|
+
### Allow/NotAllow Pattern Listeners
|
|
216
|
+
|
|
217
|
+
For destructuring and assignment contexts:
|
|
218
|
+
|
|
219
|
+
```go
|
|
220
|
+
rule.ListenerOnAllowPattern(ast.KindObjectLiteralExpression) // inside destructuring
|
|
221
|
+
rule.ListenerOnNotAllowPattern(ast.KindArrayLiteralExpression) // outside destructuring
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Used by no_unsafe_assignment and unbound_method.
|
|
225
|
+
|
|
226
|
+
## Type Checker APIs
|
|
227
|
+
|
|
228
|
+
### Getting Types
|
|
229
|
+
|
|
230
|
+
```go
|
|
231
|
+
// Get the type of any AST node
|
|
232
|
+
t := ctx.TypeChecker.GetTypeAtLocation(node)
|
|
233
|
+
|
|
234
|
+
// Get type with constraint resolution (unwraps type params)
|
|
235
|
+
t := utils.GetConstrainedTypeAtLocation(ctx.TypeChecker, node)
|
|
236
|
+
|
|
237
|
+
// Get the contextual type (what TypeScript expects at this position)
|
|
238
|
+
t := checker.Checker_getContextualType(ctx.TypeChecker, node, checker.ContextFlagsNone)
|
|
239
|
+
|
|
240
|
+
// Get the apparent type (resolves mapped types, intersections)
|
|
241
|
+
t := checker.Checker_getApparentType(ctx.TypeChecker, t)
|
|
242
|
+
|
|
243
|
+
// Get awaited type (unwraps Promise)
|
|
244
|
+
t := checker.Checker_getAwaitedType(ctx.TypeChecker, t)
|
|
245
|
+
|
|
246
|
+
// Get type from a type annotation node
|
|
247
|
+
t := checker.Checker_getTypeFromTypeNode(ctx.TypeChecker, typeNode)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Type Flag Checks
|
|
251
|
+
|
|
252
|
+
TypeFlags are bitmasks — check with `utils.IsTypeFlagSet`:
|
|
253
|
+
|
|
254
|
+
```go
|
|
255
|
+
// Check specific flags
|
|
256
|
+
if utils.IsTypeFlagSet(t, checker.TypeFlagsVoid) { return }
|
|
257
|
+
if utils.IsTypeFlagSet(t, checker.TypeFlagsUndefined) { return }
|
|
258
|
+
if utils.IsTypeFlagSet(t, checker.TypeFlagsNever) { return }
|
|
259
|
+
if utils.IsTypeFlagSet(t, checker.TypeFlagsAny) { return }
|
|
260
|
+
|
|
261
|
+
// Combine flags with |
|
|
262
|
+
if utils.IsTypeFlagSet(t, checker.TypeFlagsVoid|checker.TypeFlagsUndefined|checker.TypeFlagsNever) {
|
|
263
|
+
return // skip void, undefined, and never
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Convenience helpers
|
|
267
|
+
utils.IsTypeAnyType(t)
|
|
268
|
+
utils.IsTypeUnknownType(t)
|
|
269
|
+
utils.IsObjectType(t)
|
|
270
|
+
utils.IsTypeParameter(t)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Union and Intersection Types
|
|
274
|
+
|
|
275
|
+
**Decomposing unions is the most common pattern** — 58 uses across all rules:
|
|
276
|
+
|
|
277
|
+
```go
|
|
278
|
+
// Iterate over union parts: `Error | string` → [Error, string]
|
|
279
|
+
for _, part := range utils.UnionTypeParts(t) {
|
|
280
|
+
if utils.IsErrorLike(ctx.Program, ctx.TypeChecker, part) {
|
|
281
|
+
hasError = true
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check if it's a union type
|
|
287
|
+
if utils.IsUnionType(t) { ... }
|
|
288
|
+
if utils.IsIntersectionType(t) { ... }
|
|
289
|
+
|
|
290
|
+
// Iterate intersection parts
|
|
291
|
+
for _, part := range utils.IntersectionTypeParts(t) { ... }
|
|
292
|
+
|
|
293
|
+
// Recursive predicate check across union/intersection
|
|
294
|
+
result := utils.TypeRecurser(t, func(t *checker.Type) bool {
|
|
295
|
+
return utils.IsTypeAnyType(t)
|
|
296
|
+
})
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Built-in Type Checks
|
|
300
|
+
|
|
301
|
+
```go
|
|
302
|
+
// Error types
|
|
303
|
+
utils.IsErrorLike(ctx.Program, ctx.TypeChecker, t)
|
|
304
|
+
utils.IsReadonlyErrorLike(ctx.Program, ctx.TypeChecker, t)
|
|
305
|
+
|
|
306
|
+
// Promise types
|
|
307
|
+
utils.IsPromiseLike(ctx.Program, ctx.TypeChecker, t)
|
|
308
|
+
utils.IsThenableType(ctx.TypeChecker, node, t)
|
|
309
|
+
|
|
310
|
+
// Array types
|
|
311
|
+
checker.Checker_isArrayType(ctx.TypeChecker, t)
|
|
312
|
+
checker.IsTupleType(t)
|
|
313
|
+
checker.Checker_isArrayOrTupleType(ctx.TypeChecker, t)
|
|
314
|
+
|
|
315
|
+
// Generic built-in matching
|
|
316
|
+
utils.IsBuiltinSymbolLike(ctx.Program, ctx.TypeChecker, t, "Function")
|
|
317
|
+
utils.IsBuiltinSymbolLike(ctx.Program, ctx.TypeChecker, t, "RegExp")
|
|
318
|
+
utils.IsBuiltinSymbolLike(ctx.Program, ctx.TypeChecker, t, "ReadonlyArray")
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Type Properties and Signatures
|
|
322
|
+
|
|
323
|
+
```go
|
|
324
|
+
// Get a named property from a type
|
|
325
|
+
prop := checker.Checker_getPropertyOfType(ctx.TypeChecker, t, "then")
|
|
326
|
+
if prop != nil {
|
|
327
|
+
propType := ctx.TypeChecker.GetTypeOfSymbolAtLocation(prop, node)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Get all properties
|
|
331
|
+
props := checker.Checker_getPropertiesOfType(ctx.TypeChecker, t)
|
|
332
|
+
|
|
333
|
+
// Get call signatures (for callable types)
|
|
334
|
+
sigs := utils.GetCallSignatures(ctx.TypeChecker, t)
|
|
335
|
+
// or
|
|
336
|
+
sigs := ctx.TypeChecker.GetCallSignatures(t)
|
|
337
|
+
|
|
338
|
+
// Get signature parameters
|
|
339
|
+
params := checker.Signature_parameters(sig)
|
|
340
|
+
|
|
341
|
+
// Get return type of a signature
|
|
342
|
+
returnType := checker.Checker_getReturnTypeOfSignature(ctx.TypeChecker, sig)
|
|
343
|
+
|
|
344
|
+
// Get type arguments (for generics, arrays, tuples)
|
|
345
|
+
typeArgs := checker.Checker_getTypeArguments(ctx.TypeChecker, t)
|
|
346
|
+
|
|
347
|
+
// Get resolved call signature at a call site
|
|
348
|
+
sig := checker.Checker_getResolvedSignature(ctx.TypeChecker, callNode)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Type Assignability
|
|
352
|
+
|
|
353
|
+
```go
|
|
354
|
+
// Check if source is assignable to target
|
|
355
|
+
if checker.Checker_isTypeAssignableTo(ctx.TypeChecker, sourceType, targetType) {
|
|
356
|
+
// source extends target
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Get base constraint of a type parameter
|
|
360
|
+
constraint := checker.Checker_getBaseConstraintOfType(ctx.TypeChecker, t)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Symbols
|
|
364
|
+
|
|
365
|
+
```go
|
|
366
|
+
// Get symbol at a location
|
|
367
|
+
symbol := ctx.TypeChecker.GetSymbolAtLocation(node)
|
|
368
|
+
|
|
369
|
+
// Get declaration for a symbol
|
|
370
|
+
decl := utils.GetDeclaration(ctx.TypeChecker, node)
|
|
371
|
+
|
|
372
|
+
// Get type from symbol
|
|
373
|
+
t := checker.Checker_getTypeOfSymbol(ctx.TypeChecker, symbol)
|
|
374
|
+
t := checker.Checker_getDeclaredTypeOfSymbol(ctx.TypeChecker, symbol)
|
|
375
|
+
|
|
376
|
+
// Check if symbol comes from default library
|
|
377
|
+
utils.IsSymbolFromDefaultLibrary(ctx.Program, symbol)
|
|
378
|
+
|
|
379
|
+
// Get the accessed property name (works with computed properties too)
|
|
380
|
+
name, ok := checker.Checker_getAccessedPropertyName(ctx.TypeChecker, node)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Formatting Types for Error Messages
|
|
384
|
+
|
|
385
|
+
```go
|
|
386
|
+
typeName := ctx.TypeChecker.TypeToString(t)
|
|
387
|
+
// → "string", "Error | User", "Promise<number>", etc.
|
|
388
|
+
|
|
389
|
+
// Shorter type name helper
|
|
390
|
+
name := utils.GetTypeName(ctx.TypeChecker, t)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## AST Navigation
|
|
394
|
+
|
|
395
|
+
### Node Casting
|
|
396
|
+
|
|
397
|
+
Every AST node is `*ast.Node`. Use `.AsXxx()` to access specific fields:
|
|
398
|
+
|
|
399
|
+
```go
|
|
400
|
+
call := node.AsCallExpression()
|
|
401
|
+
call.Expression // the callee
|
|
402
|
+
call.Arguments // argument list
|
|
403
|
+
|
|
404
|
+
binary := node.AsBinaryExpression()
|
|
405
|
+
binary.Left
|
|
406
|
+
binary.Right
|
|
407
|
+
binary.OperatorToken.Kind // ast.KindEqualsToken, ast.KindPlusToken, etc.
|
|
408
|
+
|
|
409
|
+
prop := node.AsPropertyAccessExpression()
|
|
410
|
+
prop.Expression // object
|
|
411
|
+
prop.Name() // property name node
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Type Predicates
|
|
415
|
+
|
|
416
|
+
```go
|
|
417
|
+
ast.IsCallExpression(node)
|
|
418
|
+
ast.IsPropertyAccessExpression(node)
|
|
419
|
+
ast.IsIdentifier(node)
|
|
420
|
+
ast.IsAccessExpression(node) // property OR element access
|
|
421
|
+
ast.IsBinaryExpression(node)
|
|
422
|
+
ast.IsAssignmentExpression(node, includeCompound) // a = b, a += b
|
|
423
|
+
ast.IsVoidExpression(node)
|
|
424
|
+
ast.IsAwaitExpression(node)
|
|
425
|
+
ast.IsFunctionLike(node)
|
|
426
|
+
ast.IsArrowFunction(node)
|
|
427
|
+
ast.IsStringLiteral(node)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Skipping Parentheses
|
|
431
|
+
|
|
432
|
+
Always skip parentheses when analyzing expression content:
|
|
433
|
+
|
|
434
|
+
```go
|
|
435
|
+
expression := ast.SkipParentheses(node.AsExpressionStatement().Expression)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Walking Parents
|
|
439
|
+
|
|
440
|
+
```go
|
|
441
|
+
parent := node.Parent
|
|
442
|
+
for parent != nil {
|
|
443
|
+
if ast.IsCallExpression(parent) {
|
|
444
|
+
// node is inside a call expression
|
|
445
|
+
break
|
|
446
|
+
}
|
|
447
|
+
parent = parent.Parent
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Reporting Errors
|
|
452
|
+
|
|
453
|
+
### Simple Error
|
|
454
|
+
|
|
455
|
+
```go
|
|
456
|
+
ctx.ReportNode(node, rule.RuleMessage{
|
|
457
|
+
Id: "myErrorId", // unique ID for the error
|
|
458
|
+
Description: "Something is wrong.",
|
|
459
|
+
Help: "Optional longer explanation.", // shown as help text
|
|
460
|
+
})
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Error with Auto-Fix
|
|
464
|
+
|
|
465
|
+
Fixes are applied automatically by the linter:
|
|
466
|
+
|
|
467
|
+
```go
|
|
468
|
+
ctx.ReportNodeWithFixes(node, msg, func() []rule.RuleFix {
|
|
469
|
+
return []rule.RuleFix{
|
|
470
|
+
rule.RuleFixInsertBefore(ctx.SourceFile, node, "await "),
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Error with Suggestions
|
|
476
|
+
|
|
477
|
+
Suggestions require user confirmation:
|
|
478
|
+
|
|
479
|
+
```go
|
|
480
|
+
ctx.ReportNodeWithSuggestions(node, msg, func() []rule.RuleSuggestion {
|
|
481
|
+
return []rule.RuleSuggestion{{
|
|
482
|
+
Message: rule.RuleMessage{Id: "addAwait", Description: "Add await"},
|
|
483
|
+
FixesArr: []rule.RuleFix{
|
|
484
|
+
rule.RuleFixInsertBefore(ctx.SourceFile, node, "await "),
|
|
485
|
+
},
|
|
486
|
+
}}
|
|
487
|
+
})
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Error with Multiple Labeled Ranges
|
|
491
|
+
|
|
492
|
+
Highlight multiple code locations:
|
|
493
|
+
|
|
494
|
+
```go
|
|
495
|
+
ctx.ReportDiagnostic(rule.RuleDiagnostic{
|
|
496
|
+
Range: exprRange,
|
|
497
|
+
Message: rule.RuleMessage{Id: "typeMismatch", Description: "Types are incompatible"},
|
|
498
|
+
LabeledRanges: []rule.RuleLabeledRange{
|
|
499
|
+
{Label: fmt.Sprintf("Type: %v", leftType), Range: leftRange},
|
|
500
|
+
{Label: fmt.Sprintf("Type: %v", rightType), Range: rightRange},
|
|
501
|
+
},
|
|
502
|
+
})
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Fix Helpers
|
|
506
|
+
|
|
507
|
+
```go
|
|
508
|
+
// Insert text before a node
|
|
509
|
+
rule.RuleFixInsertBefore(ctx.SourceFile, node, "await ")
|
|
510
|
+
|
|
511
|
+
// Insert text after a node
|
|
512
|
+
rule.RuleFixInsertAfter(node, ")")
|
|
513
|
+
|
|
514
|
+
// Replace a node with text
|
|
515
|
+
rule.RuleFixReplace(ctx.SourceFile, node, "newCode")
|
|
516
|
+
|
|
517
|
+
// Remove a node
|
|
518
|
+
rule.RuleFixRemove(ctx.SourceFile, node)
|
|
519
|
+
|
|
520
|
+
// Replace a specific text range
|
|
521
|
+
rule.RuleFixReplaceRange(textRange, "replacement")
|
|
522
|
+
|
|
523
|
+
// Remove a specific text range
|
|
524
|
+
rule.RuleFixRemoveRange(textRange)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Getting Token Ranges for Fixes
|
|
528
|
+
|
|
529
|
+
When you need the exact range of a keyword token (like `void`, `as`, `await`):
|
|
530
|
+
|
|
531
|
+
```go
|
|
532
|
+
import "github.com/microsoft/typescript-go/shim/scanner"
|
|
533
|
+
|
|
534
|
+
// Get range of token at a position
|
|
535
|
+
voidTokenRange := scanner.GetRangeOfTokenAtPosition(ctx.SourceFile, node.Pos())
|
|
536
|
+
|
|
537
|
+
// Get a scanner to scan forward
|
|
538
|
+
s := scanner.GetScannerForSourceFile(ctx.SourceFile, startPos)
|
|
539
|
+
tokenRange := s.TokenRange()
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Rule Options
|
|
543
|
+
|
|
544
|
+
Rules can accept configuration via JSON:
|
|
545
|
+
|
|
546
|
+
```go
|
|
547
|
+
var MyRule = rule.Rule{
|
|
548
|
+
Name: "my-rule",
|
|
549
|
+
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
|
|
550
|
+
opts := utils.UnmarshalOptions[MyRuleOptions](options, "my-rule")
|
|
551
|
+
// opts is now typed
|
|
552
|
+
},
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
type MyRuleOptions struct {
|
|
556
|
+
IgnoreVoid bool `json:"ignoreVoid"`
|
|
557
|
+
AllowedTypes []string `json:"allowedTypes"`
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
For lintcn rules, define the options struct directly in your rule file or
|
|
562
|
+
in a separate `options.go` file in the same subfolder.
|
|
563
|
+
|
|
564
|
+
## State Tracking (Scope Stacks)
|
|
565
|
+
|
|
566
|
+
When you need to track state across function boundaries (like "does this
|
|
567
|
+
function contain an await?"), use enter/exit listener pairs with a linked
|
|
568
|
+
list as a stack:
|
|
569
|
+
|
|
570
|
+
```go
|
|
571
|
+
type scopeInfo struct {
|
|
572
|
+
hasAwait bool
|
|
573
|
+
upper *scopeInfo
|
|
574
|
+
}
|
|
575
|
+
var currentScope *scopeInfo
|
|
576
|
+
|
|
577
|
+
enterFunc := func(node *ast.Node) {
|
|
578
|
+
currentScope = &scopeInfo{upper: currentScope}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
exitFunc := func(node *ast.Node) {
|
|
582
|
+
if !currentScope.hasAwait {
|
|
583
|
+
ctx.ReportNode(node, msg)
|
|
584
|
+
}
|
|
585
|
+
currentScope = currentScope.upper
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return rule.RuleListeners{
|
|
589
|
+
ast.KindFunctionDeclaration: enterFunc,
|
|
590
|
+
rule.ListenerOnExit(ast.KindFunctionDeclaration): exitFunc,
|
|
591
|
+
ast.KindArrowFunction: enterFunc,
|
|
592
|
+
rule.ListenerOnExit(ast.KindArrowFunction): exitFunc,
|
|
593
|
+
ast.KindAwaitExpression: func(node *ast.Node) {
|
|
594
|
+
currentScope.hasAwait = true
|
|
595
|
+
},
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
## Testing
|
|
600
|
+
|
|
601
|
+
Tests use `rule_tester.RunRuleTester` which creates a TypeScript program from
|
|
602
|
+
inline code and runs the rule against it. The test file must use the same
|
|
603
|
+
package name as the rule:
|
|
604
|
+
|
|
605
|
+
```go
|
|
606
|
+
// .lintcn/my_rule/my_rule_test.go
|
|
607
|
+
package my_rule
|
|
608
|
+
|
|
609
|
+
import (
|
|
610
|
+
"testing"
|
|
611
|
+
"github.com/typescript-eslint/tsgolint/internal/rule_tester"
|
|
612
|
+
"github.com/typescript-eslint/tsgolint/internal/rules/fixtures"
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
func TestMyRule(t *testing.T) {
|
|
616
|
+
t.Parallel()
|
|
617
|
+
rule_tester.RunRuleTester(
|
|
618
|
+
fixtures.GetRootDir(),
|
|
619
|
+
"tsconfig.minimal.json",
|
|
620
|
+
t,
|
|
621
|
+
&MyRule,
|
|
622
|
+
validCases,
|
|
623
|
+
invalidCases,
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Valid Test Cases (should NOT trigger)
|
|
629
|
+
|
|
630
|
+
```go
|
|
631
|
+
var validCases = []rule_tester.ValidTestCase{
|
|
632
|
+
{Code: `const x = getUser("id");`},
|
|
633
|
+
{Code: `void dangerousCall();`},
|
|
634
|
+
// tsx support
|
|
635
|
+
{Code: `<div onClick={() => {}} />`, Tsx: true},
|
|
636
|
+
// custom filename
|
|
637
|
+
{Code: `import x from './foo'`, FileName: "index.ts"},
|
|
638
|
+
// with rule options
|
|
639
|
+
{Code: `getUser("id");`, Options: MyRuleOptions{IgnoreVoid: true}},
|
|
640
|
+
// with extra files for multi-file tests
|
|
641
|
+
{
|
|
642
|
+
Code: `import { x } from './helper';`,
|
|
643
|
+
Files: map[string]string{
|
|
644
|
+
"helper.ts": `export const x = 1;`,
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Invalid Test Cases (SHOULD trigger)
|
|
651
|
+
|
|
652
|
+
```go
|
|
653
|
+
var invalidCases = []rule_tester.InvalidTestCase{
|
|
654
|
+
// Basic — just check the error fires
|
|
655
|
+
{
|
|
656
|
+
Code: `
|
|
657
|
+
declare function getUser(id: string): Error | { name: string };
|
|
658
|
+
getUser("id");
|
|
659
|
+
`,
|
|
660
|
+
Errors: []rule_tester.InvalidTestCaseError{
|
|
661
|
+
{MessageId: "noUnhandledError"},
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
// With exact position
|
|
665
|
+
{
|
|
666
|
+
Code: `getUser("id");`,
|
|
667
|
+
Errors: []rule_tester.InvalidTestCaseError{
|
|
668
|
+
{MessageId: "noUnhandledError", Line: 1, Column: 1, EndColumn: 15},
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
// With suggestions
|
|
672
|
+
{
|
|
673
|
+
Code: `
|
|
674
|
+
declare const arr: number[];
|
|
675
|
+
delete arr[0];
|
|
676
|
+
`,
|
|
677
|
+
Errors: []rule_tester.InvalidTestCaseError{
|
|
678
|
+
{
|
|
679
|
+
MessageId: "noArrayDelete",
|
|
680
|
+
Suggestions: []rule_tester.InvalidTestCaseSuggestion{
|
|
681
|
+
{
|
|
682
|
+
MessageId: "useSplice",
|
|
683
|
+
Output: `
|
|
684
|
+
declare const arr: number[];
|
|
685
|
+
arr.splice(0, 1);
|
|
686
|
+
`,
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
// With auto-fix output (code after fix applied)
|
|
693
|
+
{
|
|
694
|
+
Code: `const x = foo as any;`,
|
|
695
|
+
Output: []string{`const x = foo;`},
|
|
696
|
+
Errors: []rule_tester.InvalidTestCaseError{
|
|
697
|
+
{MessageId: "unsafeAssertion"},
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### Important Test Details
|
|
704
|
+
|
|
705
|
+
- **MessageId** must match the `Id` field in your `rule.RuleMessage`
|
|
706
|
+
- **Line/Column** are 1-indexed, optional (omit for flexibility)
|
|
707
|
+
- **Output** is the code after ALL auto-fixes are applied (iterates up to 10 times)
|
|
708
|
+
- **Suggestions** check the output of each individual suggestion fix
|
|
709
|
+
- Tests run in parallel by default (`t.Parallel()`)
|
|
710
|
+
- Use `Only: true` on a test case to run only that test (like `.only` in vitest)
|
|
711
|
+
- Use `Skip: true` to skip a test case
|
|
712
|
+
|
|
713
|
+
### Running Tests
|
|
714
|
+
|
|
715
|
+
```bash
|
|
716
|
+
cd .lintcn
|
|
717
|
+
go test -v ./... # all tests
|
|
718
|
+
go test -v -run TestMyRule # specific test
|
|
719
|
+
go test -count=1 ./... # bypass test cache
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Snapshots
|
|
723
|
+
|
|
724
|
+
Tests generate snapshot files with the full diagnostic output — message text,
|
|
725
|
+
annotated source code, and underlined ranges. Run with `UPDATE_SNAPS=true` to
|
|
726
|
+
create or update them:
|
|
727
|
+
|
|
728
|
+
```bash
|
|
729
|
+
# From the build workspace (found via `lintcn build` output path)
|
|
730
|
+
UPDATE_SNAPS=true go test -run TestMyRule -count=1 ./rules/my_rule/
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Snapshots are written to `internal/rule_tester/__snapshots__/{rule-name}.snap`
|
|
734
|
+
inside the cached tsgolint source. Copy them into your rule folder for reference:
|
|
735
|
+
|
|
736
|
+
```
|
|
737
|
+
.lintcn/my_rule/__snapshots__/my-rule.snap
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
**Always read the snapshot after writing tests** — it shows the exact messages
|
|
741
|
+
your rule produces, which is how you verify the output makes sense. Example
|
|
742
|
+
snapshot from `no-type-assertion`:
|
|
743
|
+
|
|
744
|
+
```
|
|
745
|
+
[TestNoTypeAssertion/invalid-7 - 1]
|
|
746
|
+
Diagnostic 1: typeAssertion (4:14 - 4:22)
|
|
747
|
+
Message: Type assertion `as User ({ name: string; age: number })`.
|
|
748
|
+
The expression type is `Error | User`. Try removing the assertion
|
|
749
|
+
or narrowing the type instead.
|
|
750
|
+
3 | declare const x: User | Error;
|
|
751
|
+
4 | const y = x as User;
|
|
752
|
+
| ~~~~~~~~~
|
|
753
|
+
5 |
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
[TestNoTypeAssertion/invalid-8 - 1]
|
|
757
|
+
Diagnostic 1: typeAssertion (4:14 - 4:24)
|
|
758
|
+
Message: Type assertion `as Config ({ host: string; port: number })`.
|
|
759
|
+
The expression type is `Config | null`. Try removing the assertion
|
|
760
|
+
or narrowing the type instead.
|
|
761
|
+
3 | declare const x: Config | null;
|
|
762
|
+
4 | const y = x as Config;
|
|
763
|
+
| ~~~~~~~~~~~
|
|
764
|
+
5 |
|
|
765
|
+
---
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
This shows: the message ID, position, full description text, and the source
|
|
769
|
+
code with the flagged range underlined. Use this to verify your error messages
|
|
770
|
+
are helpful and include enough type information for agents to act on.
|
|
771
|
+
|
|
772
|
+
## Complete Rule Example: no-unhandled-error
|
|
773
|
+
|
|
774
|
+
A real rule that enforces the errore pattern — errors when a call expression
|
|
775
|
+
returns a type containing `Error` and the result is discarded:
|
|
776
|
+
|
|
777
|
+
```go
|
|
778
|
+
// .lintcn/no_unhandled_error/no_unhandled_error.go
|
|
779
|
+
|
|
780
|
+
// lintcn:name no-unhandled-error
|
|
781
|
+
// lintcn:description Disallow discarding expressions that are subtypes of Error
|
|
782
|
+
|
|
783
|
+
package no_unhandled_error
|
|
784
|
+
|
|
785
|
+
import (
|
|
786
|
+
"github.com/microsoft/typescript-go/shim/ast"
|
|
787
|
+
"github.com/microsoft/typescript-go/shim/checker"
|
|
788
|
+
"github.com/typescript-eslint/tsgolint/internal/rule"
|
|
789
|
+
"github.com/typescript-eslint/tsgolint/internal/utils"
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
var NoUnhandledErrorRule = rule.Rule{
|
|
793
|
+
Name: "no-unhandled-error",
|
|
794
|
+
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
|
|
795
|
+
return rule.RuleListeners{
|
|
796
|
+
ast.KindExpressionStatement: func(node *ast.Node) {
|
|
797
|
+
exprStatement := node.AsExpressionStatement()
|
|
798
|
+
expression := ast.SkipParentheses(exprStatement.Expression)
|
|
799
|
+
|
|
800
|
+
// void expressions are intentional discards
|
|
801
|
+
if ast.IsVoidExpression(expression) {
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// only check call expressions and await expressions wrapping calls
|
|
806
|
+
innerExpr := expression
|
|
807
|
+
if ast.IsAwaitExpression(innerExpr) {
|
|
808
|
+
innerExpr = ast.SkipParentheses(innerExpr.Expression())
|
|
809
|
+
}
|
|
810
|
+
if !ast.IsCallExpression(innerExpr) {
|
|
811
|
+
return
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
t := ctx.TypeChecker.GetTypeAtLocation(expression)
|
|
815
|
+
|
|
816
|
+
// skip void, undefined, never
|
|
817
|
+
if utils.IsTypeFlagSet(t,
|
|
818
|
+
checker.TypeFlagsVoid|checker.TypeFlagsVoidLike|
|
|
819
|
+
checker.TypeFlagsUndefined|checker.TypeFlagsNever) {
|
|
820
|
+
return
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// check if any union part is Error-like
|
|
824
|
+
for _, part := range utils.UnionTypeParts(t) {
|
|
825
|
+
if utils.IsErrorLike(ctx.Program, ctx.TypeChecker, part) {
|
|
826
|
+
ctx.ReportNode(node, rule.RuleMessage{
|
|
827
|
+
Id: "noUnhandledError",
|
|
828
|
+
Description: "Error-typed return value is not handled.",
|
|
829
|
+
})
|
|
830
|
+
return
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
## Go Workspace Setup
|
|
840
|
+
|
|
841
|
+
`.lintcn/` needs these generated files (created by `lintcn add` automatically):
|
|
842
|
+
|
|
843
|
+
**go.mod** — module name MUST be a child path of tsgolint for `internal/`
|
|
844
|
+
package access:
|
|
845
|
+
|
|
846
|
+
```
|
|
847
|
+
module github.com/typescript-eslint/tsgolint/lintcn-rules
|
|
848
|
+
|
|
849
|
+
go 1.26
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
**go.work** — workspace linking to cached tsgolint source:
|
|
853
|
+
|
|
854
|
+
```
|
|
855
|
+
go 1.26
|
|
856
|
+
|
|
857
|
+
use (
|
|
858
|
+
.
|
|
859
|
+
./.tsgolint
|
|
860
|
+
./.tsgolint/typescript-go
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
replace (
|
|
864
|
+
github.com/microsoft/typescript-go/shim/ast => ./.tsgolint/shim/ast
|
|
865
|
+
github.com/microsoft/typescript-go/shim/checker => ./.tsgolint/shim/checker
|
|
866
|
+
// ... all 14 shim modules
|
|
867
|
+
)
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
**.tsgolint/** — symlink to cached tsgolint clone (gitignored).
|
|
871
|
+
|
|
872
|
+
With this setup, gopls provides full autocomplete and go-to-definition on all
|
|
873
|
+
tsgolint and typescript-go APIs.
|