charness 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +11 -13
- package/DEVELOPMENT.md +27 -6
- package/MIGRATION_1_TO_2.md +1 -1
- package/MIGRATION_RENAME_TO_CHARNESS.md +133 -0
- package/README.md +185 -618
- package/SETUP.md +644 -0
- package/dist/adapters/adapterEventRouting.d.ts +2 -1
- package/dist/adapters/adapterEventRouting.d.ts.map +1 -1
- package/dist/adapters/adapterEventRouting.js +3 -0
- package/dist/adapters/adapterEventRouting.js.map +1 -1
- package/dist/adapters/adapterNames.d.ts +13 -0
- package/dist/adapters/adapterNames.d.ts.map +1 -1
- package/dist/adapters/adapterNames.js +16 -0
- package/dist/adapters/adapterNames.js.map +1 -1
- package/dist/adapters/backendUnavailableError.d.ts +11 -0
- package/dist/adapters/backendUnavailableError.d.ts.map +1 -0
- package/dist/adapters/backendUnavailableError.js +19 -0
- package/dist/adapters/backendUnavailableError.js.map +1 -0
- package/dist/adapters/claudeCliAdapter.d.ts +30 -7
- package/dist/adapters/claudeCliAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeCliAdapter.js +71 -17
- package/dist/adapters/claudeCliAdapter.js.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.d.ts +36 -3
- package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.js +95 -31
- package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
- package/dist/adapters/createAdapter.d.ts +3 -14
- package/dist/adapters/createAdapter.d.ts.map +1 -1
- package/dist/adapters/createAdapter.js +9 -19
- package/dist/adapters/createAdapter.js.map +1 -1
- package/dist/adapters/openCodeAdapter.d.ts +140 -12
- package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/openCodeAdapter.js +386 -120
- package/dist/adapters/openCodeAdapter.js.map +1 -1
- package/dist/apiErrorRetry.d.ts +70 -6
- package/dist/apiErrorRetry.d.ts.map +1 -1
- package/dist/apiErrorRetry.js +312 -34
- package/dist/apiErrorRetry.js.map +1 -1
- package/dist/apiRetryKick.d.ts +26 -1
- package/dist/apiRetryKick.d.ts.map +1 -1
- package/dist/apiRetryKick.js +41 -2
- package/dist/apiRetryKick.js.map +1 -1
- package/dist/bot.d.ts +2 -1
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +743 -190
- package/dist/bot.js.map +1 -1
- package/dist/cli/bot.d.ts +4 -2
- package/dist/cli/bot.d.ts.map +1 -1
- package/dist/cli/bot.js +11 -3
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli/botEntry.js +3 -1
- package/dist/cli/botEntry.js.map +1 -1
- package/dist/cli/claudeProfiles.d.ts +4 -4
- package/dist/cli/claudeProfiles.js +5 -5
- package/dist/cli/claudeProfiles.js.map +1 -1
- package/dist/cli/envLoader.d.ts +6 -5
- package/dist/cli/envLoader.d.ts.map +1 -1
- package/dist/cli/envLoader.js +8 -27
- package/dist/cli/envLoader.js.map +1 -1
- package/dist/cli/hot.d.ts +9 -3
- package/dist/cli/hot.d.ts.map +1 -1
- package/dist/cli/hot.js +55 -19
- package/dist/cli/hot.js.map +1 -1
- package/dist/cli.js +12 -12
- package/dist/cli.js.map +1 -1
- package/dist/configDirFile.d.ts +73 -0
- package/dist/configDirFile.d.ts.map +1 -0
- package/dist/configDirFile.js +216 -0
- package/dist/configDirFile.js.map +1 -0
- package/dist/connectors/jira/adf.d.ts +21 -0
- package/dist/connectors/jira/adf.d.ts.map +1 -1
- package/dist/connectors/jira/adf.js +39 -0
- package/dist/connectors/jira/adf.js.map +1 -1
- package/dist/connectors/jira/answerSink.d.ts +20 -3
- package/dist/connectors/jira/answerSink.d.ts.map +1 -1
- package/dist/connectors/jira/answerSink.js +126 -13
- package/dist/connectors/jira/answerSink.js.map +1 -1
- package/dist/connectors/jira/client.d.ts +4 -0
- package/dist/connectors/jira/client.d.ts.map +1 -1
- package/dist/connectors/jira/client.js +3 -0
- package/dist/connectors/jira/client.js.map +1 -1
- package/dist/connectors/jira/config.d.ts +10 -1
- package/dist/connectors/jira/config.d.ts.map +1 -1
- package/dist/connectors/jira/config.js +11 -2
- package/dist/connectors/jira/config.js.map +1 -1
- package/dist/connectors/jira/connector.d.ts +7 -1
- package/dist/connectors/jira/connector.d.ts.map +1 -1
- package/dist/connectors/jira/connector.js +23 -4
- package/dist/connectors/jira/connector.js.map +1 -1
- package/dist/connectors/jira/contextLedger.d.ts +6 -0
- package/dist/connectors/jira/contextLedger.d.ts.map +1 -1
- package/dist/connectors/jira/contextLedger.js +16 -0
- package/dist/connectors/jira/contextLedger.js.map +1 -1
- package/dist/connectors/jira/contextPreamble.d.ts.map +1 -1
- package/dist/connectors/jira/contextPreamble.js +6 -3
- package/dist/connectors/jira/contextPreamble.js.map +1 -1
- package/dist/connectors/jira/inbound.d.ts +60 -7
- package/dist/connectors/jira/inbound.d.ts.map +1 -1
- package/dist/connectors/jira/inbound.js +169 -16
- package/dist/connectors/jira/inbound.js.map +1 -1
- package/dist/connectors/jira/issueBlocks.d.ts +6 -1
- package/dist/connectors/jira/issueBlocks.d.ts.map +1 -1
- package/dist/connectors/jira/issueBlocks.js +5 -1
- package/dist/connectors/jira/issueBlocks.js.map +1 -1
- package/dist/connectors/jira/prompt.d.ts +2 -0
- package/dist/connectors/jira/prompt.d.ts.map +1 -1
- package/dist/connectors/jira/prompt.js +16 -0
- package/dist/connectors/jira/prompt.js.map +1 -1
- package/dist/connectors/jira/trigger.d.ts +12 -3
- package/dist/connectors/jira/trigger.d.ts.map +1 -1
- package/dist/connectors/jira/trigger.js +8 -1
- package/dist/connectors/jira/trigger.js.map +1 -1
- package/dist/connectors/jira/triggerLog.d.ts +10 -3
- package/dist/connectors/jira/triggerLog.d.ts.map +1 -1
- package/dist/connectors/jira/triggerLog.js +18 -2
- package/dist/connectors/jira/triggerLog.js.map +1 -1
- package/dist/connectors/telegram/commands/autoContinueLimits.d.ts +2 -2
- package/dist/connectors/telegram/commands/autoContinueLimits.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/autoContinueLimits.js +4 -4
- package/dist/connectors/telegram/commands/autoContinueLimits.js.map +1 -1
- package/dist/connectors/telegram/commands/backendSwitch.d.ts +25 -0
- package/dist/connectors/telegram/commands/backendSwitch.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/backendSwitch.js +104 -0
- package/dist/connectors/telegram/commands/backendSwitch.js.map +1 -0
- package/dist/connectors/telegram/commands/callbackMessage.d.ts +12 -0
- package/dist/connectors/telegram/commands/callbackMessage.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/callbackMessage.js +24 -0
- package/dist/connectors/telegram/commands/callbackMessage.js.map +1 -1
- package/dist/connectors/telegram/commands/callbackToast.d.ts +36 -0
- package/dist/connectors/telegram/commands/callbackToast.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/callbackToast.js +83 -0
- package/dist/connectors/telegram/commands/callbackToast.js.map +1 -0
- package/dist/connectors/telegram/commands/claudeAccounts.d.ts +18 -0
- package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/claudeAccounts.js +220 -66
- package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -1
- package/dist/connectors/telegram/commands/displayModes.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/displayModes.js +4 -2
- package/dist/connectors/telegram/commands/displayModes.js.map +1 -1
- package/dist/connectors/telegram/commands/fallbackChain.d.ts +5 -8
- package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/fallbackChain.js +183 -193
- package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
- package/dist/connectors/telegram/commands/limitsBoard.d.ts +40 -0
- package/dist/connectors/telegram/commands/limitsBoard.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/limitsBoard.js +281 -0
- package/dist/connectors/telegram/commands/limitsBoard.js.map +1 -0
- package/dist/connectors/telegram/commands/modelPage.d.ts +130 -0
- package/dist/connectors/telegram/commands/modelPage.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/modelPage.js +120 -0
- package/dist/connectors/telegram/commands/modelPage.js.map +1 -0
- package/dist/connectors/telegram/commands/modelProviders.d.ts +9 -1
- package/dist/connectors/telegram/commands/modelProviders.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/modelProviders.js +203 -111
- package/dist/connectors/telegram/commands/modelProviders.js.map +1 -1
- package/dist/connectors/telegram/commands/stt.d.ts +32 -0
- package/dist/connectors/telegram/commands/stt.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/stt.js +460 -0
- package/dist/connectors/telegram/commands/stt.js.map +1 -0
- package/dist/effortLevels.d.ts +57 -7
- package/dist/effortLevels.d.ts.map +1 -1
- package/dist/effortLevels.js +65 -9
- package/dist/effortLevels.js.map +1 -1
- package/dist/i18n/de.d.ts.map +1 -1
- package/dist/i18n/de.js +160 -17
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +160 -17
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +160 -17
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/fr.js +160 -17
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/hi.d.ts.map +1 -1
- package/dist/i18n/hi.js +160 -17
- package/dist/i18n/hi.js.map +1 -1
- package/dist/i18n/ja.d.ts.map +1 -1
- package/dist/i18n/ja.js +160 -17
- package/dist/i18n/ja.js.map +1 -1
- package/dist/i18n/ka.d.ts.map +1 -1
- package/dist/i18n/ka.js +160 -17
- package/dist/i18n/ka.js.map +1 -1
- package/dist/i18n/pt.d.ts.map +1 -1
- package/dist/i18n/pt.js +160 -17
- package/dist/i18n/pt.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +160 -17
- package/dist/i18n/ru.js.map +1 -1
- package/dist/i18n/uk.d.ts.map +1 -1
- package/dist/i18n/uk.js +160 -17
- package/dist/i18n/uk.js.map +1 -1
- package/dist/i18n/uz.d.ts.map +1 -1
- package/dist/i18n/uz.js +160 -17
- package/dist/i18n/uz.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +160 -17
- package/dist/i18n/zh.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/installManager.d.ts +36 -6
- package/dist/installManager.d.ts.map +1 -1
- package/dist/installManager.js +124 -26
- package/dist/installManager.js.map +1 -1
- package/dist/instanceLock.d.ts.map +1 -1
- package/dist/instanceLock.js +10 -8
- package/dist/instanceLock.js.map +1 -1
- package/dist/instanceSettings.d.ts +52 -0
- package/dist/instanceSettings.d.ts.map +1 -0
- package/dist/instanceSettings.js +136 -0
- package/dist/instanceSettings.js.map +1 -0
- package/dist/legacyMigration.d.ts +69 -0
- package/dist/legacyMigration.d.ts.map +1 -0
- package/dist/legacyMigration.js +338 -0
- package/dist/legacyMigration.js.map +1 -0
- package/dist/legacyNames.d.ts +107 -0
- package/dist/legacyNames.d.ts.map +1 -0
- package/dist/legacyNames.js +230 -0
- package/dist/legacyNames.js.map +1 -0
- package/dist/pinnedStatus.d.ts +7 -1
- package/dist/pinnedStatus.d.ts.map +1 -1
- package/dist/pinnedStatus.js +1 -1
- package/dist/pinnedStatus.js.map +1 -1
- package/dist/platform/answerSink.d.ts +5 -0
- package/dist/platform/answerSink.d.ts.map +1 -1
- package/dist/platform/answerSink.js.map +1 -1
- package/dist/requests/answerRequest.d.ts +2 -0
- package/dist/requests/answerRequest.d.ts.map +1 -1
- package/dist/requests/answerRequest.js +3 -1
- package/dist/requests/answerRequest.js.map +1 -1
- package/dist/requests/answerStyle.d.ts +11 -0
- package/dist/requests/answerStyle.d.ts.map +1 -0
- package/dist/requests/answerStyle.js +14 -0
- package/dist/requests/answerStyle.js.map +1 -0
- package/dist/requests/answerTail.d.ts.map +1 -1
- package/dist/requests/answerTail.js +2 -0
- package/dist/requests/answerTail.js.map +1 -1
- package/dist/requests/requestHeader.d.ts.map +1 -1
- package/dist/requests/requestHeader.js +3 -0
- package/dist/requests/requestHeader.js.map +1 -1
- package/dist/requests/requestLedger.d.ts +8 -0
- package/dist/requests/requestLedger.d.ts.map +1 -1
- package/dist/requests/requestLedger.js +20 -2
- package/dist/requests/requestLedger.js.map +1 -1
- package/dist/requests/types.d.ts +3 -1
- package/dist/requests/types.d.ts.map +1 -1
- package/dist/scheduler/mcpSurface.d.ts +9 -1
- package/dist/scheduler/mcpSurface.d.ts.map +1 -1
- package/dist/scheduler/mcpSurface.js +49 -16
- package/dist/scheduler/mcpSurface.js.map +1 -1
- package/dist/shutdown.d.ts +14 -0
- package/dist/shutdown.d.ts.map +1 -1
- package/dist/shutdown.js +16 -0
- package/dist/shutdown.js.map +1 -1
- package/dist/state.d.ts +57 -3
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +67 -3
- package/dist/state.js.map +1 -1
- package/dist/stt/sttAutoInstall.d.ts +39 -0
- package/dist/stt/sttAutoInstall.d.ts.map +1 -0
- package/dist/stt/sttAutoInstall.js +158 -0
- package/dist/stt/sttAutoInstall.js.map +1 -0
- package/dist/stt/sttChildEnv.d.ts +11 -0
- package/dist/stt/sttChildEnv.d.ts.map +1 -0
- package/dist/stt/sttChildEnv.js +30 -0
- package/dist/stt/sttChildEnv.js.map +1 -0
- package/dist/stt/sttConfigEdit.d.ts +71 -0
- package/dist/stt/sttConfigEdit.d.ts.map +1 -0
- package/dist/stt/sttConfigEdit.js +144 -0
- package/dist/stt/sttConfigEdit.js.map +1 -0
- package/dist/stt/sttInstall.d.ts +147 -0
- package/dist/stt/sttInstall.d.ts.map +1 -0
- package/dist/stt/sttInstall.js +491 -0
- package/dist/stt/sttInstall.js.map +1 -0
- package/dist/stt/sttInstallEntry.d.ts +2 -0
- package/dist/stt/sttInstallEntry.d.ts.map +1 -0
- package/dist/stt/sttInstallEntry.js +31 -0
- package/dist/stt/sttInstallEntry.js.map +1 -0
- package/dist/stt/sttInstallReporter.d.ts +45 -0
- package/dist/stt/sttInstallReporter.d.ts.map +1 -0
- package/dist/stt/sttInstallReporter.js +57 -0
- package/dist/stt/sttInstallReporter.js.map +1 -0
- package/dist/stt/sttRuntime.d.ts +35 -0
- package/dist/stt/sttRuntime.d.ts.map +1 -0
- package/dist/stt/sttRuntime.js +73 -0
- package/dist/stt/sttRuntime.js.map +1 -0
- package/dist/stt/sttScreenPlan.d.ts +193 -0
- package/dist/stt/sttScreenPlan.d.ts.map +1 -0
- package/dist/stt/sttScreenPlan.js +541 -0
- package/dist/stt/sttScreenPlan.js.map +1 -0
- package/dist/stt/sttSettings.d.ts +201 -0
- package/dist/stt/sttSettings.d.ts.map +1 -0
- package/dist/stt/sttSettings.js +387 -0
- package/dist/stt/sttSettings.js.map +1 -0
- package/dist/stt/sttSidecarOwner.d.ts +115 -0
- package/dist/stt/sttSidecarOwner.d.ts.map +1 -0
- package/dist/stt/sttSidecarOwner.js +475 -0
- package/dist/stt/sttSidecarOwner.js.map +1 -0
- package/dist/stt/transcribeChain.d.ts +36 -0
- package/dist/stt/transcribeChain.d.ts.map +1 -0
- package/dist/stt/transcribeChain.js +42 -0
- package/dist/stt/transcribeChain.js.map +1 -0
- package/dist/types.d.ts +77 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/adapterSwitchSessionIds.d.ts +32 -0
- package/dist/utils/adapterSwitchSessionIds.d.ts.map +1 -0
- package/dist/utils/adapterSwitchSessionIds.js +44 -0
- package/dist/utils/adapterSwitchSessionIds.js.map +1 -0
- package/dist/utils/backendSwitchPlan.d.ts +63 -0
- package/dist/utils/backendSwitchPlan.d.ts.map +1 -0
- package/dist/utils/backendSwitchPlan.js +73 -0
- package/dist/utils/backendSwitchPlan.js.map +1 -0
- package/dist/utils/claudeAccountPickerPlan.d.ts +12 -1
- package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -1
- package/dist/utils/claudeAccountPickerPlan.js +20 -1
- package/dist/utils/claudeAccountPickerPlan.js.map +1 -1
- package/dist/utils/claudeProfiles.d.ts +7 -0
- package/dist/utils/claudeProfiles.d.ts.map +1 -1
- package/dist/utils/claudeProfiles.js +19 -0
- package/dist/utils/claudeProfiles.js.map +1 -1
- package/dist/utils/claudeTranscriptContent.d.ts +24 -0
- package/dist/utils/claudeTranscriptContent.d.ts.map +1 -0
- package/dist/utils/claudeTranscriptContent.js +107 -0
- package/dist/utils/claudeTranscriptContent.js.map +1 -0
- package/dist/utils/claudeUsage.d.ts +47 -8
- package/dist/utils/claudeUsage.d.ts.map +1 -1
- package/dist/utils/claudeUsage.js +76 -14
- package/dist/utils/claudeUsage.js.map +1 -1
- package/dist/utils/claudeUsageRefresher.d.ts +8 -1
- package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
- package/dist/utils/claudeUsageRefresher.js +9 -4
- package/dist/utils/claudeUsageRefresher.js.map +1 -1
- package/dist/utils/effortPickerPlan.d.ts +54 -0
- package/dist/utils/effortPickerPlan.d.ts.map +1 -0
- package/dist/utils/effortPickerPlan.js +56 -0
- package/dist/utils/effortPickerPlan.js.map +1 -0
- package/dist/utils/ensureSessionPlan.d.ts +20 -4
- package/dist/utils/ensureSessionPlan.d.ts.map +1 -1
- package/dist/utils/ensureSessionPlan.js +17 -4
- package/dist/utils/ensureSessionPlan.js.map +1 -1
- package/dist/utils/fallbackBridge.d.ts +6 -2
- package/dist/utils/fallbackBridge.d.ts.map +1 -1
- package/dist/utils/fallbackBridge.js +10 -3
- package/dist/utils/fallbackBridge.js.map +1 -1
- package/dist/utils/fallbackChain.d.ts +36 -0
- package/dist/utils/fallbackChain.d.ts.map +1 -1
- package/dist/utils/fallbackChain.js +65 -1
- package/dist/utils/fallbackChain.js.map +1 -1
- package/dist/utils/fallbackSwitchNotice.d.ts +41 -0
- package/dist/utils/fallbackSwitchNotice.d.ts.map +1 -0
- package/dist/utils/fallbackSwitchNotice.js +75 -0
- package/dist/utils/fallbackSwitchNotice.js.map +1 -0
- package/dist/utils/fileSendService.d.ts +33 -10
- package/dist/utils/fileSendService.d.ts.map +1 -1
- package/dist/utils/fileSendService.js +78 -27
- package/dist/utils/fileSendService.js.map +1 -1
- package/dist/utils/jsonStreamHost.d.ts +39 -0
- package/dist/utils/jsonStreamHost.d.ts.map +1 -1
- package/dist/utils/jsonStreamHost.js +60 -1
- package/dist/utils/jsonStreamHost.js.map +1 -1
- package/dist/utils/limitEpisodeRecovery.d.ts +16 -2
- package/dist/utils/limitEpisodeRecovery.d.ts.map +1 -1
- package/dist/utils/limitEpisodeRecovery.js +44 -3
- package/dist/utils/limitEpisodeRecovery.js.map +1 -1
- package/dist/utils/limitSettings.d.ts +25 -0
- package/dist/utils/limitSettings.d.ts.map +1 -0
- package/dist/utils/limitSettings.js +31 -0
- package/dist/utils/limitSettings.js.map +1 -0
- package/dist/utils/limitsBoardPlan.d.ts +52 -0
- package/dist/utils/limitsBoardPlan.d.ts.map +1 -0
- package/dist/utils/limitsBoardPlan.js +93 -0
- package/dist/utils/limitsBoardPlan.js.map +1 -0
- package/dist/utils/openCodeCompactPlugin.d.ts +11 -3
- package/dist/utils/openCodeCompactPlugin.d.ts.map +1 -1
- package/dist/utils/openCodeCompactPlugin.js +37 -5
- package/dist/utils/openCodeCompactPlugin.js.map +1 -1
- package/dist/utils/openCodeUsage.d.ts +93 -0
- package/dist/utils/openCodeUsage.d.ts.map +1 -0
- package/dist/utils/openCodeUsage.js +248 -0
- package/dist/utils/openCodeUsage.js.map +1 -0
- package/dist/utils/operatorSwitchPlan.d.ts +61 -0
- package/dist/utils/operatorSwitchPlan.d.ts.map +1 -0
- package/dist/utils/operatorSwitchPlan.js +32 -0
- package/dist/utils/operatorSwitchPlan.js.map +1 -0
- package/dist/utils/promptDelivery.d.ts +40 -0
- package/dist/utils/promptDelivery.d.ts.map +1 -1
- package/dist/utils/promptDelivery.js +31 -0
- package/dist/utils/promptDelivery.js.map +1 -1
- package/dist/utils/recvPreviewRedaction.d.ts +14 -4
- package/dist/utils/recvPreviewRedaction.d.ts.map +1 -1
- package/dist/utils/recvPreviewRedaction.js +19 -7
- package/dist/utils/recvPreviewRedaction.js.map +1 -1
- package/dist/utils/startupReadiness.d.ts +3 -3
- package/dist/utils/startupReadiness.d.ts.map +1 -1
- package/dist/utils/startupReadiness.js +2 -2
- package/dist/utils/startupReadiness.js.map +1 -1
- package/dist/utils/threadStatusReport.d.ts +3 -1
- package/dist/utils/threadStatusReport.d.ts.map +1 -1
- package/dist/utils/threadStatusReport.js +4 -3
- package/dist/utils/threadStatusReport.js.map +1 -1
- package/dist/utils/timezone.d.ts +23 -0
- package/dist/utils/timezone.d.ts.map +1 -1
- package/dist/utils/timezone.js +38 -0
- package/dist/utils/timezone.js.map +1 -1
- package/dist/utils/transcribeAudio.d.ts +25 -12
- package/dist/utils/transcribeAudio.d.ts.map +1 -1
- package/dist/utils/transcribeAudio.js +39 -21
- package/dist/utils/transcribeAudio.js.map +1 -1
- package/dist/utils/usageRefreshBudget.d.ts +9 -0
- package/dist/utils/usageRefreshBudget.d.ts.map +1 -0
- package/dist/utils/usageRefreshBudget.js +27 -0
- package/dist/utils/usageRefreshBudget.js.map +1 -0
- package/examples/.env.example +13 -9
- package/examples/docker-compose.yml +20 -17
- package/package.json +4 -1
- package/stt/gigaam_sidecar.py +800 -0
package/README.md
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
<h1 align="center">Charness</h1>
|
|
2
2
|
|
|
3
|
-
<p align="center"
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/charness"><img src="https://img.shields.io/npm/v/charness?logo=npm" alt="npm version"></a>
|
|
5
|
+
<a href="https://www.npmjs.com/package/charness"><img src="https://img.shields.io/node/v/charness?logo=node.js" alt="Node.js version"></a>
|
|
6
|
+
<a href="https://github.com/olosegres/charness/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/charness" alt="license"></a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<h2 align="center">Run multiple Claude Code / OpenCode agents and connect them to your Telegram or Jira.</h2>
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
- **In Telegram** you control your agents through a bot or a group, so it becomes a terminal for OpenCode and Claude Code — every thread is like a terminal tab, plus scheduling and voice control.
|
|
12
|
+
- **In Jira** it starts an agent when you assign a ticket to a special user: the agent reads the description, comments and attachments and does the work according to the ticket's status — like a real employee.
|
|
13
|
+
|
|
14
|
+
<p align="center">It runs fine on your laptop. But it is called a <b>c</b>loud <b>harness</b> for a reason — put it on a server and your agents keep working while the laptop sleeps.</p>
|
|
6
15
|
|
|
7
16
|
<p align="center">
|
|
8
17
|
<img src="docs/images/hero-v2.png" alt="Charness — all your agents in Telegram; chat, code, ship; powerful slash commands" width="900" />
|
|
9
18
|
</p>
|
|
10
19
|
|
|
20
|
+
## What it does
|
|
21
|
+
|
|
22
|
+
### Common (any surface)
|
|
23
|
+
|
|
24
|
+
These hold whichever surface you drive it from — the same core serves all of them:
|
|
25
|
+
|
|
26
|
+
- **Agent backends** — OpenCode (native server API over HTTP+SSE) and Claude Code (three backends — json-stream, per-turn and tmux)
|
|
27
|
+
- **Sleeping agents** — a Claude json-stream topic's process is stopped when idle (or after every turn in per-turn mode) and the next message resumes the same conversation by itself, prompt cache still warm
|
|
28
|
+
- **Several Claude accounts** — keep more than one Claude subscription signed in side by side and choose per topic which one a session runs on (`/accounts`); every account's usage windows are shown next to it, and `/limits` is a one-tap switch board over all of them and over the OpenCode providers. See [Several accounts](#several-accounts-accounts)
|
|
29
|
+
- **Waits out usage limits** — when the agent hits a usage / session limit the bot reads the reset time out of the error (honouring the timezone the provider quotes; a mere warning such as `Approaching session limit · resets …` or the switch onto extra usage is not a limit), waits, then tells the agent to continue by itself and pins the "work resumed" message so a muted topic still notifies you; `/limits_autoswitch` turns waiting off per topic and `/limits` skips a single resume. With a **fallback chain** (`/limits_autoswitch`: other Claude accounts, OpenCode providers and models, in your order of preference) the limit notice offers «🔁 Switch to …» buttons to move the topic to the next entry with room and continue at once — or the bot does it by itself when you turn switching on; across backends the new agent gets a context bridge with the previous one's last turns
|
|
30
|
+
- **Restart-safe** — agents run as external processes (tmux, `opencode serve`) that the bot re-adopts after a restart instead of killing them; a prompt sent while a session boots is buffered and replayed. See [Restart behaviour](#restart-behaviour)
|
|
31
|
+
|
|
32
|
+
### Telegram
|
|
33
|
+
|
|
11
34
|
Charness turns Telegram into a terminal for running agentic CLIs. Open as
|
|
12
35
|
many tabs as you need — even two on the same project for parallel work — and
|
|
13
36
|
drive the agents from your phone or tablet, voice messages included.
|
|
@@ -16,27 +39,22 @@ Think **OpenClaw for vibe coding**, driven entirely from Telegram: no complex
|
|
|
16
39
|
setup, no extra dashboards — direct access to your own **OpenCode** /
|
|
17
40
|
**Claude Code** running on your machine or server.
|
|
18
41
|
|
|
19
|
-
### Features
|
|
20
|
-
|
|
21
42
|
- **Simple setup & management** — install the CLI, put your bot token in one `.env`, bind a topic to a folder, and run; everything after is managed from Telegram — no dashboards
|
|
22
43
|
- **Multi-thread / topic** — one topic per directory with your agent; two topics can share one folder for parallel work — run almost unlimited agents in parallel
|
|
23
44
|
- **Two chat surfaces** — forum-group topics and the owner's bot DM, both served at once by default, with tabs on either
|
|
24
|
-
- **Agent backends** — OpenCode (native server API over HTTP+SSE) and Claude Code (three backends — json-stream, per-turn and tmux)
|
|
25
|
-
- **Sleeping agents** — a Claude json-stream topic's process is stopped when idle (or after every turn in per-turn mode) and the next message resumes the same conversation by itself, prompt cache still warm
|
|
26
45
|
- **Raw terminal** — `/terminal` binds a topic to a real `$SHELL` in the project folder
|
|
27
46
|
- **Notifications** — when the agent asks a question, the bot pins that message so even a muted topic notifies you: the pin pierces mute, giving exactly one notification per question (unpinned once you answer)
|
|
28
47
|
- **Scheduled & self-driving runs** — `/schedule` arms cron / one-shot / N-times jobs per topic, and the agent can schedule *itself* via injected MCP tools: nightly reviews, recurring reports, a "finish this tomorrow at 9" hand-off. Each job fires as a fresh session with your prompt; restart-safe, with one catch-up for a run missed while the bot was down
|
|
29
48
|
- **Reminders** — `/reminders` sets a plain reminder with inline buttons (how often, which day, what time, then the text by typing or voice); at the due time the bot posts and pins it itself, so even a muted topic notifies you. No agent, no bound folder, no date parsing — works in any topic, General included
|
|
30
49
|
- **Inbound files** — photos / documents / video / audio sent to a topic are saved and announced to the agent; albums arrive as one prompt
|
|
31
50
|
- **Outbound files** — the agent sends files back to you — a generated chart, a screenshot, a log, a rendered PDF — single files or albums, straight into the topic
|
|
32
|
-
- **Voice input** —
|
|
51
|
+
- **Voice input** — voice notes are transcribed by an ordered chain of engines — Groq, OpenAI or your own OpenAI-compatible server — set up in `/stt` (or `stt.json`); a local GigaAM engine is optional; if one fails, the next takes over
|
|
33
52
|
- **Display verbosity and view** — `/verbosity` (plus `/thinking`, `/tool_results`, `/subagent`) per topic: detail `minimal|short|full`, and what the topic shows — `stream` (default), `stream_answers` or `answers` (only the agent's pinned answers, no stream)
|
|
34
53
|
- **Time-aware prompts** — `/timestamps on` prepends each forwarded prompt's send time (local-offset ISO), so a days-long session knows what "yesterday" or "2 days ago" means; agent-facing only, per topic
|
|
35
54
|
- **Reply context** — reply to a message in a topic and the quoted text is folded into the prompt the agent receives, so it sees what you point at without re-pasting; works for text, voice and files (a photo, a document, an album…), both agents; a reply TO a photo, file or voice note names it, with the path of the bot's saved copy when it has one
|
|
36
55
|
- **Your timezone** — `/timezone Europe/Moscow` once, and schedules fire at your wall-clock time while the agent is told what time it actually is; no server-clock guessing
|
|
37
|
-
- **Waits out usage limits** — when the agent hits a usage / session limit the bot reads the reset time out of the error (honouring the timezone the provider quotes), waits, then tells the agent to continue by itself and pins the "work resumed" message so a muted topic still notifies you; `/limits` turns waiting off per topic or skips a single resume. With a **fallback chain** (`/limits`: other Claude accounts, OpenCode providers and models, in your order of preference) the limit notice offers «🔁 Switch to …» buttons to move the topic to the next entry with room and continue at once — or the bot does it by itself when you turn switching on; across backends the new agent gets a context bridge with the previous one's last turns
|
|
38
56
|
|
|
39
|
-
|
|
57
|
+
#### Two surfaces: group topics, bot DM, or both
|
|
40
58
|
|
|
41
59
|
The bot serves two Telegram surfaces:
|
|
42
60
|
|
|
@@ -50,289 +68,22 @@ The bot serves two Telegram surfaces:
|
|
|
50
68
|
the reply grows in place in one message instead of arriving as a flood of
|
|
51
69
|
separate messages.
|
|
52
70
|
|
|
53
|
-
##
|
|
54
|
-
|
|
55
|
-
The bot runs anywhere the agent CLIs run:
|
|
56
|
-
|
|
57
|
-
- **Your own working machine** — the simplest start: launch `telegramcode`
|
|
58
|
-
from your projects parent folder and your laptop/desktop becomes the
|
|
59
|
-
backend. The catch: agents are only reachable while the machine is on and
|
|
60
|
-
awake.
|
|
61
|
-
- **A remote server (cloud VPS/VDS or bare metal)** — the more convenient
|
|
62
|
-
setup: organize your working environment there once (git + your repos,
|
|
63
|
-
Claude Code / OpenCode, `telegramcode`) and you get an always-on dev box you
|
|
64
|
-
drive from any device through Telegram — phone, tablet, voice. When picking
|
|
65
|
-
one, a bare-metal server without virtualization beats a virtualized VPS/VDS:
|
|
66
|
-
its NVMe drives are usually much faster, which matters once you also build
|
|
67
|
-
and run a dev server there, not just the agents. Classic terminal access to
|
|
68
|
-
the very same Claude sessions stays available over SSH — run plain `claude`
|
|
69
|
-
in the project folder: it reads the same `~/.claude/projects/<cwd-slug>/`
|
|
70
|
-
session store the bot uses, so a session started in a Telegram thread can
|
|
71
|
-
be picked up in the terminal and vice versa.
|
|
72
|
-
|
|
73
|
-
## Quick Start
|
|
74
|
-
|
|
75
|
-
Five steps: install the CLI, create a bot, (optionally) set up a group, launch
|
|
76
|
-
from your projects folder, bind a topic to an agent.
|
|
77
|
-
|
|
78
|
-
> **[manual]** tags a step you must do by hand in the Telegram app — an agent
|
|
79
|
-
> running on the host can't do it for you. Everything unmarked is a host-side
|
|
80
|
-
> command (or file edit) that can be automated.
|
|
81
|
-
|
|
82
|
-
### 1. Install the CLI
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
npm install -g charness # needs Node ≥ 22.12
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
This registers the `telegramcode` command (it keeps the project's former name;
|
|
89
|
-
`charness` works too). Coming from the old `telegramcode` npm package (2.x)?
|
|
90
|
-
Run `npm uninstall -g telegramcode` first: both install the same commands. The
|
|
91
|
-
config, `DATA_DIR` and sessions carry over as they are.
|
|
92
|
-
|
|
93
|
-
Prefer the whole bot in a container, isolated from the host? Use
|
|
94
|
-
[Run with Docker](#run-with-docker) instead — the bot and group steps are the
|
|
95
|
-
same. To hack on the bot itself, see [Run from source](#run-from-source).
|
|
96
|
-
|
|
97
|
-
### 2. Create the bot
|
|
98
|
-
|
|
99
|
-
**[manual]** In Telegram, with @BotFather:
|
|
100
|
-
|
|
101
|
-
1. Message [@BotFather](https://t.me/BotFather), send `/newbot`, follow prompts.
|
|
102
|
-
2. Save the token (`123456789:ABCdef...`).
|
|
103
|
-
3. **Disable privacy mode** — same chat: `/setprivacy` → pick this bot →
|
|
104
|
-
`Disable`. Without this the bot only sees `/commands`, not free-form text.
|
|
105
|
-
|
|
106
|
-
<p><img src="docs/images/01-botfather.png" width="360" alt="@BotFather: /newbot to get the token, then /setprivacy → this bot → Disable" /></p>
|
|
107
|
-
|
|
108
|
-
Put the token where the bot reads it — the global config `.env`:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
mkdir -p ~/.config/telegramcode
|
|
112
|
-
$EDITOR ~/.config/telegramcode/.env
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
```dotenv
|
|
116
|
-
# ~/.config/telegramcode/.env — the one place your keys live
|
|
117
|
-
TELEGRAM_BOT_TOKEN=123456789:ABCdef...
|
|
118
|
-
# GROQ_API_KEY=... # optional: free voice transcription
|
|
119
|
-
# OWNER_USER_ID=... # set to drive the bot from its DM (step 3)
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
All keys the bot needs live in this one file — the bot token and the optional
|
|
123
|
-
`GROQ_API_KEY` for voice sit side by side, no separate stores. (Agent
|
|
124
|
-
provider auth is the exception, and lives inside the agents themselves:
|
|
125
|
-
`claude login` for Claude, `/connect` or OpenCode's own config for OpenCode —
|
|
126
|
-
see [Environment Variables](#environment-variables).) The wrapper loads env
|
|
127
|
-
from two locations, in order — a global base and an optional per-project
|
|
128
|
-
override:
|
|
129
|
-
|
|
130
|
-
1. `~/.config/telegramcode/.env` — base, set once, used everywhere.
|
|
131
|
-
2. `$PWD/.env` — per-project tweaks (last write wins).
|
|
132
|
-
|
|
133
|
-
The full annotated template is `.env.example` (shipped with the package and in
|
|
134
|
-
the repo).
|
|
135
|
-
|
|
136
|
-
### 3. Set up a group (optional) or DM
|
|
137
|
-
|
|
138
|
-
The bot serves a **forum supergroup**, the **owner's private chat (DM)**, or
|
|
139
|
-
both at once (`CHAT_MODE`, default `both`). The group is the nicest UX — a
|
|
140
|
-
visible topic list, per-topic names/icons, quick agent switching — but it is
|
|
141
|
-
**optional**: with `OWNER_USER_ID` set you can run entirely from the bot's DM.
|
|
142
|
-
|
|
143
|
-
**To use a group (recommended)** — **[manual]**, all in the Telegram client:
|
|
144
|
-
|
|
145
|
-
1. In a Telegram client: `New Group` → name it → add the bot.
|
|
146
|
-
2. Open group settings → enable **Topics** (Forum mode).
|
|
147
|
-
3. Promote the bot to admin with these rights:
|
|
148
|
-
- `Manage Topics` (required — bind/create threads)
|
|
149
|
-
- `Delete Messages` (for `/clear_messages`)
|
|
150
|
-
- `Pin Messages` (per-thread status banner + question alerts; `/doctor`
|
|
151
|
-
warns if missing, but the bot still runs without it)
|
|
152
|
-
4. **Remove the bot from the group and add it again.** Telegram caches the
|
|
153
|
-
privacy-mode flag on join; without re-adding, the bot keeps private-mode and
|
|
154
|
-
ignores free-form messages (see
|
|
155
|
-
[Troubleshooting → Bot doesn't see text](#bot-doesnt-see-text)).
|
|
156
|
-
|
|
157
|
-
<p>
|
|
158
|
-
<img src="docs/images/02-enable-topics.png" width="360" alt="Group settings → Topics toggle enabled (Forum mode)" />
|
|
159
|
-
<img src="docs/images/03-promote-admin.png" width="360" alt="Bot admin rights: Manage Topics, Delete Messages, Pin Messages ticked" />
|
|
160
|
-
</p>
|
|
161
|
-
|
|
162
|
-
The group **id is automatic** — leave `ALLOWED_GROUP_ID` empty and the bot
|
|
163
|
-
auto-pairs with the first forum supergroup an admin talks to it from (the
|
|
164
|
-
`-100…` id is saved to `state.json`; re-point later with `/pair`). To pin a
|
|
165
|
-
specific id by hand, set `ALLOWED_GROUP_ID` to the numeric value (a group
|
|
166
|
-
**name** is not accepted) — that also disables auto-pairing. **Access control:**
|
|
167
|
-
whoever is a creator/administrator of that group may drive the bot — there is no
|
|
168
|
-
separate allow-list.
|
|
169
|
-
|
|
170
|
-
**To use the DM instead (or as well):** **[manual]** send `/start` to
|
|
171
|
-
[@userinfobot](https://t.me/userinfobot) for your numeric user id, then add it
|
|
172
|
-
(host-side) to the same `.env`:
|
|
173
|
-
|
|
174
|
-
```dotenv
|
|
175
|
-
OWNER_USER_ID=987654321
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
With the default `CHAT_MODE=both`, an unset `OWNER_USER_ID` just leaves the DM
|
|
179
|
-
surface inert (group-only); set it and the DM lights up alongside the group. Use
|
|
180
|
-
`CHAT_MODE=dm` to skip the group entirely.
|
|
181
|
-
|
|
182
|
-
### 4. Launch from your projects folder
|
|
183
|
-
|
|
184
|
-
```bash
|
|
185
|
-
cd ~/projects && telegramcode
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
That `$PWD` — the parent folder holding your repositories — is the folder the
|
|
189
|
-
bot works in; each topic binds to a subfolder under it. There's nothing else to
|
|
190
|
-
configure for it. A single-instance lockfile (`$DATA_DIR/instance.lock`) stops a
|
|
191
|
-
second bot starting under the same user; stale locks (after `kill -9`) are
|
|
192
|
-
reclaimed automatically.
|
|
193
|
-
|
|
194
|
-
### 5. Bind a topic and talk to an agent
|
|
195
|
-
|
|
196
|
-
This step is **[manual]** — it all happens inside the Telegram app.
|
|
197
|
-
|
|
198
|
-
**In the group:**
|
|
199
|
-
|
|
200
|
-
1. Open the group; in the General topic send `/ls` — the bot lists subfolders
|
|
201
|
-
under the work root.
|
|
202
|
-
2. Create a topic (the `+` button).
|
|
203
|
-
3. In the new topic: `/bind <subdir>` (auto-bound if the topic name matches a
|
|
204
|
-
subdir).
|
|
205
|
-
4. `/claude` or `/opencode` → talk to the agent (text or voice).
|
|
206
|
-
|
|
207
|
-
<p><img src="docs/images/04-bind-and-run.png" width="360" alt="A topic bound with /bind, /claude started, and the agent replying" /></p>
|
|
71
|
+
## Quick start
|
|
208
72
|
|
|
209
|
-
|
|
210
|
-
|
|
73
|
+
1. `npm install -g charness` (needs Node ≥ 22.12).
|
|
74
|
+
2. Put your bot token (from [@BotFather](https://t.me/BotFather)) into `~/.config/charness/.env` as
|
|
75
|
+
`TELEGRAM_BOT_TOKEN=…`.
|
|
76
|
+
3. Run `charness` from the folder that holds your projects: `cd ~/projects && charness`.
|
|
77
|
+
4. In your Telegram group (or the bot's DM): `/bind <subfolder>` in a topic, then `/claude` or `/opencode` and
|
|
78
|
+
talk to the agent — text or voice.
|
|
211
79
|
|
|
212
|
-
|
|
213
|
-
`/clear_messages` deletes the topic's bot messages.
|
|
80
|
+
Full setup — Telegram, Jira, environment, updating: [SETUP.md](SETUP.md)
|
|
214
81
|
|
|
215
|
-
|
|
216
|
-
> `claude --dangerously-skip-permissions` in the project folder. It is the same
|
|
217
|
-
> binary and the same `~/.claude/projects/<cwd-slug>/` session store the bot
|
|
218
|
-
> uses, so a thread started in Telegram can be picked up with `claude --resume`
|
|
219
|
-
> and vice versa.
|
|
82
|
+
## Reference
|
|
220
83
|
|
|
221
|
-
###
|
|
84
|
+
### Commands (Telegram)
|
|
222
85
|
|
|
223
|
-
|
|
224
|
-
`/terminal`, OpenCode and its server) and the bot's MCP server. Everything works as on a host; the host only
|
|
225
|
-
lends the container the folders it mounts, so the bot and its agents see nothing else of the machine.
|
|
226
|
-
|
|
227
|
-
```
|
|
228
|
-
host container telegramcode-<instance>
|
|
229
|
-
your instance env file (tokens) ── ro ──▶ the bot (hot mode) + a restart loop
|
|
230
|
-
DATA_DIR, WORK_ROOT ──────▶ tmux, the agents, OpenCode, the bot MCP
|
|
231
|
-
a clone of this repository ──────▶ /opt/telegramcode
|
|
232
|
-
the agents' home folder ──────▶ /home/telegramcode: their Claude / OpenCode
|
|
233
|
-
login, sessions and CLI installs
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
1. **Image and checkout.** Clone the repository for the container (a full clone of its own, not a worktree and
|
|
237
|
-
not the checkout a host instance runs from) and build the image from it:
|
|
238
|
-
|
|
239
|
-
```bash
|
|
240
|
-
git clone https://github.com/olosegres/charness /srv/telegramcode/checkout
|
|
241
|
-
cd /srv/telegramcode/checkout && docker build -t telegramcode -f docker/Dockerfile .
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
The image holds only system tools (tmux, git, ffmpeg, ripgrep, …) and the yarn the checkout's `package.json`
|
|
245
|
-
names (build from the checkout's root, which is how the build reads it; rebuild after a yarn bump so no
|
|
246
|
-
container has to download it). On first start the container installs the checkout's dependencies for the
|
|
247
|
-
image and builds it; the bot installs Claude Code and OpenCode into the agents' home.
|
|
248
|
-
2. **Instance env file**, as for a host install (`TELEGRAM_BOT_TOKEN`, `DATA_DIR`, `WORK_ROOT`, …). It stays on
|
|
249
|
-
the host and is mounted read-only. Every host path it names (`DATA_DIR`, `WORK_ROOT`, `TMUX_TMPDIR`, …) is
|
|
250
|
-
mounted at the SAME path, so `state.json`, sessions and file paths stay valid inside and an instance can move
|
|
251
|
-
between host and container unchanged.
|
|
252
|
-
3. **Run** (or use `examples/docker-compose.yml`):
|
|
253
|
-
|
|
254
|
-
```bash
|
|
255
|
-
docker run -d --name telegramcode-main --hostname telegramcode-main \
|
|
256
|
-
--restart unless-stopped --stop-timeout 60 \
|
|
257
|
-
-v /srv/telegramcode/checkout:/opt/telegramcode \
|
|
258
|
-
-v /srv/telegramcode/home:/home/telegramcode \
|
|
259
|
-
-v /srv/telegramcode/data:/srv/telegramcode/data \
|
|
260
|
-
-v /srv/projects:/srv/projects \
|
|
261
|
-
-v /srv/telegramcode/instance.env:/srv/telegramcode/instance.env:ro \
|
|
262
|
-
-e ENV_FILE=/srv/telegramcode/instance.env \
|
|
263
|
-
telegramcode
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
No port is published: Telegram is long polling, and the bot's MCP server and the OpenCode server listen on the
|
|
267
|
-
container's own loopback. Keep the `--hostname` fixed and unique per instance: the instance lock tells its own
|
|
268
|
-
restarted container from another machine by it.
|
|
269
|
-
4. **Log the agents in once**, with the account the agents should use (a separate one is best):
|
|
270
|
-
`docker exec -it telegramcode-main claude`, then `/login` (or `/login` in a topic). OpenCode: `/connect` in a
|
|
271
|
-
topic, or `docker exec -it telegramcode-main opencode auth login`. Never copy your own `~/.claude` login in:
|
|
272
|
-
a second holder of its refresh token can log the first one out. For git, put a `.gitconfig` and an SSH key of
|
|
273
|
-
their own into the agents' home.
|
|
274
|
-
|
|
275
|
-
Running it:
|
|
276
|
-
|
|
277
|
-
- **Restart the bot only** (agents keep running and are adopted): `docker exec telegramcode-main
|
|
278
|
-
telegramcode-restart-bot`. A crashed bot is restarted by the container after 20 s.
|
|
279
|
-
- **Update the bot**: `docker exec telegramcode-main /opt/telegramcode/scripts/self-update.sh` (by hand or from
|
|
280
|
-
a timer on the host); hot mode picks the change up. A change to `src/cli.ts`, `src/cli/hot.ts` or `nodemon.json`
|
|
281
|
-
needs the bot restart above. **Update Claude Code / OpenCode**: they update themselves in the agents' home, or
|
|
282
|
-
`docker exec telegramcode-main npm install -g @anthropic-ai/claude-code` / `… opencode-ai`.
|
|
283
|
-
- **A container restart is a reboot**: every agent process ends; each conversation resumes with its next message.
|
|
284
|
-
- **Rootless Docker** (recommended): the container's root is your user, so everything written on the mounts is
|
|
285
|
-
yours. **Rootful Docker**: add `--user "$(id -u):$(id -g)"` and give that user the agents' home.
|
|
286
|
-
- **Moving a host instance in**: stop it first (the lock refuses a lock written on another host), mount its
|
|
287
|
-
folders at their paths, and copy its conversations' folders from `~/.claude/projects/` into the agents' home
|
|
288
|
-
`.claude/projects/` — without them each conversation starts a fresh session on its next message.
|
|
289
|
-
- **What the agents can read**: everything mounted, the instance env file with its tokens, `DATA_DIR` and the
|
|
290
|
-
checkout included. The container keeps the rest of the host out of reach; running the agents as a second user
|
|
291
|
-
inside the container would close that gap later.
|
|
292
|
-
|
|
293
|
-
### Run from source
|
|
294
|
-
|
|
295
|
-
To hack on the bot itself:
|
|
296
|
-
|
|
297
|
-
```bash
|
|
298
|
-
git clone https://github.com/olosegres/charness && cd charness
|
|
299
|
-
yarn install && yarn build
|
|
300
|
-
npm install -g . # registers the `telegramcode` (and `charness`) command
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Shortcut: `yarn install-link` runs `yarn install && yarn build && npm link` in
|
|
304
|
-
one shot — `npm link` symlinks the global `telegramcode` to this folder, so
|
|
305
|
-
later rebuilds (`yarn build`) are picked up without re-installing. Config and
|
|
306
|
-
launch are identical to steps 2–5 above.
|
|
307
|
-
|
|
308
|
-
## Required files structure
|
|
309
|
-
|
|
310
|
-
Launch `telegramcode` from the parent folder that holds your project repos —
|
|
311
|
-
that `$PWD` is the work root, and every topic binds to a subfolder under it:
|
|
312
|
-
|
|
313
|
-
```
|
|
314
|
-
$PWD=/home/user/src ← run the bot here ($PWD = work root)
|
|
315
|
-
├── projectA/ ← Topic "projectA" (claude)
|
|
316
|
-
├── projectB/ ← Topic "projB-frontend" (claude)
|
|
317
|
-
│ ← Topic "projB-backend" (opencode) ← one folder, two topics
|
|
318
|
-
│ ← Topic "projB-refactor" (claude)
|
|
319
|
-
└── projectC/ ← Topic "projectC" (opencode)
|
|
320
|
-
|
|
321
|
-
Telegram forum supergroup
|
|
322
|
-
├── Bot is admin with can_manage_topics + can_delete_messages + can_pin_messages
|
|
323
|
-
└── Routes each topic by (chatId, threadId) → remembered in state.json
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
**What the bot remembers (`state.json`).** So a restart is seamless, the bot
|
|
327
|
-
saves which topic is bound to which folder, each topic's live agent session (to
|
|
328
|
-
re-attach it after a reboot), your per-topic preferences (agent, model,
|
|
329
|
-
reasoning effort, language, display verbosity), scheduled prompts, and the
|
|
330
|
-
paired group id. This file lives in `DATA_DIR` (default `~/.telegramCode`) and
|
|
331
|
-
is written safely, so a crash can't corrupt it.
|
|
332
|
-
|
|
333
|
-
## Commands
|
|
334
|
-
|
|
335
|
-
### In any topic
|
|
86
|
+
#### In any topic
|
|
336
87
|
|
|
337
88
|
Most commands work in any topic; a binding (`/bind`) is required only to
|
|
338
89
|
actually start an agent or terminal in the folder.
|
|
@@ -340,12 +91,12 @@ actually start an agent or terminal in the folder.
|
|
|
340
91
|
| Command | Description |
|
|
341
92
|
|---|---|
|
|
342
93
|
| `/claude`, `/opencode`, `/oc` | Start agent in this topic's bound folder |
|
|
343
|
-
| `/terminal` | Open a raw `$SHELL` in the bound folder — see [Raw terminal](#raw-terminal-terminal) |
|
|
94
|
+
| `/terminal` | Open a raw `$SHELL` in the bound folder — see [Raw terminal](#raw-terminal-terminal-telegram) |
|
|
344
95
|
| `/claude_mode` | Switch this topic's Claude backend (json-stream ⇄ per-turn ⇄ tmux-scrape); bare shows a picker — see [Claude Code backends](#claude-code-backends-claude_mode) |
|
|
345
96
|
| `/model` | Switch model through a two-level picker: providers, then that provider's models (10 per page). On OpenCode the first row is 🕘 Recently used — a flat list of the models picked before, across providers (bot-wide, most recent first, up to 10), one tap to switch; it appears once something has been picked. Each provider row has a 🙈 to hide it from the picker (bot-wide, persisted; 👁 brings it back) — `/model <provider/model>` still reaches a hidden provider. If OpenCode is waiting on an old provider's retry, the next prompt interrupts that wait and starts with the selected model instead of sitting queued behind it |
|
|
346
|
-
| `/accounts` | Every account the bot works with, on one screen. **Claude accounts** (when you keep several subscriptions signed in): each profile with its email, plan (personal Pro/Max or corporate Team/Enterprise), how many topics use it and its usage windows, ✓ on the active one; tap a profile to switch this topic (a running session is restarted under it, same conversation — at the end of its turn if it is working); in **General** the pick becomes the default for all topics. Per profile: 🔑 signs it in again, 🗑 removes it (after a confirmation; for `default`, which IS `~/.claude`, «remove» signs it out — see below). The usage lines give each window's percentage USED and when it resets as a time from now (`5h 5% 🔄 in 4h · week 20% 🔄 in 5d 8h · Fable 4% 🔄 in 5d 8h`; «🔄 in 2h», «🔄 in 2d 1h», «🔄 in 30m»; Fable only on a plan that has its own weekly window); when the 5h window's reset is unknown the weekly one is shown in its place, and with no reset at all only the percentage. The hint paragraph under the account list says the percentages are used. «➕ Claude account» asks for a name and runs the sign-in. **OpenCode providers**: the connected ones are listed with a
|
|
347
|
-
| `/effort` |
|
|
348
|
-
| `/verbosity` | Output-verbosity macro (`minimal\|short\|full`): sets the thinking, tool-results and sub-agent display prefs at once; `/thinking`, `/tool_results`, `/subagent` point-override afterwards. Mixed prefs show as "custom" in the picker. The picker's second row (or `/verbosity stream\|stream_answers\|answers`) sets what the topic SHOWS — "Stream only" (default), "Stream + answers" (the stream plus the agent's pinned answers) or "Answers only" (the pinned answers, no stream; an agent that keeps writing after its final answer is reminded once to send what matters); a switch applies from the next message, and `/status` names the view |
|
|
97
|
+
| `/accounts` | Every account the bot works with, on one screen. **Claude accounts** (when you keep several subscriptions signed in): each profile with its email, plan (personal Pro/Max or corporate Team/Enterprise), how many topics use it and its usage windows, ✓ on the active one; tap a profile to switch this topic (a running session is restarted under it, same conversation — at the end of its turn if it is working; on a topic that runs OpenCode the tap switches the topic to Claude on that account, after a confirmation when there is a conversation to hand over); in **General** the pick becomes the default for all topics. The title says where the topic really runs (`Claude «work»`, `OpenCode openai/gpt-5`) and the ✓ sits on that row — on no account while OpenCode runs. Per profile: 🔑 signs it in again, 🗑 removes it (after a confirmation; for `default`, which IS `~/.claude`, «remove» signs it out — see below). The usage lines give each window's percentage USED and when it resets as a time from now (`5h 5% 🔄 in 4h · week 20% 🔄 in 5d 8h · Fable 4% 🔄 in 5d 8h`; «🔄 in 2h», «🔄 in 2d 1h», «🔄 in 30m»; Fable only on a plan that has its own weekly window); when the 5h window's reset is unknown the weekly one is shown in its place, and with no reset at all only the percentage. The hint paragraph under the account list says the percentages are used. «➕ Claude account» asks for a name and runs the sign-in. **OpenCode providers**: the connected ones are listed with how each is signed in (`ChatGPT subscription` with its ~5 h / ~7 d usage windows like an account's, `API key`). In a topic a provider row is `[provider] [🔑] [🔌]`: a tap switches the topic to the provider through its model page (✓ on the model it runs, ⛔ on a limited one; a provider with one model is picked at once), 🔑 connects it again, 🔌 disconnects it (it asks, then removes the stored credentials; a provider OpenCode enables from an environment variable, e.g. `OPENROUTER_API_KEY`, stays active — the reply says so and points at hiding it in `/model`); in **General** a provider row is «🔌 Disconnect <provider>» and 🔑 only; «➕ OpenCode provider» asks for the provider id and continues with the connect flow (OAuth methods when available, otherwise an API key whose message is deleted from Telegram). What happens when an account hits a usage limit is set in `/limits_autoswitch`. Text forms: `/accounts add <name>`, `/accounts use <name>`, `/accounts use <provider>/<model>` (an OpenCode model, in a topic), `/accounts login <name>`, `/accounts remove <name>`, `/accounts autoswitch on\|off`. `/account`, `/connect [provider [key]]` and `/disconnect [provider]` still work as hidden aliases (the menu lists only `/accounts`). See [Several accounts](#several-accounts-accounts) |
|
|
98
|
+
| `/effort` | Reasoning effort. **In a topic:** shows the level its session runs and where the next start's level comes from («medium — as in General» / «high — set for this topic»; a running session keeps the level it started with when General moves on: «xhigh (medium from the next start) — as in General») with a button per level — Claude: the native `/effort` levels (`low…ultracode`); OpenCode: the current model's variants, applied per-prompt; General's `ultracode` is the model's highest variant and `auto` sends no variant — and «As in General (medium)», named with the level the topic will really get (OpenCode: rounded to the model's variants), which drops the topic's own pick (a live session is brought to the default at once). **In General:** the default effort for every topic that has no effort of its own (✓ on the current one; `medium` until you pick), saved to `settings.json` and applied from each topic's next session start or wake-up — running sessions are not restarted. No env configuration — see [Instance settings](SETUP.md#instance-settings-settingsjson) |
|
|
99
|
+
| `/verbosity` | Output-verbosity macro (`minimal\|short\|full`): sets the thinking, tool-results and sub-agent display prefs at once; `/thinking`, `/tool_results`, `/subagent` point-override afterwards. Mixed prefs show as "custom" in the picker. The picker's second row (or `/verbosity stream\|stream_answers\|answers`) sets what the topic SHOWS — "Stream only" (default), "Stream + answers" (the stream plus the agent's pinned answers) or "Answers only" (the pinned answers, no stream; an agent that keeps writing after its final answer is reminded once to send what matters); the answers are written like for a manager or QA — brief and to the point; a switch applies from the next message, and `/status` names the view |
|
|
349
100
|
| `/thinking` | Chain-of-thought display: `full` keeps the reasoning, `short` collapses to "💭 thought for Ns", `minimal` keeps only the live working cue |
|
|
350
101
|
| `/tool_results` | Completed tool-call output: `full` whole body, `short` capped (15 lines / 1200 chars), `minimal` transient 🔧 status only |
|
|
351
102
|
| `/subagent` | Sub-agent transcript: status-only with a ticking elapsed counter (`minimal`/`short`), or streamed "🤖 ⤷" chunks (`full`) |
|
|
@@ -353,23 +104,25 @@ actually start an agent or terminal in the folder.
|
|
|
353
104
|
| `/rename_session` | Rename the current live session (OpenCode; Claude transcripts have no title) |
|
|
354
105
|
| `/quit`, `/q` | End the session — Claude: graceful double Ctrl+C; OpenCode/terminal: `stopSession`. Releases the persisted session id, so a bot restart won't auto-reattach it (resume later via `/sessions`). A topic whose agent process is gone but whose session is kept (it "sleeps": the next message, file, voice note, `/compact`, `/model` or `/effort` resumes it by itself) is released the same way |
|
|
355
106
|
| `/new`, `/clear_session` | End the current session and start a fresh one (same adapter) |
|
|
356
|
-
| `/status` | This thread's status: agent, bound folder, the Claude account it runs on and the resolved workdir; a live session also shows the model it is actually running, effort, start time, runtime version and context use (anything the backend cannot report renders as unknown). On a Claude topic the account is its profile (name · email as in `/accounts`, marked ⛔ while it is at its limit, with a second row for a switch still pending); an OpenCode topic's model already names its provider |
|
|
107
|
+
| `/status` | This thread's status: agent, bound folder, the Claude account it runs on and the resolved workdir; a live session also shows the model it is actually running, the effort it runs (when the default moved on under it: «xhigh (medium from the next start)»), start time, runtime version and context use (anything the backend cannot report renders as unknown). On a Claude topic the account is its profile (name · email as in `/accounts`, marked ⛔ while it is at its limit, with a second row for a switch still pending); an OpenCode topic's model already names its provider |
|
|
357
108
|
| `/output` | Last 500 lines of agent output (sent as at most 5 chunks; the overflow is reported as omitted) |
|
|
358
109
|
| `/c`, `/y`, `/n` | Ctrl+C / "y" / "n" |
|
|
359
110
|
| `/enter`, `/up`, `/down`, `/tab` | tmux key passthrough |
|
|
360
111
|
| `/esc`, `/escape` | Send a raw Escape — interrupt the current turn / dismiss a selector |
|
|
361
|
-
| `/schedule` | Schedule a prompt in free text — the agent parses the time and owns the job; see [Scheduler](#scheduler-schedule) |
|
|
362
|
-
| `/
|
|
112
|
+
| `/schedule` | Schedule a prompt in free text — the agent parses the time and owns the job; see [Scheduler](#scheduler-schedule-telegram) |
|
|
113
|
+
| `/stt` | Voice recognition, from any topic (General included); instance-wide, applied to the next voice note — no restart. The main screen holds only what applies to recognition as a whole: the ordered list of **engines** (the first that works answers; when one fails the next takes over) — one full-width button each (`1. GigaAM (local) · running · 10s+25% · ✓` — name, the local engine's short state, timeout `base s + % of the voice`, ✓ usable / ✗ disabled or skipped) — then «➕ Add engine», «🌐 Language» (auto or `ru en uk de fr es`) and «✖️ Close». A tap on an engine opens its **card**: ⬆️ Up / ⬇️ Down, ✅ Use / 🚫 Don't use, ⏱ Timeout (presets), then what the kind adds — 🔑 key for **Groq**, **OpenAI** or a **custom** OpenAI-compatible server (its address, a base like `http://host:8000` is completed; then «does it need a key?»), and 🗑 Remove. A pasted key is deleted from the topic the moment it is accepted, never shown again and kept out of the logs; an address with a login in it is refused. **GigaAM (local)** (Russian only, runs on this machine) is added through «➕ Add engine» like the others: installed, it joins the list at once; not installed, a confirmation (≈ 400 MB on disk; ≈ 0.5 GB RAM while it runs, up to ≈ 0.9 GB on a long voice, nothing while stopped) leads to the install with a live progress message, and the message becomes its card. Its card holds everything local: the status line (ready · running / stopped / paused after failed starts / installing / not installed), «⏱ Exit after idle» (5 / 15 / 30 / 60 min or never), «⚙️ Workers» (1 / 2 / 4 / 8), «🔄 Try again now» (while paused), «Remove from list» (the files stay) and «🗑 Remove model files». Everything lives in one file, `stt.json` — see [Voice input](SETUP.md#voice-input). `/voice` and `/speech` are hidden aliases |
|
|
114
|
+
| `/reminders` | Reminders set with buttons only — the bot posts and pins them itself, with no agent involved; works in any topic, bound or not. See [Reminders](#reminders-reminders-telegram) |
|
|
363
115
|
| `/clear_messages` | Delete bot messages in this topic (up to 48h, Telegram limit) |
|
|
364
116
|
| `/clear` | Forwarded to the agent (Claude wipes context; OpenCode plain text) — not a bot command anymore. Also purges the topic's file-intake dir |
|
|
365
117
|
| `/compact` | Compact the agent's context — a real, confirmed compaction on OpenCode and on the default (stream) Claude backend; the tmux Claude backend has the literal command forwarded (its TUI compacts); terminal: not supported |
|
|
366
118
|
| `/compact_on_idle` | Toggle auto-compaction after ~55 min idle (Enable/Disable picker; `AGENT_IDLE_MINUTES` sets the window). Per topic; run it in **General** to set the default for all topics. On by default. Each idle compaction posts one short notice; the picker's «Show summary» / «Hide summary» row (the `/compact_summary` switch) adds the agent's full summary after it. Fires at most once per active period (until you write again); if a question was pending it is re-asked with tappable buttons after the compaction. On the json-stream backends the idle mark also STOPS the agent process, compaction or not — the topic sleeps and your next message resumes it (`/status` says so) |
|
|
367
119
|
| `/compact_summary` | Toggle posting the agent's full summary after a compaction — `/compact`, the idle compaction and the agent's own `compact_conversation` (Enable/Disable picker). Per topic; run it in **General** to set the default for all topics. Off by default: a compaction ends in a short line. On OpenCode it also hides the summary OpenCode streams itself |
|
|
368
|
-
| `/limits` |
|
|
120
|
+
| `/limits` | The **switch board**: every signed-in Claude account and every connected OpenCode provider on one screen. The **message** carries the figures — `Usage (% used)` and a line per account in the same wording `/accounts` uses (`default — ⏱ 5h 14% 🔄 in 3h · week 3% 🔄 in 6d`; `⛔ at its limit until …` while the account is limited; `(stale)` when the last poll could not refresh), a provider signed in through a ChatGPT subscription after them (`openai (ChatGPT) — ⏱ 5h 12% 🔄 in 3h · week 40% 🔄 in 4d`), the two automatic-switching settings' state with a link to `/limits_autoswitch`, and the caption «Choose the active model:». The **buttons** are one full-width name each — `✓ Claude · work`, `Claude · default`, `OpenCode · openai` — with no figure on them (the figures do not fit a phone's button); ✓ marks where the topic runs; a signed-out account is not listed. **A tap switches this topic**: another Claude account (the running session restarts under it, same conversation — at the end of its turn when it is working), an OpenCode provider (opens its model page, 10 models per page with ⛔ on a limited one; a provider with one model switches at once), the ✓ row (the model page of where the topic runs: change the model). Across backends with a conversation to hand over the bot asks first (✅ Continue / ✖️ Cancel); while a usage-limit wait is armed the tap is the limit move — the wait ends and the work continues, no question — and «⏭ Skip once» is offered. An unbound topic is told to bind a folder first. In **General** a Claude row sets the default account for all topics (the caption reads «Choose the default Claude account:»), and an OpenCode row only says that a model is picked inside a topic. The limit notice names this command. The old text forms `/limits rebuild`, `/limits clear`, `/limits switch …`, `/limits wait …` still work (they are `/limits_autoswitch`'s verbs). See [Usage limits and the fallback models](#usage-limits-and-the-fallback-models-limits-and-limits_autoswitch) |
|
|
121
|
+
| `/limits_autoswitch` | The setup of automatic switching on a usage limit, with a short text: a title, what the topic runs on and where its two settings come from. **Fallback models** — an ordered list of Claude accounts (with an optional model) and OpenCode providers with a model, top = preferred, where a topic continues when its agent hits a limit. One full-width button per entry (`✓ 1. Claude · work · opus`, `2. Claude · default ⛔ 14:00`, `3. OpenCode · openai · gpt-5 🚫` — ✓ where the topic runs, ⛔ limited until, 🔒 not signed in, ❓ no longer offered, 🚫 left out of automatic switching); a tap opens that entry in the same message with ⬆️ Up / ⬇️ Down, 🚫 Don't use / ✅ Use (a left-out entry stays usable by hand through `/model` and `/accounts`), 🗑 Remove and ⬅️ Back. Below the list: «➕ Add» (account or provider, then the model; an entry of a backend the list does not use yet — an OpenCode model next to Claude accounts, or the reverse — asks first, because a limit then hands a conversation over in compacted form; including a 🚫 entry that does that asks the same way), «🔄 Fill from connected» (a draft from what is connected; entries you left out stay left out; asks first when a list exists), «🗑 Clear». The list is ONE instance-wide list and is editable from every topic. **Two settings** decide what a limit does: «🔁 Auto switch» — auto switch to a fallback model (OFF by default: the limit notice carries «🔁 Switch to …» buttons instead) and «⏳ Wait for reset» — wait for the reset and continue (ON by default; while a wait is armed the topic screen also offers «⏭ Skip once»). In General they are the defaults for all topics and each is one button that flips the value. In a regular topic each is one short full-width button showing the effective value (`🔁 Auto switch: ON`; one line of text above the list says where each value comes from — `🔁 as in General · ⏳ set for this topic`); a tap opens the setting in the same message with ON / OFF / «As in General (<the General value>)» — the topic's own choice marked ✓ — and ⬅️ Back, and a choice applies and returns to the screen. Switch ON: the topic moves to the first available entry at once. OFF: buttons on the notice, and if none is pressed the bot waits for the reset (waiting ON); both OFF: it only reports the limit. When every enabled entry, the current one included, is limited, the wait ends at the EARLIEST reset and the topic continues on that entry (the notice names it and the time). Text forms: `/limits_autoswitch rebuild`, `/limits_autoswitch clear`, `/limits_autoswitch switch on\|off\|same`, `/limits_autoswitch wait on\|off\|same` (the same verbs work after `/limits`). `/fallback` and `/auto_continue_limits [on\|off]` still work as hidden aliases. See [Usage limits and the fallback models](#usage-limits-and-the-fallback-models-limits-and-limits_autoswitch) |
|
|
369
122
|
| `/bind` | Bare: current binding + folder picker, with «leave current dir» (the old `/unbind`) and «create new folder» buttons |
|
|
370
123
|
| `/mcp` | List MCP servers active for this thread |
|
|
371
124
|
|
|
372
|
-
|
|
125
|
+
#### Info / ops (work anywhere)
|
|
373
126
|
|
|
374
127
|
| Command | Description |
|
|
375
128
|
|---|---|
|
|
@@ -387,27 +140,28 @@ actually start an agent or terminal in the folder.
|
|
|
387
140
|
| `/timezone` | Set the timezone every clock the bot touches speaks in — schedules, agent time context, time renders. `/timezone Europe/Moscow`, `/timezone +04:00`, `/timezone auto` (host zone); bare opens a region → zone picker. Instance-wide, not per chat |
|
|
388
141
|
| `/pair` | Bind this forum supergroup to the bot (re-point auto-pairing) — works from any topic of the target group; a numeric `ALLOWED_GROUP_ID` env locks pairing |
|
|
389
142
|
|
|
390
|
-
|
|
143
|
+
#### General-only
|
|
391
144
|
|
|
392
145
|
| Command | Description |
|
|
393
146
|
|---|---|
|
|
394
147
|
| `/quit-all`, `/quitall` | End every active agent in every bound topic (also releases their session ids) |
|
|
395
148
|
|
|
396
|
-
|
|
149
|
+
#### Natural language
|
|
397
150
|
|
|
398
151
|
In a bound topic you can also type:
|
|
399
152
|
- `claude fix the bug` / `opencode add tests`
|
|
400
153
|
- A plain message after `/claude` is already running → routed as input.
|
|
401
154
|
|
|
402
|
-
Voice messages are transcribed
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
155
|
+
Voice messages are transcribed by the speech engines you set up with `/stt` (Groq,
|
|
156
|
+
OpenAI, any OpenAI-compatible server, or the local GigaAM engine — see [Voice input](SETUP.md#voice-input)) and follow
|
|
157
|
+
the same routing. The engines form an ordered chain: when one fails, the next one answers within the
|
|
158
|
+
same voice note. The last one retries a temporary failure automatically (after 5 s, then 15 s) and
|
|
159
|
+
says so in the topic — no need to re-record the voice note.
|
|
406
160
|
|
|
407
161
|
`/terminal` is never started from a natural-language phrase — only the
|
|
408
162
|
explicit command opens a shell.
|
|
409
163
|
|
|
410
|
-
|
|
164
|
+
### Claude Code backends (`/claude_mode`)
|
|
411
165
|
|
|
412
166
|
"Claude Code" in a topic is one user-facing choice with three interchangeable
|
|
413
167
|
backends:
|
|
@@ -468,14 +222,18 @@ so one account's usage limit does not stop every topic.
|
|
|
468
222
|
profile keeps your current login out of the way), finish the sign-in and paste
|
|
469
223
|
the code back into the topic. The code message is deleted; the bot confirms
|
|
470
224
|
with the account's email and plan.
|
|
471
|
-
- **Switching:** tap a profile in `/accounts`
|
|
225
|
+
- **Switching:** tap a profile in `/accounts` or an account on the `/limits`
|
|
226
|
+
board (or `/accounts use <name>`). A
|
|
472
227
|
running session on the default (stream) backend is restarted under the new
|
|
473
228
|
account with the same conversation — right away when idle, at the end of its
|
|
474
229
|
turn when it is working; a sleeping topic simply resumes under it. The
|
|
475
230
|
tmux-scrape backend is reopened the same way when idle and waits for the next
|
|
476
231
|
start when busy. In the **General** topic the pick is the default for all
|
|
477
232
|
topics; a topic with its own pick keeps it («↩ Follow the default» drops it).
|
|
478
|
-
A profile that is not signed in cannot be picked.
|
|
233
|
+
A profile that is not signed in cannot be picked. On a topic that runs OpenCode the
|
|
234
|
+
pick is a switch to Claude on that account (see *A manual switch between Claude and
|
|
235
|
+
OpenCode asks first* under the fallback models). While a usage-limit wait is armed a
|
|
236
|
+
pick is the limit move: the wait ends and the work continues on the account you tapped.
|
|
479
237
|
- **Removing:** «🗑 Remove» deletes the profile's login directory. The profile
|
|
480
238
|
that is currently the instance default cannot be removed (pick another one in
|
|
481
239
|
General first); topics that had picked the removed profile return to the
|
|
@@ -496,49 +254,85 @@ so one account's usage limit does not stop every topic.
|
|
|
496
254
|
percentage is USED. A reset is shown as a time from
|
|
497
255
|
now (`🔄 in 2d 1h`; the two largest units, minutes only under an hour); an
|
|
498
256
|
unknown 5h reset shows the weekly one, and with no reset at all only the
|
|
499
|
-
percentage is shown. `
|
|
257
|
+
percentage is shown. `charness claude-profiles` prints
|
|
500
258
|
the same relative form; its `--json` and the MCP tool keep ISO instants. The bot reads the figures with
|
|
501
259
|
each profile's own login every 10 minutes, when `/accounts` or `/limits` opens and when an
|
|
502
|
-
agent asks; a json-stream session's own rate-limit events keep them current
|
|
260
|
+
agent asks (the ChatGPT subscription of an OpenCode provider is read the same way, see below); a json-stream session's own rate-limit events keep them current
|
|
503
261
|
between polls. A token the CLI has not renewed yet shows the last figures as
|
|
504
262
|
`(stale)`; a profile whose session was refused on a window is marked
|
|
505
263
|
`⛔ at its limit until …` and is skipped by the recommendation below.
|
|
506
264
|
- **Another account takes over on a limit.** When a topic's account hits a
|
|
507
|
-
usage limit the bot looks at the [fallback chain](#usage-limits-and-the-fallback-models-limits);
|
|
265
|
+
usage limit the bot looks at the [fallback chain](#usage-limits-and-the-fallback-models-limits-and-limits_autoswitch);
|
|
508
266
|
without one it ranks the other signed-in profiles by headroom on the windows
|
|
509
267
|
the topic's model drains (fresh figures, not marked limited). The limit
|
|
510
268
|
notice carries «🔁 Switch to <entry>» buttons, best first: a tap moves the
|
|
511
269
|
topic there (the running session is restarted under the new account with the
|
|
512
270
|
same conversation), drops the wait for the reset and tells the agent to
|
|
513
271
|
continue right away. With **switching** on («🔁 Auto switch to a
|
|
514
|
-
fallback model» in `/
|
|
515
|
-
topics) the bot does the same by itself and posts one line — «
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
272
|
+
fallback model» in `/limits_autoswitch`, or `/limits_autoswitch switch on`; in General the default for all
|
|
273
|
+
topics) the bot does the same by itself and posts one short line — «Account
|
|
274
|
+
«a» hit its usage limit — this topic continues on «b». The limit resets
|
|
275
|
+
<time>. Switch: /limits». The target is named by its account alone
|
|
276
|
+
and gets its model only when the switch changes it («b» (sonnet)); an
|
|
277
|
+
OpenCode rung is written without quotes on either side — the target as
|
|
278
|
+
«OpenCode provider/model», the side that hit its limit as «Model
|
|
279
|
+
provider/model». It is OFF by default: moving
|
|
280
|
+
work between subscriptions is your call. The exhausted profile is marked
|
|
281
|
+
limited until its window resets, so it is not picked again before then. The
|
|
282
|
+
reset itself does not move the topic back: `/limits`, which the notice names,
|
|
283
|
+
lists every account and provider with its usage and one tap moves the topic
|
|
284
|
+
(see below). Only a wait that ends on another entry
|
|
285
|
+
moves a topic by itself (see below). When nothing can take over the bot waits
|
|
286
|
+
for the reset as before. A Claude session that works when a limit hits is
|
|
287
|
+
moved at once (the turn is dead; background tasks it started are stopped and
|
|
288
|
+
the notice says so); only a tmux Claude session with nothing to resume yet is
|
|
289
|
+
left to the end of its turn.
|
|
523
290
|
- **Which account for a child process.** An agent that launches a detached
|
|
524
291
|
`claude` (a worker, a reviewer) asks the bot's `telegramBot` MCP tool
|
|
525
292
|
`claude_profiles` — optionally with the model it will run — and gets every
|
|
526
293
|
profile with its windows plus `recommended`: the signed-in, unlimited profile
|
|
527
|
-
with the most headroom (
|
|
528
|
-
|
|
294
|
+
with the most headroom (the session window and the shared weekly window bound
|
|
295
|
+
every model; Fable additionally drains its own weekly window). It then
|
|
529
296
|
starts the child with `CLAUDE_CONFIG_DIR=<configDir>` (`null` = the default
|
|
530
297
|
profile: set nothing). The same answer from a shell, for a worker without
|
|
531
|
-
MCP: `
|
|
298
|
+
MCP: `charness claude-profiles [--best] [--model <name>] [--json]` —
|
|
532
299
|
`--best` prints just the directory (an empty line for the default), so
|
|
533
|
-
`CLAUDE_CONFIG_DIR="$(
|
|
300
|
+
`CLAUDE_CONFIG_DIR="$(charness claude-profiles --best --model fable)"`
|
|
534
301
|
works as is; it reads the bot's cache off `DATA_DIR/state.json`.
|
|
535
302
|
|
|
536
|
-
### Usage limits and the fallback models (`/limits`)
|
|
303
|
+
### Usage limits and the fallback models (`/limits` and `/limits_autoswitch`)
|
|
537
304
|
|
|
538
305
|
When a topic's agent stops on a usage limit — a Claude account window, an
|
|
539
|
-
OpenAI quota through OpenCode, a Google
|
|
540
|
-
the work going somewhere else instead of waiting
|
|
541
|
-
|
|
306
|
+
OpenAI quota through OpenCode, a Google quota spent for the day or the plan — the
|
|
307
|
+
bot can keep the work going somewhere else instead of waiting (a per-minute
|
|
308
|
+
throttle, or a reset a few minutes away, is simply retried). Two screens split
|
|
309
|
+
the job:
|
|
310
|
+
**`/limits` is the switch board** — every signed-in Claude account and every connected
|
|
311
|
+
OpenCode provider with its usage, one tap to move the topic there by hand — and
|
|
312
|
+
**`/limits_autoswitch` is the setup of the automatic switching**: ONE
|
|
313
|
+
instance-wide, ordered list of **fallback models** (where to go), top = preferred,
|
|
314
|
+
and the two settings (auto switch, wait for the reset):
|
|
315
|
+
|
|
316
|
+
- **The board.** `/limits` (see the command table) is where the notice sends you
|
|
317
|
+
(«The limit resets <time>. Switch: /limits»): a Claude account row changes the
|
|
318
|
+
account (model and conversation kept), an OpenCode provider row opens its model page
|
|
319
|
+
and a pick changes the model (the same session), the ✓ row changes the model of where
|
|
320
|
+
the topic runs, and across backends with a conversation the bot asks first. Every
|
|
321
|
+
entry point — a board row, an `/accounts` row, a model page, `/accounts use` — takes the
|
|
322
|
+
one switch path, so a pick behaves the same wherever it is made. While a limit wait is
|
|
323
|
+
armed a tap is the limit move: another account, another provider or model, and — on the
|
|
324
|
+
limited Claude account itself — a model the limit does not cover (a Fable-only weekly limit
|
|
325
|
+
does not stop `opus` or `sonnet`; the usage windows say what is exhausted) continue the work
|
|
326
|
+
at once; a pick still inside the limit's scope (Fable again, or any model while the session or
|
|
327
|
+
the shared weekly window is exhausted — Fable drains both too — or when the windows are unknown)
|
|
328
|
+
only changes the model and the wait stays. A tap that switched answers with a short «✅ This topic runs on …».
|
|
329
|
+
- **OpenCode usage.** A provider signed in through a **ChatGPT subscription** (the `openai`
|
|
330
|
+
provider with an `oauth` record in OpenCode's `auth.json`) shows its ~5 h and ~7 d
|
|
331
|
+
windows in the `/limits` message and in `/accounts`, read the way a Claude account's are: the stored
|
|
332
|
+
token as it is (never refreshed by the bot; an expired one marks the figures out of date),
|
|
333
|
+
every 10 minutes and when a screen opens, cached in `state.json`. API-key providers and
|
|
334
|
+
other subscriptions show only how they are signed in (`/accounts`); a model that hit a limit carries `⛔ <reset>` on its model page.
|
|
335
|
+
A failed read just leaves the provider without a usage line.
|
|
542
336
|
|
|
543
337
|
- **Left out of switching.** ✅ / 🚫 on a row includes or excludes that entry from
|
|
544
338
|
automatic switching and from the notice's switch buttons. A connected
|
|
@@ -552,7 +346,7 @@ instance-wide, ordered list of **fallback models** (where to go), top = preferre
|
|
|
552
346
|
an OpenCode provider with a model (`openai/gpt-5`, `google/gemini-2.5-pro`,
|
|
553
347
|
…). Only what is actually connected is offered: the signed-in profiles, and
|
|
554
348
|
the providers the OpenCode server is authenticated for (its live catalog).
|
|
555
|
-
- **Drafting.** «🔄 Fill from connected» (or `/
|
|
349
|
+
- **Drafting.** «🔄 Fill from connected» (or `/limits_autoswitch rebuild`) writes a draft from what
|
|
556
350
|
is signed in: the Claude accounts in their `/accounts` order (keeping the
|
|
557
351
|
topic's model), then the OpenCode models topics picked recently, then every
|
|
558
352
|
other provider's strongest model; an account or provider you had left out of
|
|
@@ -588,11 +382,23 @@ instance-wide, ordered list of **fallback models** (where to go), top = preferre
|
|
|
588
382
|
report in one short message what the task is, what is done and what remains.
|
|
589
383
|
A later `/claude` or `/opencode` returns to the parked conversation; `/new`
|
|
590
384
|
and `/quit` release it as usual.
|
|
591
|
-
-
|
|
592
|
-
|
|
593
|
-
`/
|
|
594
|
-
|
|
595
|
-
|
|
385
|
+
- **A manual switch between Claude and OpenCode asks first.** `/claude` or `/opencode` (also the agent
|
|
386
|
+
buttons and the natural-language «start …» phrases) on a topic that runs the other backend, and a Claude
|
|
387
|
+
account tapped in `/accounts` or on the `/limits` board (or `/accounts use <name>`) — and an OpenCode provider's model picked on a topic that runs Claude — move the topic the same
|
|
388
|
+
way as a limit does — parked conversation of the target resumed, the context bridge as its first prompt (text typed
|
|
389
|
+
with the command, `/opencode do X`, rides in it). When the topic holds a conversation to hand over (an agent with at
|
|
390
|
+
least one turn, running or asleep) the bot first asks «🔁 Switch this topic from OpenCode to Claude «work»? The
|
|
391
|
+
conversation context will be handed over in compacted form.» with ✅ Continue / ✖️ Cancel: Continue switches and
|
|
392
|
+
turns the question into one line saying so, Cancel changes nothing; either consumes the keyboard, and an old
|
|
393
|
+
question only says it is out of date. A topic with nothing to hand over (no agent, or one just started and not yet
|
|
394
|
+
spoken to) switches at once, and a Claude account tapped on a Claude topic applies at once, as before. `/model`
|
|
395
|
+
lists only the current backend's models, so it never switches backend.
|
|
396
|
+
- The reset of the exhausted entry does **not** move the topic back by itself —
|
|
397
|
+
the switch notice names the reset time. The one thing that can is the wait: when
|
|
398
|
+
every enabled entry is limited, the wait is re-aimed at the entry that resets
|
|
399
|
+
first, and that may be the one the topic left (see above).
|
|
400
|
+
|
|
401
|
+
### Scheduler (`/schedule`) (Telegram)
|
|
596
402
|
|
|
597
403
|
A topic can have scheduled prompts: at fire time the bot posts the prompt
|
|
598
404
|
into the topic, pins the announcement (pins accumulate as run history),
|
|
@@ -612,7 +418,7 @@ with the thread's last-used backend.
|
|
|
612
418
|
them (an expired one-shot is dropped). Reminders keep running — see
|
|
613
419
|
below. Run history: `DATA_DIR/scheduler-runs.jsonl`.
|
|
614
420
|
|
|
615
|
-
|
|
421
|
+
### Reminders (`/reminders`) (Telegram)
|
|
616
422
|
|
|
617
423
|
A reminder is the bot's own notification, not a job for an agent: at the due
|
|
618
424
|
time the bot posts your text into the topic and pins it — and the pin is what
|
|
@@ -647,7 +453,7 @@ a single catch-up.
|
|
|
647
453
|
Reminders are yours, not the agent's: the scheduling tools an agent can call
|
|
648
454
|
do not see them, so it can neither list nor delete them.
|
|
649
455
|
|
|
650
|
-
|
|
456
|
+
### Raw terminal (`/terminal`) (Telegram)
|
|
651
457
|
|
|
652
458
|
`/terminal` binds the topic to a real interactive `$SHELL` (in tmux) in the
|
|
653
459
|
bound folder — a third backend alongside the agents, mutually exclusive
|
|
@@ -659,7 +465,7 @@ re-adopted. Known v1 limitation: full-screen TUIs (vim, htop, less) render
|
|
|
659
465
|
messy; normal commands, builds, and logs stream cleanly. The shell comes
|
|
660
466
|
from the `SHELL` env (fallback `/bin/bash`).
|
|
661
467
|
|
|
662
|
-
|
|
468
|
+
### File intake (Telegram)
|
|
663
469
|
|
|
664
470
|
Send a file to a bound topic with an active agent and the bot hands it to
|
|
665
471
|
the agent:
|
|
@@ -675,9 +481,9 @@ the agent:
|
|
|
675
481
|
bare `/clear` purges the topic's intake dir (the agent's context is gone,
|
|
676
482
|
so the files are useless); files older than 30 days are swept daily.
|
|
677
483
|
|
|
678
|
-
|
|
484
|
+
### Observability
|
|
679
485
|
|
|
680
|
-
- **Output trace** — ON by default for every thread: each incoming update,
|
|
486
|
+
- **Output trace** (Telegram) — ON by default for every thread: each incoming update,
|
|
681
487
|
adapter emit, and outgoing Bot API call (with outcome, incl. 429 details)
|
|
682
488
|
is recorded into hourly buckets `DATA_DIR/output-trace-*.jsonl` (pruned
|
|
683
489
|
after 6h). `/trace off all` turns it off durably; bare `/trace` reports
|
|
@@ -686,11 +492,11 @@ the agent:
|
|
|
686
492
|
- **Console tee** — the bot's stdout/stderr is mirrored to
|
|
687
493
|
`DATA_DIR/bot-console-*.log` (same hourly buckets, same 6h prune), so
|
|
688
494
|
boot logs stay readable post-incident without the operator's terminal.
|
|
689
|
-
- **`/timestamps on`** — prepends each forwarded prompt's send time
|
|
495
|
+
- **`/timestamps on`** (Telegram) — prepends each forwarded prompt's send time
|
|
690
496
|
(local-offset ISO) so a days-long session knows what "yesterday" means.
|
|
691
497
|
Agent-facing only, default off, persisted per topic.
|
|
692
498
|
|
|
693
|
-
|
|
499
|
+
### Timezone (Telegram)
|
|
694
500
|
|
|
695
501
|
The bot runs on the host clock, which on a typical server is UTC. `/timezone`
|
|
696
502
|
declares yours ONCE, instance-wide, and every clock the bot touches follows:
|
|
@@ -715,219 +521,7 @@ The current zone and its wall clock also show in `/status`. Nothing is stored
|
|
|
715
521
|
until you set one, so an install that never runs `/timezone` behaves exactly as
|
|
716
522
|
it did before.
|
|
717
523
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
### Set Before Launch
|
|
721
|
-
|
|
722
|
-
| Variable | Description |
|
|
723
|
-
|---|---|
|
|
724
|
-
| `TELEGRAM_BOT_TOKEN` | Token from @BotFather |
|
|
725
|
-
|
|
726
|
-
Start from the parent folder containing your projects: `cd ~/projects && telegramcode`.
|
|
727
|
-
That `$PWD` is the folder the bot works in — nothing else to set for it.
|
|
728
|
-
|
|
729
|
-
**Access control.** There is no user allow-list. Whoever is a **creator or
|
|
730
|
-
administrator of the served forum group** may talk to the agent — read live from
|
|
731
|
-
Telegram (`getChatAdministrators`) and cached for 1h. Promote someone in the
|
|
732
|
-
group to grant access; demote/remove them to revoke it — the bot subscribes to
|
|
733
|
-
`chat_member` updates, so an admin change invalidates the cache and takes
|
|
734
|
-
effect immediately (the 1h TTL is only the fallback). The bot must be a group
|
|
735
|
-
admin itself (it already needs that to create topics and pin). Anonymous admins
|
|
736
|
-
can't be matched from their messages, so post non-anonymously. The DM surface
|
|
737
|
-
(when enabled) is gated separately: only the configured `OWNER_USER_ID` may
|
|
738
|
-
talk to the bot there.
|
|
739
|
-
|
|
740
|
-
> **Security model — group admin (or the DM owner) ⇒ shell on the host.** The agents run with
|
|
741
|
-
> permission checks disabled (`--dangerously-skip-permissions` for Claude,
|
|
742
|
-
> auto-approve for OpenCode) and `/terminal` opens a raw `$SHELL` in the bound
|
|
743
|
-
> folder, so anyone who can talk to the bot can execute arbitrary commands as
|
|
744
|
-
> the bot's OS user. Only promote people you would trust with SSH access to
|
|
745
|
-
> that machine, and keep the served group itself private.
|
|
746
|
-
|
|
747
|
-
### External Optional
|
|
748
|
-
|
|
749
|
-
| Variable | Default | Description |
|
|
750
|
-
|---|---|---|
|
|
751
|
-
| `ALLOWED_GROUP_ID` | (auto-pair) | Numeric forum supergroup id (`-100…`). Leave empty to auto-pair with the first forum group a group **admin/creator** contacts the bot from (id is saved to `state.json`; re-point with `/pair`). A **name** is not accepted. A numeric value disables auto-pairing |
|
|
752
|
-
| `DATA_DIR` | `~/.telegramCode` | Per-instance state. **Mandatory** if you run two bots on the same host — otherwise both share `state.json` and `mcp.json` and corrupt each other |
|
|
753
|
-
| `CHAT_MODE` | `both` | Which surface(s) this instance serves: `group`, `dm`, or `both` — see [Two surfaces](#two-surfaces-group-topics-bot-dm-or-both) |
|
|
754
|
-
| `OWNER_USER_ID` | — | **Optional.** Numeric Telegram user id of the owner — leave it unset to run group-only (the DM surface stays inert; a group works fully without it). Set it only to enable the owner-DM surface; **required** solely for `CHAT_MODE=dm` |
|
|
755
|
-
| `GROQ_API_KEY` | — | Recommended for voice transcription. Without it, voice messages are not transcribed unless you intentionally configure the OpenAI fallback |
|
|
756
|
-
|
|
757
|
-
**Localized in 12 languages** — the bot UI (commands, buttons, notices) speaks
|
|
758
|
-
中文, English, Français, ქართული, Deutsch, हिन्दी, 日本語, Português, Русский,
|
|
759
|
-
Español, Українська, Oʻzbekcha. The language is automatic per Telegram chat:
|
|
760
|
-
explicit `/language <locale>` override wins, then the sender's Telegram client
|
|
761
|
-
language, then the last supported Telegram locale seen for that chat, then
|
|
762
|
-
English.
|
|
763
|
-
Supported locales: `en`, `de`, `fr`, `es`, `pt`, `ru`, `zh`, `ja`, `hi`, `uz`,
|
|
764
|
-
`ka`, `uk`. Bare `/language` opens a single-message inline picker with each language
|
|
765
|
-
shown by its own name (endonym), sorted A→Z by English name, and a `🌐 Auto` button
|
|
766
|
-
(all 12 fit at once — no pagination). Tapping a language (or `🌐 Auto`) applies it, then the menu
|
|
767
|
-
disappears and a short confirmation stays in the chosen language;
|
|
768
|
-
`/language auto` returns a DM/group to automatic selection.
|
|
769
|
-
|
|
770
|
-
Agent provider/auth setup is normally done inside the agents themselves:
|
|
771
|
-
`claude login` for Claude CLI and `/connect <provider-id>` (for example,
|
|
772
|
-
`/connect openrouter`) or OpenCode's own config/plugins for OpenCode. No provider
|
|
773
|
-
API key env var is required by the bot for text sessions. For OpenCode, install
|
|
774
|
-
any third-party provider plugins or authentication resolvers before launch if
|
|
775
|
-
your chosen providers need them.
|
|
776
|
-
|
|
777
|
-
### Advanced / Not Normally Needed
|
|
778
|
-
|
|
779
|
-
| Variable | Default | Set only when |
|
|
780
|
-
|---|---|---|
|
|
781
|
-
| `WORK_ROOT` | `$PWD` | You cannot control the process cwd; normal launch uses `cd <projects-parent> && telegramcode` |
|
|
782
|
-
| `OPENCODE_URL` | `http://localhost:4096` | You use a custom OpenCode port or external server; the port must differ per instance |
|
|
783
|
-
| `OPENCODE_USERNAME` | `opencode` | `OPENCODE_PASSWORD` is set for a protected OpenCode server |
|
|
784
|
-
| `OPENCODE_PASSWORD` | — | Connecting to a protected OpenCode server |
|
|
785
|
-
| `OPENCODE_ALLOW_REMOTE` | — | `OPENCODE_URL` intentionally points outside loopback |
|
|
786
|
-
| `OPENCODE_BIN` | (auto) | The `opencode` binary is not on PATH or you use a fork |
|
|
787
|
-
| `CLAUDE_BIN` | (auto) | The `claude` binary is not on PATH due to nvm/asdf/systemd PATH differences |
|
|
788
|
-
| `OPENAI_API_KEY` | — | You intentionally use OpenAI Whisper as the voice fallback instead of Groq |
|
|
789
|
-
| `ANTHROPIC_API_KEY` | — | A custom MCP/OpenCode plugin/auth resolver explicitly reads it; not needed for normal Claude CLI auth. The json-stream Claude backend strips it from the agent's env (subscription billing) |
|
|
790
|
-
| `SHELL` | `/bin/bash` | You want `/terminal` to open a different shell than your login shell (host env var, not set by the bot) |
|
|
791
|
-
| `SCHEDULER_MCP_PORT` | an OS-chosen free loopback port | You need a stable, explicitly chosen port; it must differ from this instance's `OPENCODE_URL` port |
|
|
792
|
-
| `CLAUDE_SCRAPE_DEBUG` | off | You are debugging Claude tmux scraping and need full RAW/FILTERED chunks |
|
|
793
|
-
| `AGENT_IDLE_MINUTES` | `55` | A json-stream Claude topic idle for this long is compacted (when `/compact_on_idle` allows) and its process stopped; the session sleeps and the next message resumes it — shorten it only for a test; a non-positive or non-numeric value keeps the default |
|
|
794
|
-
| `REQUEST_BACKSTOP_MINUTES` | `90` | A request (a Jira hand-over, a message in an answers-only topic) with nothing seen working on it for this long is re-posted to its session — shorten it only for a test; a non-positive or non-numeric value keeps the default |
|
|
795
|
-
| `TMUX_SOCKET_NAME` | — (the default tmux server) | The instance must run its agents on its OWN tmux server (`tmux -L <name>`; any plain name except `default`). Recommended for a second instance on one host: at boot an instance kills every agent session on its server that its own state does not know |
|
|
796
|
-
| `ENV_FILE` | — | Absolute path of the ONLY env file to read — no `~/.config/telegramcode/.env`, no `$PWD/.env`; its values win over the inherited environment. Set it in the launching environment, never inside an env file (the start refuses that). `scripts/run-isolated.sh <env-file>` starts an instance with a clean environment plus this variable |
|
|
797
|
-
| `CONNECTORS` | `telegram` | Experimental: the surfaces this instance serves, as a comma list — see [Jira connector](#jira-connector-experimental). An instance that lists `jira` refuses to start on Node below 22.12 and without `ENV_FILE`, `TMUX_SOCKET_NAME` and `DATA_DIR/jira.json`, and — with Telegram off — with a `TELEGRAM_BOT_TOKEN` or any `ATLASSIAN_*` variable in its environment. Any instance refuses to start on a `DATA_DIR` that holds conversations of a surface it does not serve |
|
|
798
|
-
|
|
799
|
-
> `WORK_DIR` (1.x) is retired. Use the wrapper from the desired parent folder
|
|
800
|
-
> instead of carrying the old env forward.
|
|
801
|
-
|
|
802
|
-
## Jira connector (experimental)
|
|
803
|
-
|
|
804
|
-
The same core can serve **Jira Cloud issues** instead of (or next to) Telegram topics: an issue assigned to
|
|
805
|
-
a dedicated AI account becomes a conversation with an agent, and the agent answers in comments.
|
|
806
|
-
|
|
807
|
-
```
|
|
808
|
-
a person assigns an issue to the AI account (in a trigger status)
|
|
809
|
-
→ the instance polls Jira as the AI account and opens a request
|
|
810
|
-
→ the agent works in the project's folder; its streamed output never reaches Jira
|
|
811
|
-
→ it answers through the answer_request tool:
|
|
812
|
-
progress → a comment, the issue stays with the AI
|
|
813
|
-
question → a comment, the issue goes back to the person who handed it over
|
|
814
|
-
final → a comment, the issue goes back to that person
|
|
815
|
-
→ an agent that stops answering is reminded; when the reminders give up, a
|
|
816
|
-
comment says a person has to look, and the issue goes back
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
- **One issue = one conversation = one agent session**, bound to the folder its project maps to. The agent
|
|
820
|
-
reads the issue's text as information, never as instructions.
|
|
821
|
-
- **The agent gets the whole issue.** Every request's prompt carries the issue's fields, the full description,
|
|
822
|
-
its sub-tasks or an epic's children, links and remote links, an attachment list and ALL comments (oldest
|
|
823
|
-
first, restricted ones marked) — with a trusted group, minus what outsiders wrote (see below). A screenshot
|
|
824
|
-
or video pasted into the description or a comment shows up as a placeholder where it sat, naming its
|
|
825
|
-
attachment. Nothing is cut: a comment over 10 000 characters, or any text
|
|
826
|
-
that would not fit the prompt, is written whole to a file in the conversation's folder under `DATA_DIR/files`
|
|
827
|
-
and the prompt says where. A later hand-over to the same conversation carries only what changed since the
|
|
828
|
-
agent's last prompt (new and edited comments, deleted ones named, the rest listed as unchanged); a fresh
|
|
829
|
-
session, or one whose context was compacted, gets the whole issue again.
|
|
830
|
-
- **Who gets the answer.** Each hand-over is a request from its sender. Two people handing one issue over in
|
|
831
|
-
turn get two answers, each their own comment; the first closing answer hands the issue back to its sender,
|
|
832
|
-
the second finds the issue no longer the AI's and leaves the assignee alone. The same person handing over
|
|
833
|
-
again replaces their earlier open request; the agent answers only what the new hand-over adds (if it
|
|
834
|
-
already finished the first, a short pointer to that answer).
|
|
835
|
-
- **Budget.** An issue handed over more than `runBudgetPer24h` times in 24 hours is handed back with a notice
|
|
836
|
-
and opens no request.
|
|
837
|
-
- **Who can steer it.** With `trustedGroup` (see Setup) only the members of ONE Jira group, the AI account and
|
|
838
|
-
Jira's apps and automations count. A hand-over by anyone else — another person, a service-desk customer —
|
|
839
|
-
opens no request: one notice on the issue names the group and the issue goes back to that person. Text
|
|
840
|
-
written or edited by anyone else (a comment, the description, the summary, labels and extra fields, a
|
|
841
|
-
file's name and content) is replaced in the prompt by a `[withheld: …]` marker, so the agent knows there
|
|
842
|
-
is something and can ask its requester. Without `trustedGroup` everyone who can edit or comment steers the
|
|
843
|
-
agent, and the instance logs one line saying so.
|
|
844
|
-
|
|
845
|
-
### Setup
|
|
846
|
-
|
|
847
|
-
1. Create a Jira account for the AI (its API token is the instance's only secret) and note its account id.
|
|
848
|
-
2. Recommended: create or choose a Jira group of the people whose words may steer the AI (for example
|
|
849
|
-
`ai-trusted`), and give the AI account the *Browse users and groups* global permission so that it can read
|
|
850
|
-
the group. Then the agent reads only the text and files of its members and of Jira's apps and automations,
|
|
851
|
-
and only their hand-overs start it.
|
|
852
|
-
3. An instance of its own: a `DATA_DIR`, a `WORK_ROOT` with one folder per project, a private tmux server, and
|
|
853
|
-
an env file that is the ONLY configuration it reads:
|
|
854
|
-
|
|
855
|
-
```bash
|
|
856
|
-
# /path/to/jira-instance.env
|
|
857
|
-
CONNECTORS=jira
|
|
858
|
-
DATA_DIR=/path/to/jira-instance/data
|
|
859
|
-
WORK_ROOT=/path/to/jira-instance/work
|
|
860
|
-
TMUX_SOCKET_NAME=jira-instance
|
|
861
|
-
OPENCODE_URL=http://localhost:4097 # a port of its own — the default 4096 is refused (other instances use it)
|
|
862
|
-
JIRA_AI_API_TOKEN=... # referenced by jira.json below, never written there
|
|
863
|
-
```
|
|
864
|
-
|
|
865
|
-
4. `DATA_DIR/jira.json` — the allowlist, the trusted group and the credentials (`${VAR}` expands from the env file; an unknown
|
|
866
|
-
key is an error):
|
|
867
|
-
|
|
868
|
-
```json
|
|
869
|
-
{
|
|
870
|
-
"site": "example.atlassian.net",
|
|
871
|
-
"email": "ai-account@example.com",
|
|
872
|
-
"apiToken": "${JIRA_AI_API_TOKEN}",
|
|
873
|
-
"accountId": "<the AI account's id>",
|
|
874
|
-
"projects": {
|
|
875
|
-
"PROJ": { "folder": "proj", "triggerStatuses": ["In Progress"], "extraFields": ["customfield_10042"] }
|
|
876
|
-
},
|
|
877
|
-
"trustedGroup": "ai-trusted",
|
|
878
|
-
"pollIntervalSeconds": 90,
|
|
879
|
-
"runBudgetPer24h": 5,
|
|
880
|
-
"model": "opus",
|
|
881
|
-
"effort": "high"
|
|
882
|
-
}
|
|
883
|
-
```
|
|
884
|
-
|
|
885
|
-
`trustedGroup` (optional, but without it everyone who can edit or comment steers the agent) is the group's
|
|
886
|
-
NAME — one group per instance; a team with several trust circles runs a second instance. The instance
|
|
887
|
-
resolves it to the group's id at start, so renaming the group while it runs keeps working, and reads its
|
|
888
|
-
members again whenever its last read is older than 5 minutes: a removed member loses the AI's trust within
|
|
889
|
-
that time. List only projects where only your team can write, and where no automation acts on text or
|
|
890
|
-
actions from outside it — an e-mail handler, a portal form, a copy from another project, a rule that
|
|
891
|
-
hands an issue to the AI when someone outside the team acts: an app's or automation's text and hand-over
|
|
892
|
-
are trusted, so such a rule brings that outside text, or that person's hand-over, to the agent. Nor list a
|
|
893
|
-
project where anyone outside the team holds the *Edit all comments* permission: Jira Cloud keeps no history
|
|
894
|
-
of a comment's edits, so a comment is judged by its author and its LAST editor only — an outsider's edit of a
|
|
895
|
-
member's comment, followed by a member's own edit, reads as trusted.
|
|
896
|
-
|
|
897
|
-
`projects` maps each served project key to its folder under `WORK_ROOT` and the status names in which an
|
|
898
|
-
assigned issue is a request; the agent loads that folder's project memory (`CLAUDE.md`, `.claude/`, … in it
|
|
899
|
-
and in every parent) like any Claude Code session, while your user-level Claude setup stays out of its
|
|
900
|
-
sessions — except that for a folder under HOME that walk reads `~/.claude/CLAUDE.md` back in.
|
|
901
|
-
`pollIntervalSeconds` is 10–600 (default 90);
|
|
902
|
-
`runBudgetPer24h` defaults to 5; `model` and `effort` set the sessions' Claude model and reasoning effort
|
|
903
|
-
(optional; absent → `opus` and `high`, either key overrides only its own default). `extraFields` (optional,
|
|
904
|
-
none by default) names fields by id (`customfield_…`, or a system one like `duedate`) the agent should see next to the standard
|
|
905
|
-
ones, shown under their site name; an id the site does not list is logged at start and left out. `agentBinaries` (optional, host runtime) maps a tool name to the absolute path of an executable — for
|
|
906
|
-
example `{ "ffmpeg": "/opt/tools/ffmpeg" }` — which is checked at start and put on the agents' PATH. `adapter` is `claude-json-stream` (the default — the agent process is stopped when an issue
|
|
907
|
-
idles) or `claude-per-turn` (stopped after every answer); either way the issue's conversation sleeps between
|
|
908
|
-
requests and the next request resumes it.
|
|
909
|
-
|
|
910
|
-
5. Start it with a clean environment, so nothing of the calling shell reaches it:
|
|
911
|
-
|
|
912
|
-
```bash
|
|
913
|
-
scripts/run-isolated.sh /path/to/jira-instance.env
|
|
914
|
-
```
|
|
915
|
-
|
|
916
|
-
The start refuses a missing `ENV_FILE`, `TMUX_SOCKET_NAME` or `jira.json`, an `OPENCODE_URL` on the default
|
|
917
|
-
port, a `TELEGRAM_BOT_TOKEN` or any `ATLASSIAN_*` variable when Telegram is off, a token that belongs to
|
|
918
|
-
another account, an unknown project or status, a `trustedGroup` the site does not have or the AI account
|
|
919
|
-
may not read, and a `DATA_DIR` holding another surface's state. An unreachable Jira does not refuse the start —
|
|
920
|
-
polling backs off and resumes.
|
|
921
|
-
|
|
922
|
-
The agent sessions of a Jira instance run without the user-level Claude settings, hooks and skills, with the
|
|
923
|
-
bot's MCP server only, without the native question tool, and with an environment reduced to a short allowlist
|
|
924
|
-
— the Jira token is not in the agent's environment. The agent still runs as the instance's OS user and can read
|
|
925
|
-
the instance's env file and `DATA_DIR`, so run the instance under an OS user that holds nothing else, with an AI
|
|
926
|
-
account that sees only the allowlisted projects. **A production Jira account runs only in the Docker container**
|
|
927
|
-
([Run with Docker](#run-with-docker)): there the agent can still read the instance's env file and `DATA_DIR`,
|
|
928
|
-
but nothing of the host outside the container's mounts. Module details: `src/connectors/jira/README.md`.
|
|
929
|
-
|
|
930
|
-
## Bot-injected agent tools
|
|
524
|
+
### Bot-injected agent tools
|
|
931
525
|
|
|
932
526
|
The bot injects its own `telegramBot` MCP server into every bot-started session
|
|
933
527
|
— for Claude via a generated `--mcp-config`, for OpenCode via runtime
|
|
@@ -939,9 +533,6 @@ agent even when two clients use the same JSON-RPC request id. It exposes:
|
|
|
939
533
|
|
|
940
534
|
- `schedule_create` / `schedule_list` / `schedule_cancel` — the agent-side
|
|
941
535
|
scheduling API behind `/schedule`;
|
|
942
|
-
- `jira_get_attachment` — Jira conversations only: saves the original of one attachment of the conversation's
|
|
943
|
-
own issue (any size) and returns its path, so the agent can read an image or run `ffmpeg` on a video (with a
|
|
944
|
-
trusted group, only a file uploaded by a trusted account);
|
|
945
536
|
- `compact_conversation` — lets the agent compact its OWN session when you ask it
|
|
946
537
|
to in plain words ("compact our conversation"). It arms the compaction and
|
|
947
538
|
returns immediately; the bot runs the real compaction the moment the current
|
|
@@ -949,9 +540,12 @@ agent even when two clients use the same JSON-RPC request id. It exposes:
|
|
|
949
540
|
- `send_file_to_user` — lets the agent push files from the bound folder into the
|
|
950
541
|
topic. A single MP4 uses Bot API `sendVideo`; eligible all-video and mixed
|
|
951
542
|
photo/video albums use `sendMediaGroup`, with each MP4 represented by an
|
|
952
|
-
`InputMediaVideo` entry. `as_file:true` explicitly forces documents.
|
|
953
|
-
|
|
954
|
-
|
|
543
|
+
`InputMediaVideo` entry. `as_file:true` explicitly forces documents. A file is
|
|
544
|
+
opened so that a path swapped after validation cannot reach outside the bound
|
|
545
|
+
folder: on Linux component by component beneath a pinned folder descriptor
|
|
546
|
+
(`/proc/self/fd`); on Windows and macOS by its canonical path, held to the
|
|
547
|
+
validated file's device/inode identity and resolved again inside the folder
|
|
548
|
+
after the open. Existing
|
|
955
549
|
path, count, caption, and size caps apply; canonical upload basenames have
|
|
956
550
|
control characters and quoted-string metacharacters replaced before they enter multipart headers. Cancelling
|
|
957
551
|
the MCP call removes queued work. While any upload stream is still being
|
|
@@ -992,52 +586,7 @@ Third-party OpenCode MCP servers, including a separate `telegram-mcp`, belong to
|
|
|
992
586
|
the OpenCode configuration of the Linux user that runs the bot. They are not
|
|
993
587
|
injected by Charness; run `opencode mcp list` as that user to verify them.
|
|
994
588
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
Two host instances, or a host instance next to a container: each pair of variables below must differ to avoid
|
|
998
|
-
silent corruption. Two containers need only different tokens, groups, `DATA_DIR`s, container names and hostnames: their
|
|
999
|
-
ports and tmux servers are their own.
|
|
1000
|
-
|
|
1001
|
-
| What | Why it must differ |
|
|
1002
|
-
|---|---|
|
|
1003
|
-
| `TELEGRAM_BOT_TOKEN` | Telegram routes updates to a single long-poller per token; sharing → message loss |
|
|
1004
|
-
| `ALLOWED_GROUP_ID` | The bot gates by group id; sharing → cross-instance leakage |
|
|
1005
|
-
| `WORK_ROOT` | Each instance manages its subtree; sharing → tmux name collisions |
|
|
1006
|
-
| `DATA_DIR` | `state.json` / `mcp.json` / `threads/` per instance; sharing → corrupted JSON |
|
|
1007
|
-
| `OPENCODE_URL` port | OpenCode server binds the port; second start fails with `EADDRINUSE` and you'd silently share sessions |
|
|
1008
|
-
| `SCHEDULER_MCP_PORT` | Optional stable scheduler-MCP port; if set, it must differ from that instance's `OPENCODE_URL` port |
|
|
1009
|
-
| `TMUX_SOCKET_NAME` | Optional, recommended: each instance's boot kills the agent sessions on its tmux server that its own state does not know — on one shared server, the other instance's |
|
|
1010
|
-
|
|
1011
|
-
## Updating a deployment (self-update, no root)
|
|
1012
|
-
|
|
1013
|
-
`scripts/self-update.sh` refreshes a checkout in place, running as the
|
|
1014
|
-
checkout's owning user — no root, no cross-user copying. It is safe to run
|
|
1015
|
-
blindly (e.g. from cron):
|
|
1016
|
-
|
|
1017
|
-
- fast-forward only: it skips silently when the tree has tracked changes, a
|
|
1018
|
-
merge/rebase is in progress, or local history diverged from the upstream —
|
|
1019
|
-
a dev clone that is ahead of origin is never touched;
|
|
1020
|
-
- `yarn install --immutable` runs only when `yarn.lock` / `package.json`
|
|
1021
|
-
changed;
|
|
1022
|
-
- a hot-mode instance (`telegramcode hot`) rebuilds and restarts the worker
|
|
1023
|
-
by itself (after a dependency install the script touches `tsconfig.json`
|
|
1024
|
-
so `tsc -w` recompiles with the new modules); a non-hot instance gets
|
|
1025
|
-
`yarn build` plus a restart notice — the script never kills processes;
|
|
1026
|
-
- changes to the hot supervisor itself (`src/cli.ts`, `src/cli/hot.ts`,
|
|
1027
|
-
`nodemon.json`) are flagged with a warning: those need a manual
|
|
1028
|
-
`telegramcode hot` restart (nodemon reloads only the worker).
|
|
1029
|
-
|
|
1030
|
-
For unattended updates give the deploying user read access to the repo (e.g.
|
|
1031
|
-
a read-only GitLab deploy key on a passphrase-less SSH key) and add a cron
|
|
1032
|
-
entry:
|
|
1033
|
-
|
|
1034
|
-
```cron
|
|
1035
|
-
*/10 * * * * /path/to/telegramcode/scripts/self-update.sh >> "$HOME/.local/state/telegramcode/self-update.log" 2>&1
|
|
1036
|
-
```
|
|
1037
|
-
|
|
1038
|
-
(create the log directory once: `mkdir -p ~/.local/state/telegramcode`).
|
|
1039
|
-
|
|
1040
|
-
## Restart behaviour
|
|
589
|
+
### Restart behaviour
|
|
1041
590
|
|
|
1042
591
|
Bot restarts are designed to be invisible: agents run in external
|
|
1043
592
|
processes (`tmux`, `opencode serve`), so the bot re-adopts them instead
|
|
@@ -1053,6 +602,21 @@ or adopted ownership is revalidated against the exact hostname and port before
|
|
|
1053
602
|
a signal is sent, without trusting a reused PID or assuming an adopted listener
|
|
1054
603
|
owns its process group.
|
|
1055
604
|
A source rebuild therefore does not terminate an in-flight OpenCode turn.
|
|
605
|
+
When `opencode serve` itself dies, the bot brings it back the same way a session
|
|
606
|
+
start does — installing the binary first if it is missing, retrying a failed
|
|
607
|
+
start for about a minute — and restores the sessions silently: only a topic whose
|
|
608
|
+
reply was in progress is told it was lost, and a message sent meanwhile is held
|
|
609
|
+
and delivered once the server is back. Only if every attempt fails does a topic
|
|
610
|
+
get one short line saying the next message will try again (the error itself goes
|
|
611
|
+
to the bot log).
|
|
612
|
+
A turn's failure is posted once, although OpenCode reports most failures twice
|
|
613
|
+
(the session error and the failed message); a provider plugin's `[Debug Info]`
|
|
614
|
+
block (opencode-antigravity-auth appends one with your Google project id) is kept
|
|
615
|
+
out of the topic and stays in the bot log. Gemini's "Requests ending with a model
|
|
616
|
+
turn are not supported" — OpenCode v1's agent loop sends a history that ends on
|
|
617
|
+
the model's own turn, fixed upstream only in v2 — gets one short line and the
|
|
618
|
+
bot's "continue" at once, which is a user turn; the same refusal right after it
|
|
619
|
+
ends with the usual give-up line.
|
|
1056
620
|
The supervisor reads the same checkout `.env` as the worker before the initial
|
|
1057
621
|
start. Hot mode supports Linux and macOS; it refuses to start on Windows because
|
|
1058
622
|
nodemon cannot gracefully drain its worker tree there.
|
|
@@ -1102,7 +666,7 @@ thread not found` from a send triggers binding cleanup. Closed topics
|
|
|
1102
666
|
are detected by `TOPIC_CLOSED` errors and surface a friendly message
|
|
1103
667
|
asking the user to reopen.
|
|
1104
668
|
|
|
1105
|
-
In the Docker container (see [Run with Docker](#run-with-docker)) the same holds for a bot restart
|
|
669
|
+
In the Docker container (see [Run with Docker](SETUP.md#run-with-docker)) the same holds for a bot restart
|
|
1106
670
|
inside the container; a restart of the container itself ends every agent process, and each conversation
|
|
1107
671
|
resumes with its next message.
|
|
1108
672
|
|
|
@@ -1111,9 +675,9 @@ The default `control-group` mode kills tmux and `opencode serve` children,
|
|
|
1111
675
|
which defeats restart/reattach. Process restarts intentionally leave agent
|
|
1112
676
|
sessions alive; use `/quit` or `/quit-all` when you want to terminate them.
|
|
1113
677
|
|
|
1114
|
-
|
|
678
|
+
### Troubleshooting
|
|
1115
679
|
|
|
1116
|
-
|
|
680
|
+
#### Bot doesn't see text (Telegram)
|
|
1117
681
|
|
|
1118
682
|
You disabled privacy mode in @BotFather **after** adding the bot. The
|
|
1119
683
|
flag is cached on join. Fix:
|
|
@@ -1124,7 +688,7 @@ flag is cached on join. Fix:
|
|
|
1124
688
|
|
|
1125
689
|
Verify with `/doctor` — the privacy line should report it as disabled.
|
|
1126
690
|
|
|
1127
|
-
|
|
691
|
+
#### `/bind` says "folder not found" (Telegram)
|
|
1128
692
|
|
|
1129
693
|
The folder you launched from doesn't contain a subfolder with that name (or
|
|
1130
694
|
it's a symlink pointing outside it). Run `/ls` to see what's available, or fix
|
|
@@ -1132,19 +696,19 @@ the host path. Path-traversal attempts (`../`, absolute paths,
|
|
|
1132
696
|
NUL bytes, NFC-vs-NFD mismatches) are rejected by design — see
|
|
1133
697
|
`validateSubdir` in `src/validation.ts`.
|
|
1134
698
|
|
|
1135
|
-
|
|
699
|
+
#### "Missing right" / `/clear_messages` does nothing (Telegram)
|
|
1136
700
|
|
|
1137
701
|
The bot needs admin rights `can_delete_messages` and `can_pin_messages`
|
|
1138
702
|
in addition to `can_manage_topics`. Open group settings → admins →
|
|
1139
703
|
the bot → tick the missing boxes. `/doctor` shows the exact gap.
|
|
1140
704
|
|
|
1141
|
-
|
|
705
|
+
#### Two instances on the same machine fight each other
|
|
1142
706
|
|
|
1143
707
|
You forgot to set distinct `DATA_DIR` and `OPENCODE_URL`. The bot logs
|
|
1144
708
|
`[startup] DATA_DIR=<path>` on boot — if both instances log the same
|
|
1145
|
-
path,
|
|
709
|
+
path, give each its own `DATA_DIR` and `OPENCODE_URL` port and restart both.
|
|
1146
710
|
|
|
1147
|
-
|
|
711
|
+
#### Claude opens a session picker instead of resuming
|
|
1148
712
|
|
|
1149
713
|
The 1.x adapter invoked `claude --resume` with no UUID and relied on the
|
|
1150
714
|
interactive picker, which doesn't survive a non-interactive pty. 2.0
|
|
@@ -1154,12 +718,12 @@ anyway, your local claude CLI is older than 2.1.x — upgrade with
|
|
|
1154
718
|
`npm i -g @anthropic-ai/claude-code` (the bot also auto-installs on
|
|
1155
719
|
first use, see `src/installManager.ts`).
|
|
1156
720
|
|
|
1157
|
-
|
|
721
|
+
#### OpenCode "address already in use"
|
|
1158
722
|
|
|
1159
723
|
Two instances on the same host with the same `OPENCODE_URL`. Pick a
|
|
1160
724
|
different port for the second instance (e.g. `4097`).
|
|
1161
725
|
|
|
1162
|
-
|
|
726
|
+
### FAQ
|
|
1163
727
|
|
|
1164
728
|
**How do I run Claude Code from my phone?**
|
|
1165
729
|
Install Charness on your own machine or VPS, create a Telegram bot, and bind a forum topic (or the bot's DM) to a project folder. Start a Claude Code session in that topic and you drive it entirely from Telegram: type prompts, send voice notes, or drop in files, and the agent's output streams back into the same topic. Your phone never runs the agent; it stays on your machine.
|
|
@@ -1168,7 +732,7 @@ Install Charness on your own machine or VPS, create a Telegram bot, and bind a f
|
|
|
1168
732
|
Yes. The bot only makes outbound connections to Telegram, so nothing listens for inbound traffic and you never expose a port. OpenCode runs as a local HTTP server that the bot talks to on loopback, and you reach it from anywhere through Telegram.
|
|
1169
733
|
|
|
1170
734
|
**How do I run a coding agent on a VPS and drive it from Telegram?**
|
|
1171
|
-
Set up your VPS once with git, your repos, the agent CLIs, and `
|
|
735
|
+
Set up your VPS once with git, your repos, the agent CLIs, and `charness`, then launch the bot from your projects folder. You get an always-on dev box you drive from any device: phone, tablet, or voice. Because the agent lives on the server, it keeps working even when your laptop sleeps, and the same sessions are still reachable over plain SSH.
|
|
1172
736
|
|
|
1173
737
|
**Does my session survive a restart?**
|
|
1174
738
|
Yes, both a bot restart and an agent-server restart. State is persisted to disk; on restart the bot re-attaches to the running Claude session in tmux or re-connects to OpenCode, and an in-flight turn is delivered once it is back. A prompt sent during startup is buffered and replayed, never dropped.
|
|
@@ -1177,21 +741,24 @@ Yes, both a bot restart and an agent-server restart. State is persisted to disk;
|
|
|
1177
741
|
Yes. Each topic picks its backend independently: OpenCode over its native HTTP+SSE server, or Claude Code (tmux or json-stream). One topic is one project with its own isolated session, like terminal tabs, and two topics can point at the same folder for parallel work.
|
|
1178
742
|
|
|
1179
743
|
**Can I talk to the agent by voice?**
|
|
1180
|
-
Yes. Send a voice message to a topic and it is transcribed
|
|
744
|
+
Yes. Send a voice message to a topic and it is transcribed (by Groq, OpenAI or your own Whisper-compatible server) and forwarded to the agent as a normal prompt, so you can vibe code hands-free from your phone.
|
|
1181
745
|
|
|
1182
|
-
|
|
746
|
+
### Documentation
|
|
1183
747
|
|
|
748
|
+
- **[SETUP.md](SETUP.md)** — installing and configuring: Telegram, Jira, environment, updating; Docker as an alternative to npm.
|
|
1184
749
|
- **[DEVELOPMENT.md](DEVELOPMENT.md)** — architecture and local development (build, test, hot-reload, Docker dev loop).
|
|
1185
750
|
- **[MIGRATION_1_TO_2.md](MIGRATION_1_TO_2.md)** — upgrading a 1.x install to 2.0.
|
|
1186
751
|
|
|
1187
|
-
|
|
752
|
+
### The story behind it
|
|
1188
753
|
|
|
1189
754
|
I started running Claude Code on my own machine, and I kept wanting to check on it after I stepped away from the desk. Back then there was no way to reach it from a phone, so I put together a small bot that launched the agent and let me talk to it over Telegram. It ran on my own computer, which had to stay on the whole time.
|
|
1190
755
|
|
|
1191
756
|
Then I moved the whole thing onto a server that never sleeps, added OpenCode next to Claude Code, and it grew into what it is now: voice control, file intake, scheduling, a raw terminal, and sessions that survive restarts. These days I run it every day and drive everything from my phone or tablet, no laptop required.
|
|
1192
757
|
|
|
758
|
+
The project was called TelegramCode before it became Charness: its npm package was `telegramcode` up to 2.0.3 and is `charness` since 3.0.0.
|
|
759
|
+
|
|
1193
760
|
Full write-up: [Vibe-coding by voice, from my phone](https://dev.to/olosegres/vibe-coding-by-voice-from-my-phone-claude-code-and-opencode-over-telegram-j75).
|
|
1194
761
|
|
|
1195
|
-
|
|
762
|
+
### License
|
|
1196
763
|
|
|
1197
764
|
MIT
|