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.
Files changed (417) hide show
  1. package/.env.example +11 -13
  2. package/DEVELOPMENT.md +27 -6
  3. package/MIGRATION_1_TO_2.md +1 -1
  4. package/MIGRATION_RENAME_TO_CHARNESS.md +133 -0
  5. package/README.md +185 -618
  6. package/SETUP.md +644 -0
  7. package/dist/adapters/adapterEventRouting.d.ts +2 -1
  8. package/dist/adapters/adapterEventRouting.d.ts.map +1 -1
  9. package/dist/adapters/adapterEventRouting.js +3 -0
  10. package/dist/adapters/adapterEventRouting.js.map +1 -1
  11. package/dist/adapters/adapterNames.d.ts +13 -0
  12. package/dist/adapters/adapterNames.d.ts.map +1 -1
  13. package/dist/adapters/adapterNames.js +16 -0
  14. package/dist/adapters/adapterNames.js.map +1 -1
  15. package/dist/adapters/backendUnavailableError.d.ts +11 -0
  16. package/dist/adapters/backendUnavailableError.d.ts.map +1 -0
  17. package/dist/adapters/backendUnavailableError.js +19 -0
  18. package/dist/adapters/backendUnavailableError.js.map +1 -0
  19. package/dist/adapters/claudeCliAdapter.d.ts +30 -7
  20. package/dist/adapters/claudeCliAdapter.d.ts.map +1 -1
  21. package/dist/adapters/claudeCliAdapter.js +71 -17
  22. package/dist/adapters/claudeCliAdapter.js.map +1 -1
  23. package/dist/adapters/claudeJsonStreamAdapter.d.ts +36 -3
  24. package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
  25. package/dist/adapters/claudeJsonStreamAdapter.js +95 -31
  26. package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
  27. package/dist/adapters/createAdapter.d.ts +3 -14
  28. package/dist/adapters/createAdapter.d.ts.map +1 -1
  29. package/dist/adapters/createAdapter.js +9 -19
  30. package/dist/adapters/createAdapter.js.map +1 -1
  31. package/dist/adapters/openCodeAdapter.d.ts +140 -12
  32. package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
  33. package/dist/adapters/openCodeAdapter.js +386 -120
  34. package/dist/adapters/openCodeAdapter.js.map +1 -1
  35. package/dist/apiErrorRetry.d.ts +70 -6
  36. package/dist/apiErrorRetry.d.ts.map +1 -1
  37. package/dist/apiErrorRetry.js +312 -34
  38. package/dist/apiErrorRetry.js.map +1 -1
  39. package/dist/apiRetryKick.d.ts +26 -1
  40. package/dist/apiRetryKick.d.ts.map +1 -1
  41. package/dist/apiRetryKick.js +41 -2
  42. package/dist/apiRetryKick.js.map +1 -1
  43. package/dist/bot.d.ts +2 -1
  44. package/dist/bot.d.ts.map +1 -1
  45. package/dist/bot.js +743 -190
  46. package/dist/bot.js.map +1 -1
  47. package/dist/cli/bot.d.ts +4 -2
  48. package/dist/cli/bot.d.ts.map +1 -1
  49. package/dist/cli/bot.js +11 -3
  50. package/dist/cli/bot.js.map +1 -1
  51. package/dist/cli/botEntry.js +3 -1
  52. package/dist/cli/botEntry.js.map +1 -1
  53. package/dist/cli/claudeProfiles.d.ts +4 -4
  54. package/dist/cli/claudeProfiles.js +5 -5
  55. package/dist/cli/claudeProfiles.js.map +1 -1
  56. package/dist/cli/envLoader.d.ts +6 -5
  57. package/dist/cli/envLoader.d.ts.map +1 -1
  58. package/dist/cli/envLoader.js +8 -27
  59. package/dist/cli/envLoader.js.map +1 -1
  60. package/dist/cli/hot.d.ts +9 -3
  61. package/dist/cli/hot.d.ts.map +1 -1
  62. package/dist/cli/hot.js +55 -19
  63. package/dist/cli/hot.js.map +1 -1
  64. package/dist/cli.js +12 -12
  65. package/dist/cli.js.map +1 -1
  66. package/dist/configDirFile.d.ts +73 -0
  67. package/dist/configDirFile.d.ts.map +1 -0
  68. package/dist/configDirFile.js +216 -0
  69. package/dist/configDirFile.js.map +1 -0
  70. package/dist/connectors/jira/adf.d.ts +21 -0
  71. package/dist/connectors/jira/adf.d.ts.map +1 -1
  72. package/dist/connectors/jira/adf.js +39 -0
  73. package/dist/connectors/jira/adf.js.map +1 -1
  74. package/dist/connectors/jira/answerSink.d.ts +20 -3
  75. package/dist/connectors/jira/answerSink.d.ts.map +1 -1
  76. package/dist/connectors/jira/answerSink.js +126 -13
  77. package/dist/connectors/jira/answerSink.js.map +1 -1
  78. package/dist/connectors/jira/client.d.ts +4 -0
  79. package/dist/connectors/jira/client.d.ts.map +1 -1
  80. package/dist/connectors/jira/client.js +3 -0
  81. package/dist/connectors/jira/client.js.map +1 -1
  82. package/dist/connectors/jira/config.d.ts +10 -1
  83. package/dist/connectors/jira/config.d.ts.map +1 -1
  84. package/dist/connectors/jira/config.js +11 -2
  85. package/dist/connectors/jira/config.js.map +1 -1
  86. package/dist/connectors/jira/connector.d.ts +7 -1
  87. package/dist/connectors/jira/connector.d.ts.map +1 -1
  88. package/dist/connectors/jira/connector.js +23 -4
  89. package/dist/connectors/jira/connector.js.map +1 -1
  90. package/dist/connectors/jira/contextLedger.d.ts +6 -0
  91. package/dist/connectors/jira/contextLedger.d.ts.map +1 -1
  92. package/dist/connectors/jira/contextLedger.js +16 -0
  93. package/dist/connectors/jira/contextLedger.js.map +1 -1
  94. package/dist/connectors/jira/contextPreamble.d.ts.map +1 -1
  95. package/dist/connectors/jira/contextPreamble.js +6 -3
  96. package/dist/connectors/jira/contextPreamble.js.map +1 -1
  97. package/dist/connectors/jira/inbound.d.ts +60 -7
  98. package/dist/connectors/jira/inbound.d.ts.map +1 -1
  99. package/dist/connectors/jira/inbound.js +169 -16
  100. package/dist/connectors/jira/inbound.js.map +1 -1
  101. package/dist/connectors/jira/issueBlocks.d.ts +6 -1
  102. package/dist/connectors/jira/issueBlocks.d.ts.map +1 -1
  103. package/dist/connectors/jira/issueBlocks.js +5 -1
  104. package/dist/connectors/jira/issueBlocks.js.map +1 -1
  105. package/dist/connectors/jira/prompt.d.ts +2 -0
  106. package/dist/connectors/jira/prompt.d.ts.map +1 -1
  107. package/dist/connectors/jira/prompt.js +16 -0
  108. package/dist/connectors/jira/prompt.js.map +1 -1
  109. package/dist/connectors/jira/trigger.d.ts +12 -3
  110. package/dist/connectors/jira/trigger.d.ts.map +1 -1
  111. package/dist/connectors/jira/trigger.js +8 -1
  112. package/dist/connectors/jira/trigger.js.map +1 -1
  113. package/dist/connectors/jira/triggerLog.d.ts +10 -3
  114. package/dist/connectors/jira/triggerLog.d.ts.map +1 -1
  115. package/dist/connectors/jira/triggerLog.js +18 -2
  116. package/dist/connectors/jira/triggerLog.js.map +1 -1
  117. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts +2 -2
  118. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts.map +1 -1
  119. package/dist/connectors/telegram/commands/autoContinueLimits.js +4 -4
  120. package/dist/connectors/telegram/commands/autoContinueLimits.js.map +1 -1
  121. package/dist/connectors/telegram/commands/backendSwitch.d.ts +25 -0
  122. package/dist/connectors/telegram/commands/backendSwitch.d.ts.map +1 -0
  123. package/dist/connectors/telegram/commands/backendSwitch.js +104 -0
  124. package/dist/connectors/telegram/commands/backendSwitch.js.map +1 -0
  125. package/dist/connectors/telegram/commands/callbackMessage.d.ts +12 -0
  126. package/dist/connectors/telegram/commands/callbackMessage.d.ts.map +1 -1
  127. package/dist/connectors/telegram/commands/callbackMessage.js +24 -0
  128. package/dist/connectors/telegram/commands/callbackMessage.js.map +1 -1
  129. package/dist/connectors/telegram/commands/callbackToast.d.ts +36 -0
  130. package/dist/connectors/telegram/commands/callbackToast.d.ts.map +1 -0
  131. package/dist/connectors/telegram/commands/callbackToast.js +83 -0
  132. package/dist/connectors/telegram/commands/callbackToast.js.map +1 -0
  133. package/dist/connectors/telegram/commands/claudeAccounts.d.ts +18 -0
  134. package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -1
  135. package/dist/connectors/telegram/commands/claudeAccounts.js +220 -66
  136. package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -1
  137. package/dist/connectors/telegram/commands/displayModes.d.ts.map +1 -1
  138. package/dist/connectors/telegram/commands/displayModes.js +4 -2
  139. package/dist/connectors/telegram/commands/displayModes.js.map +1 -1
  140. package/dist/connectors/telegram/commands/fallbackChain.d.ts +5 -8
  141. package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -1
  142. package/dist/connectors/telegram/commands/fallbackChain.js +183 -193
  143. package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
  144. package/dist/connectors/telegram/commands/limitsBoard.d.ts +40 -0
  145. package/dist/connectors/telegram/commands/limitsBoard.d.ts.map +1 -0
  146. package/dist/connectors/telegram/commands/limitsBoard.js +281 -0
  147. package/dist/connectors/telegram/commands/limitsBoard.js.map +1 -0
  148. package/dist/connectors/telegram/commands/modelPage.d.ts +130 -0
  149. package/dist/connectors/telegram/commands/modelPage.d.ts.map +1 -0
  150. package/dist/connectors/telegram/commands/modelPage.js +120 -0
  151. package/dist/connectors/telegram/commands/modelPage.js.map +1 -0
  152. package/dist/connectors/telegram/commands/modelProviders.d.ts +9 -1
  153. package/dist/connectors/telegram/commands/modelProviders.d.ts.map +1 -1
  154. package/dist/connectors/telegram/commands/modelProviders.js +203 -111
  155. package/dist/connectors/telegram/commands/modelProviders.js.map +1 -1
  156. package/dist/connectors/telegram/commands/stt.d.ts +32 -0
  157. package/dist/connectors/telegram/commands/stt.d.ts.map +1 -0
  158. package/dist/connectors/telegram/commands/stt.js +460 -0
  159. package/dist/connectors/telegram/commands/stt.js.map +1 -0
  160. package/dist/effortLevels.d.ts +57 -7
  161. package/dist/effortLevels.d.ts.map +1 -1
  162. package/dist/effortLevels.js +65 -9
  163. package/dist/effortLevels.js.map +1 -1
  164. package/dist/i18n/de.d.ts.map +1 -1
  165. package/dist/i18n/de.js +160 -17
  166. package/dist/i18n/de.js.map +1 -1
  167. package/dist/i18n/en.d.ts.map +1 -1
  168. package/dist/i18n/en.js +160 -17
  169. package/dist/i18n/en.js.map +1 -1
  170. package/dist/i18n/es.d.ts.map +1 -1
  171. package/dist/i18n/es.js +160 -17
  172. package/dist/i18n/es.js.map +1 -1
  173. package/dist/i18n/fr.d.ts.map +1 -1
  174. package/dist/i18n/fr.js +160 -17
  175. package/dist/i18n/fr.js.map +1 -1
  176. package/dist/i18n/hi.d.ts.map +1 -1
  177. package/dist/i18n/hi.js +160 -17
  178. package/dist/i18n/hi.js.map +1 -1
  179. package/dist/i18n/ja.d.ts.map +1 -1
  180. package/dist/i18n/ja.js +160 -17
  181. package/dist/i18n/ja.js.map +1 -1
  182. package/dist/i18n/ka.d.ts.map +1 -1
  183. package/dist/i18n/ka.js +160 -17
  184. package/dist/i18n/ka.js.map +1 -1
  185. package/dist/i18n/pt.d.ts.map +1 -1
  186. package/dist/i18n/pt.js +160 -17
  187. package/dist/i18n/pt.js.map +1 -1
  188. package/dist/i18n/ru.d.ts.map +1 -1
  189. package/dist/i18n/ru.js +160 -17
  190. package/dist/i18n/ru.js.map +1 -1
  191. package/dist/i18n/uk.d.ts.map +1 -1
  192. package/dist/i18n/uk.js +160 -17
  193. package/dist/i18n/uk.js.map +1 -1
  194. package/dist/i18n/uz.d.ts.map +1 -1
  195. package/dist/i18n/uz.js +160 -17
  196. package/dist/i18n/uz.js.map +1 -1
  197. package/dist/i18n/zh.d.ts.map +1 -1
  198. package/dist/i18n/zh.js +160 -17
  199. package/dist/i18n/zh.js.map +1 -1
  200. package/dist/index.js +1 -1
  201. package/dist/index.js.map +1 -1
  202. package/dist/installManager.d.ts +36 -6
  203. package/dist/installManager.d.ts.map +1 -1
  204. package/dist/installManager.js +124 -26
  205. package/dist/installManager.js.map +1 -1
  206. package/dist/instanceLock.d.ts.map +1 -1
  207. package/dist/instanceLock.js +10 -8
  208. package/dist/instanceLock.js.map +1 -1
  209. package/dist/instanceSettings.d.ts +52 -0
  210. package/dist/instanceSettings.d.ts.map +1 -0
  211. package/dist/instanceSettings.js +136 -0
  212. package/dist/instanceSettings.js.map +1 -0
  213. package/dist/legacyMigration.d.ts +69 -0
  214. package/dist/legacyMigration.d.ts.map +1 -0
  215. package/dist/legacyMigration.js +338 -0
  216. package/dist/legacyMigration.js.map +1 -0
  217. package/dist/legacyNames.d.ts +107 -0
  218. package/dist/legacyNames.d.ts.map +1 -0
  219. package/dist/legacyNames.js +230 -0
  220. package/dist/legacyNames.js.map +1 -0
  221. package/dist/pinnedStatus.d.ts +7 -1
  222. package/dist/pinnedStatus.d.ts.map +1 -1
  223. package/dist/pinnedStatus.js +1 -1
  224. package/dist/pinnedStatus.js.map +1 -1
  225. package/dist/platform/answerSink.d.ts +5 -0
  226. package/dist/platform/answerSink.d.ts.map +1 -1
  227. package/dist/platform/answerSink.js.map +1 -1
  228. package/dist/requests/answerRequest.d.ts +2 -0
  229. package/dist/requests/answerRequest.d.ts.map +1 -1
  230. package/dist/requests/answerRequest.js +3 -1
  231. package/dist/requests/answerRequest.js.map +1 -1
  232. package/dist/requests/answerStyle.d.ts +11 -0
  233. package/dist/requests/answerStyle.d.ts.map +1 -0
  234. package/dist/requests/answerStyle.js +14 -0
  235. package/dist/requests/answerStyle.js.map +1 -0
  236. package/dist/requests/answerTail.d.ts.map +1 -1
  237. package/dist/requests/answerTail.js +2 -0
  238. package/dist/requests/answerTail.js.map +1 -1
  239. package/dist/requests/requestHeader.d.ts.map +1 -1
  240. package/dist/requests/requestHeader.js +3 -0
  241. package/dist/requests/requestHeader.js.map +1 -1
  242. package/dist/requests/requestLedger.d.ts +8 -0
  243. package/dist/requests/requestLedger.d.ts.map +1 -1
  244. package/dist/requests/requestLedger.js +20 -2
  245. package/dist/requests/requestLedger.js.map +1 -1
  246. package/dist/requests/types.d.ts +3 -1
  247. package/dist/requests/types.d.ts.map +1 -1
  248. package/dist/scheduler/mcpSurface.d.ts +9 -1
  249. package/dist/scheduler/mcpSurface.d.ts.map +1 -1
  250. package/dist/scheduler/mcpSurface.js +49 -16
  251. package/dist/scheduler/mcpSurface.js.map +1 -1
  252. package/dist/shutdown.d.ts +14 -0
  253. package/dist/shutdown.d.ts.map +1 -1
  254. package/dist/shutdown.js +16 -0
  255. package/dist/shutdown.js.map +1 -1
  256. package/dist/state.d.ts +57 -3
  257. package/dist/state.d.ts.map +1 -1
  258. package/dist/state.js +67 -3
  259. package/dist/state.js.map +1 -1
  260. package/dist/stt/sttAutoInstall.d.ts +39 -0
  261. package/dist/stt/sttAutoInstall.d.ts.map +1 -0
  262. package/dist/stt/sttAutoInstall.js +158 -0
  263. package/dist/stt/sttAutoInstall.js.map +1 -0
  264. package/dist/stt/sttChildEnv.d.ts +11 -0
  265. package/dist/stt/sttChildEnv.d.ts.map +1 -0
  266. package/dist/stt/sttChildEnv.js +30 -0
  267. package/dist/stt/sttChildEnv.js.map +1 -0
  268. package/dist/stt/sttConfigEdit.d.ts +71 -0
  269. package/dist/stt/sttConfigEdit.d.ts.map +1 -0
  270. package/dist/stt/sttConfigEdit.js +144 -0
  271. package/dist/stt/sttConfigEdit.js.map +1 -0
  272. package/dist/stt/sttInstall.d.ts +147 -0
  273. package/dist/stt/sttInstall.d.ts.map +1 -0
  274. package/dist/stt/sttInstall.js +491 -0
  275. package/dist/stt/sttInstall.js.map +1 -0
  276. package/dist/stt/sttInstallEntry.d.ts +2 -0
  277. package/dist/stt/sttInstallEntry.d.ts.map +1 -0
  278. package/dist/stt/sttInstallEntry.js +31 -0
  279. package/dist/stt/sttInstallEntry.js.map +1 -0
  280. package/dist/stt/sttInstallReporter.d.ts +45 -0
  281. package/dist/stt/sttInstallReporter.d.ts.map +1 -0
  282. package/dist/stt/sttInstallReporter.js +57 -0
  283. package/dist/stt/sttInstallReporter.js.map +1 -0
  284. package/dist/stt/sttRuntime.d.ts +35 -0
  285. package/dist/stt/sttRuntime.d.ts.map +1 -0
  286. package/dist/stt/sttRuntime.js +73 -0
  287. package/dist/stt/sttRuntime.js.map +1 -0
  288. package/dist/stt/sttScreenPlan.d.ts +193 -0
  289. package/dist/stt/sttScreenPlan.d.ts.map +1 -0
  290. package/dist/stt/sttScreenPlan.js +541 -0
  291. package/dist/stt/sttScreenPlan.js.map +1 -0
  292. package/dist/stt/sttSettings.d.ts +201 -0
  293. package/dist/stt/sttSettings.d.ts.map +1 -0
  294. package/dist/stt/sttSettings.js +387 -0
  295. package/dist/stt/sttSettings.js.map +1 -0
  296. package/dist/stt/sttSidecarOwner.d.ts +115 -0
  297. package/dist/stt/sttSidecarOwner.d.ts.map +1 -0
  298. package/dist/stt/sttSidecarOwner.js +475 -0
  299. package/dist/stt/sttSidecarOwner.js.map +1 -0
  300. package/dist/stt/transcribeChain.d.ts +36 -0
  301. package/dist/stt/transcribeChain.d.ts.map +1 -0
  302. package/dist/stt/transcribeChain.js +42 -0
  303. package/dist/stt/transcribeChain.js.map +1 -0
  304. package/dist/types.d.ts +77 -4
  305. package/dist/types.d.ts.map +1 -1
  306. package/dist/utils/adapterSwitchSessionIds.d.ts +32 -0
  307. package/dist/utils/adapterSwitchSessionIds.d.ts.map +1 -0
  308. package/dist/utils/adapterSwitchSessionIds.js +44 -0
  309. package/dist/utils/adapterSwitchSessionIds.js.map +1 -0
  310. package/dist/utils/backendSwitchPlan.d.ts +63 -0
  311. package/dist/utils/backendSwitchPlan.d.ts.map +1 -0
  312. package/dist/utils/backendSwitchPlan.js +73 -0
  313. package/dist/utils/backendSwitchPlan.js.map +1 -0
  314. package/dist/utils/claudeAccountPickerPlan.d.ts +12 -1
  315. package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -1
  316. package/dist/utils/claudeAccountPickerPlan.js +20 -1
  317. package/dist/utils/claudeAccountPickerPlan.js.map +1 -1
  318. package/dist/utils/claudeProfiles.d.ts +7 -0
  319. package/dist/utils/claudeProfiles.d.ts.map +1 -1
  320. package/dist/utils/claudeProfiles.js +19 -0
  321. package/dist/utils/claudeProfiles.js.map +1 -1
  322. package/dist/utils/claudeTranscriptContent.d.ts +24 -0
  323. package/dist/utils/claudeTranscriptContent.d.ts.map +1 -0
  324. package/dist/utils/claudeTranscriptContent.js +107 -0
  325. package/dist/utils/claudeTranscriptContent.js.map +1 -0
  326. package/dist/utils/claudeUsage.d.ts +47 -8
  327. package/dist/utils/claudeUsage.d.ts.map +1 -1
  328. package/dist/utils/claudeUsage.js +76 -14
  329. package/dist/utils/claudeUsage.js.map +1 -1
  330. package/dist/utils/claudeUsageRefresher.d.ts +8 -1
  331. package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
  332. package/dist/utils/claudeUsageRefresher.js +9 -4
  333. package/dist/utils/claudeUsageRefresher.js.map +1 -1
  334. package/dist/utils/effortPickerPlan.d.ts +54 -0
  335. package/dist/utils/effortPickerPlan.d.ts.map +1 -0
  336. package/dist/utils/effortPickerPlan.js +56 -0
  337. package/dist/utils/effortPickerPlan.js.map +1 -0
  338. package/dist/utils/ensureSessionPlan.d.ts +20 -4
  339. package/dist/utils/ensureSessionPlan.d.ts.map +1 -1
  340. package/dist/utils/ensureSessionPlan.js +17 -4
  341. package/dist/utils/ensureSessionPlan.js.map +1 -1
  342. package/dist/utils/fallbackBridge.d.ts +6 -2
  343. package/dist/utils/fallbackBridge.d.ts.map +1 -1
  344. package/dist/utils/fallbackBridge.js +10 -3
  345. package/dist/utils/fallbackBridge.js.map +1 -1
  346. package/dist/utils/fallbackChain.d.ts +36 -0
  347. package/dist/utils/fallbackChain.d.ts.map +1 -1
  348. package/dist/utils/fallbackChain.js +65 -1
  349. package/dist/utils/fallbackChain.js.map +1 -1
  350. package/dist/utils/fallbackSwitchNotice.d.ts +41 -0
  351. package/dist/utils/fallbackSwitchNotice.d.ts.map +1 -0
  352. package/dist/utils/fallbackSwitchNotice.js +75 -0
  353. package/dist/utils/fallbackSwitchNotice.js.map +1 -0
  354. package/dist/utils/fileSendService.d.ts +33 -10
  355. package/dist/utils/fileSendService.d.ts.map +1 -1
  356. package/dist/utils/fileSendService.js +78 -27
  357. package/dist/utils/fileSendService.js.map +1 -1
  358. package/dist/utils/jsonStreamHost.d.ts +39 -0
  359. package/dist/utils/jsonStreamHost.d.ts.map +1 -1
  360. package/dist/utils/jsonStreamHost.js +60 -1
  361. package/dist/utils/jsonStreamHost.js.map +1 -1
  362. package/dist/utils/limitEpisodeRecovery.d.ts +16 -2
  363. package/dist/utils/limitEpisodeRecovery.d.ts.map +1 -1
  364. package/dist/utils/limitEpisodeRecovery.js +44 -3
  365. package/dist/utils/limitEpisodeRecovery.js.map +1 -1
  366. package/dist/utils/limitSettings.d.ts +25 -0
  367. package/dist/utils/limitSettings.d.ts.map +1 -0
  368. package/dist/utils/limitSettings.js +31 -0
  369. package/dist/utils/limitSettings.js.map +1 -0
  370. package/dist/utils/limitsBoardPlan.d.ts +52 -0
  371. package/dist/utils/limitsBoardPlan.d.ts.map +1 -0
  372. package/dist/utils/limitsBoardPlan.js +93 -0
  373. package/dist/utils/limitsBoardPlan.js.map +1 -0
  374. package/dist/utils/openCodeCompactPlugin.d.ts +11 -3
  375. package/dist/utils/openCodeCompactPlugin.d.ts.map +1 -1
  376. package/dist/utils/openCodeCompactPlugin.js +37 -5
  377. package/dist/utils/openCodeCompactPlugin.js.map +1 -1
  378. package/dist/utils/openCodeUsage.d.ts +93 -0
  379. package/dist/utils/openCodeUsage.d.ts.map +1 -0
  380. package/dist/utils/openCodeUsage.js +248 -0
  381. package/dist/utils/openCodeUsage.js.map +1 -0
  382. package/dist/utils/operatorSwitchPlan.d.ts +61 -0
  383. package/dist/utils/operatorSwitchPlan.d.ts.map +1 -0
  384. package/dist/utils/operatorSwitchPlan.js +32 -0
  385. package/dist/utils/operatorSwitchPlan.js.map +1 -0
  386. package/dist/utils/promptDelivery.d.ts +40 -0
  387. package/dist/utils/promptDelivery.d.ts.map +1 -1
  388. package/dist/utils/promptDelivery.js +31 -0
  389. package/dist/utils/promptDelivery.js.map +1 -1
  390. package/dist/utils/recvPreviewRedaction.d.ts +14 -4
  391. package/dist/utils/recvPreviewRedaction.d.ts.map +1 -1
  392. package/dist/utils/recvPreviewRedaction.js +19 -7
  393. package/dist/utils/recvPreviewRedaction.js.map +1 -1
  394. package/dist/utils/startupReadiness.d.ts +3 -3
  395. package/dist/utils/startupReadiness.d.ts.map +1 -1
  396. package/dist/utils/startupReadiness.js +2 -2
  397. package/dist/utils/startupReadiness.js.map +1 -1
  398. package/dist/utils/threadStatusReport.d.ts +3 -1
  399. package/dist/utils/threadStatusReport.d.ts.map +1 -1
  400. package/dist/utils/threadStatusReport.js +4 -3
  401. package/dist/utils/threadStatusReport.js.map +1 -1
  402. package/dist/utils/timezone.d.ts +23 -0
  403. package/dist/utils/timezone.d.ts.map +1 -1
  404. package/dist/utils/timezone.js +38 -0
  405. package/dist/utils/timezone.js.map +1 -1
  406. package/dist/utils/transcribeAudio.d.ts +25 -12
  407. package/dist/utils/transcribeAudio.d.ts.map +1 -1
  408. package/dist/utils/transcribeAudio.js +39 -21
  409. package/dist/utils/transcribeAudio.js.map +1 -1
  410. package/dist/utils/usageRefreshBudget.d.ts +9 -0
  411. package/dist/utils/usageRefreshBudget.d.ts.map +1 -0
  412. package/dist/utils/usageRefreshBudget.js +27 -0
  413. package/dist/utils/usageRefreshBudget.js.map +1 -0
  414. package/examples/.env.example +13 -9
  415. package/examples/docker-compose.yml +20 -17
  416. package/package.json +4 -1
  417. package/stt/gigaam_sidecar.py +800 -0
package/SETUP.md ADDED
@@ -0,0 +1,644 @@
1
+ # Setup
2
+
3
+ How to install, configure and run Charness. The common part comes first — where to run it, install, the
4
+ environment, several instances on one host, updating; then follow the surface you use:
5
+ [Telegram](#telegram-setup) or [Jira](#jira-setup). The whole bot can also run in a container instead of through
6
+ npm — see [Alternatives](#alternatives) at the end. What Charness does is in the [README](README.md).
7
+
8
+ ## Common setup
9
+
10
+ ### Where to run the bot server
11
+
12
+ The bot runs anywhere the agent CLIs run:
13
+
14
+ - **Your own working machine** — the simplest start: launch `charness`
15
+ from your projects parent folder and your laptop/desktop becomes the
16
+ backend. The catch: agents are only reachable while the machine is on and
17
+ awake.
18
+ - **A remote server (cloud VPS/VDS or bare metal)** — the more convenient
19
+ setup: organize your working environment there once (git + your repos,
20
+ Claude Code / OpenCode, `charness`) and you get an always-on dev box you
21
+ drive from any device through Telegram — phone, tablet, voice. When picking
22
+ one, a bare-metal server without virtualization beats a virtualized VPS/VDS:
23
+ its NVMe drives are usually much faster, which matters once you also build
24
+ and run a dev server there, not just the agents. Classic terminal access to
25
+ the very same Claude sessions stays available over SSH — run plain `claude`
26
+ in the project folder: it reads the same `~/.claude/projects/<cwd-slug>/`
27
+ session store the bot uses, so a session started in a Telegram thread can
28
+ be picked up in the terminal and vice versa.
29
+
30
+ ### Install the CLI
31
+
32
+ ```bash
33
+ npm install -g charness # needs Node ≥ 22.12
34
+ ```
35
+
36
+ This registers the `charness` command; the former `telegramcode` command (and
37
+ `telegramCode`) still works as an alias. Coming from the old `telegramcode` npm
38
+ package (2.x)? Run `npm uninstall -g telegramcode` first: both install the same
39
+ commands. The config, `DATA_DIR` and sessions carry over as they are: a
40
+ `~/.config/telegramcode/.env` is still read while `~/.config/charness/.env` does
41
+ not exist, and a `~/.telegramCode` data dir is still used while `~/.charness` does
42
+ not exist. At a cold start the bot renames those two dirs itself (leaving a symlink
43
+ at the old path) and says so; every old name still in use prints one deprecation
44
+ line. What it does, its limits and the steps by hand:
45
+ [MIGRATION_RENAME_TO_CHARNESS.md](MIGRATION_RENAME_TO_CHARNESS.md).
46
+
47
+ ### Environment variables
48
+
49
+ These settings apply to every surface. The Telegram ones — the bot token, the group and the DM, voice — are listed
50
+ under [Telegram environment variables](#telegram-environment-variables).
51
+
52
+ #### External Optional
53
+
54
+ | Variable | Default | Description |
55
+ |---|---|---|
56
+ | `DATA_DIR` | `~/.charness` (an existing `~/.telegramCode` is kept while `~/.charness` does not exist) | 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 |
57
+
58
+ Agent provider/auth setup is normally done inside the agents themselves:
59
+ `claude login` for Claude CLI and `/connect <provider-id>` (for example,
60
+ `/connect openrouter`) or OpenCode's own config/plugins for OpenCode. No provider
61
+ API key env var is required by the bot for text sessions. For OpenCode, install
62
+ any third-party provider plugins or authentication resolvers before launch if
63
+ your chosen providers need them.
64
+
65
+ #### Advanced / Not Normally Needed
66
+
67
+ | Variable | Default | Set only when |
68
+ |---|---|---|
69
+ | `WORK_ROOT` | `$PWD` | You cannot control the process cwd; normal launch uses `cd <projects-parent> && charness` |
70
+ | `OPENCODE_URL` | `http://localhost:4096` | You use a custom OpenCode port or external server; the port must differ per instance |
71
+ | `OPENCODE_USERNAME` | `opencode` | `OPENCODE_PASSWORD` is set for a protected OpenCode server |
72
+ | `OPENCODE_PASSWORD` | — | Connecting to a protected OpenCode server |
73
+ | `OPENCODE_ALLOW_REMOTE` | — | `OPENCODE_URL` intentionally points outside loopback |
74
+ | `OPENCODE_BIN` | (auto) | The `opencode` binary is not on PATH or you use a fork |
75
+ | `CLAUDE_BIN` | (auto) | The `claude` binary is not on PATH due to nvm/asdf/systemd PATH differences |
76
+ | `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) |
77
+ | `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 |
78
+ | `CLAUDE_SCRAPE_DEBUG` | off | You are debugging Claude tmux scraping and need full RAW/FILTERED chunks |
79
+ | `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 |
80
+ | `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 |
81
+ | `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 |
82
+ | `ENV_FILE` | — | Absolute path of the ONLY env file to read — no `~/.config/charness/.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>` (in a checkout) starts an instance with a clean environment plus this variable |
83
+ | `CONNECTORS` | `telegram` | Experimental: the surfaces this instance serves, as a comma list — see [Jira setup](#jira-setup). 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 |
84
+
85
+ > `WORK_DIR` (1.x) is retired. Use the wrapper from the desired parent folder
86
+ > instead of carrying the old env forward.
87
+
88
+ ### Two instances on one host
89
+
90
+ Two host instances, or a host instance next to a container: each pair of variables below must differ to avoid
91
+ silent corruption. Two containers need only different tokens and groups (Telegram), `DATA_DIR`s, container names and
92
+ hostnames: their ports and tmux servers are their own.
93
+
94
+ | What | Why it must differ |
95
+ |---|---|
96
+ | `TELEGRAM_BOT_TOKEN` (Telegram) | Telegram routes updates to a single long-poller per token; sharing → message loss |
97
+ | `ALLOWED_GROUP_ID` (Telegram) | The bot gates by group id; sharing → cross-instance leakage |
98
+ | `WORK_ROOT` | Each instance manages its subtree; sharing → tmux name collisions |
99
+ | `DATA_DIR` | `state.json` / `mcp.json` / `threads/` per instance; sharing → corrupted JSON |
100
+ | `OPENCODE_URL` port | OpenCode server binds the port; second start fails with `EADDRINUSE` and you'd silently share sessions |
101
+ | `SCHEDULER_MCP_PORT` | Optional stable scheduler-MCP port; if set, it must differ from that instance's `OPENCODE_URL` port |
102
+ | `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 |
103
+
104
+ ### Updating
105
+
106
+ An npm install updates with `npm install -g charness@latest`; then restart `charness` (what survives a restart:
107
+ [Restart behaviour](README.md#restart-behaviour)).
108
+
109
+ **An update that renames something ships a migration guide** — read it before the restart. The names that were
110
+ renamed from TelegramCode to Charness (environment variables, the data dir, the Docker names, …) still work for
111
+ now and print a deprecation line at start; the steps are in
112
+ [MIGRATION_RENAME_TO_CHARNESS.md](MIGRATION_RENAME_TO_CHARNESS.md), the summary in the top entry of
113
+ [CHANGELOG.md](CHANGELOG.md). `scripts/self-update.sh` names a newly added migration guide in its log.
114
+
115
+ #### Updating a checkout (self-update, no root)
116
+
117
+ A checkout — the one the [Docker container](#run-with-docker) runs, or a clone from
118
+ [DEVELOPMENT.md](DEVELOPMENT.md#local-development) — updates with `scripts/self-update.sh` (not in the npm package).
119
+ It refreshes the checkout in place, running as the checkout's owning user — no root, no cross-user copying. It is
120
+ safe to run blindly (e.g. from cron):
121
+
122
+ - fast-forward only: it skips silently when the tree has tracked changes, a
123
+ merge/rebase is in progress, or local history diverged from the upstream —
124
+ a dev clone that is ahead of origin is never touched;
125
+ - `yarn install --immutable` runs only when `yarn.lock` / `package.json`
126
+ changed;
127
+ - a hot-mode instance (`charness hot`) rebuilds and restarts the worker
128
+ by itself (after a dependency install the script touches `tsconfig.json`
129
+ so `tsc -w` recompiles with the new modules); a non-hot instance gets
130
+ `yarn build` plus a restart notice — the script never kills processes;
131
+ - changes to the hot supervisor itself (`src/cli.ts`, `src/cli/hot.ts`,
132
+ `nodemon.json`) are flagged with a warning: those need a manual
133
+ `charness hot` restart (nodemon reloads only the worker).
134
+
135
+ For unattended updates give the deploying user read access to the repo (e.g.
136
+ a read-only GitLab deploy key on a passphrase-less SSH key) and add a cron
137
+ entry:
138
+
139
+ ```cron
140
+ */10 * * * * /path/to/charness/scripts/self-update.sh >> "$HOME/.local/state/charness/self-update.log" 2>&1
141
+ ```
142
+
143
+ (create the log directory once: `mkdir -p ~/.local/state/charness`).
144
+
145
+ ## Telegram setup
146
+
147
+ Four steps: create a bot, (optionally) set up a group, launch from your projects folder, bind a topic to an agent.
148
+ The CLI is installed first — see [Install the CLI](#install-the-cli). After the steps: the
149
+ [required files structure](#required-files-structure) and the
150
+ [Telegram environment variables](#telegram-environment-variables).
151
+
152
+ > **[manual]** tags a step you must do by hand in the Telegram app — an agent
153
+ > running on the host can't do it for you. Everything unmarked is a host-side
154
+ > command (or file edit) that can be automated.
155
+
156
+ ### 1. Create the bot
157
+
158
+ **[manual]** In Telegram, with @BotFather:
159
+
160
+ 1. Message [@BotFather](https://t.me/BotFather), send `/newbot`, follow prompts.
161
+ 2. Save the token (`123456789:ABCdef...`).
162
+ 3. **Disable privacy mode** — same chat: `/setprivacy` → pick this bot →
163
+ `Disable`. Without this the bot only sees `/commands`, not free-form text.
164
+
165
+ <p><img src="docs/images/01-botfather.png" width="360" alt="@BotFather: /newbot to get the token, then /setprivacy → this bot → Disable" /></p>
166
+
167
+ Put the token where the bot reads it — the global config `.env`:
168
+
169
+ ```bash
170
+ mkdir -p ~/.config/charness
171
+ $EDITOR ~/.config/charness/.env
172
+ ```
173
+
174
+ ```dotenv
175
+ # ~/.config/charness/.env — the bot token and the instance settings
176
+ TELEGRAM_BOT_TOKEN=123456789:ABCdef...
177
+ # OWNER_USER_ID=... # set to drive the bot from its DM (step 2)
178
+ ```
179
+
180
+ The bot token lives in this file; the voice-recognition keys live next to it, in
181
+ [`stt.json`](#voice-input). (Agent provider auth is the exception, and lives
182
+ inside the agents themselves: `claude login` for Claude, `/connect` or
183
+ OpenCode's own config for OpenCode — see
184
+ [Environment Variables](#environment-variables).) The wrapper loads env
185
+ from two locations, in order — a global base and an optional per-project
186
+ override:
187
+
188
+ 1. `~/.config/charness/.env` — base, set once, used everywhere.
189
+ 2. `$PWD/.env` — per-project tweaks (last write wins).
190
+
191
+ The full annotated template is `.env.example` (shipped with the package and in
192
+ the repo).
193
+
194
+ ### 2. Set up a group (optional) or DM
195
+
196
+ The bot serves a **forum supergroup**, the **owner's private chat (DM)**, or
197
+ both at once (`CHAT_MODE`, default `both`). The group is the nicest UX — a
198
+ visible topic list, per-topic names/icons, quick agent switching — but it is
199
+ **optional**: with `OWNER_USER_ID` set you can run entirely from the bot's DM.
200
+
201
+ **To use a group (recommended)** — **[manual]**, all in the Telegram client:
202
+
203
+ 1. In a Telegram client: `New Group` → name it → add the bot.
204
+ 2. Open group settings → enable **Topics** (Forum mode).
205
+ 3. Promote the bot to admin with these rights:
206
+ - `Manage Topics` (required — bind/create threads)
207
+ - `Delete Messages` (for `/clear_messages`)
208
+ - `Pin Messages` (per-thread status banner + question alerts; `/doctor`
209
+ warns if missing, but the bot still runs without it)
210
+ 4. **Remove the bot from the group and add it again.** Telegram caches the
211
+ privacy-mode flag on join; without re-adding, the bot keeps private-mode and
212
+ ignores free-form messages (see
213
+ [Troubleshooting → Bot doesn't see text](README.md#bot-doesnt-see-text-telegram)).
214
+
215
+ <p>
216
+ <img src="docs/images/02-enable-topics.png" width="360" alt="Group settings → Topics toggle enabled (Forum mode)" />
217
+ <img src="docs/images/03-promote-admin.png" width="360" alt="Bot admin rights: Manage Topics, Delete Messages, Pin Messages ticked" />
218
+ </p>
219
+
220
+ The group **id is automatic** — leave `ALLOWED_GROUP_ID` empty and the bot
221
+ auto-pairs with the first forum supergroup an admin talks to it from (the
222
+ `-100…` id is saved to `state.json`; re-point later with `/pair`). To pin a
223
+ specific id by hand, set `ALLOWED_GROUP_ID` to the numeric value (a group
224
+ **name** is not accepted) — that also disables auto-pairing. **Access control:**
225
+ whoever is a creator/administrator of that group may drive the bot — there is no
226
+ separate allow-list.
227
+
228
+ **To use the DM instead (or as well):** **[manual]** send `/start` to
229
+ [@userinfobot](https://t.me/userinfobot) for your numeric user id, then add it
230
+ (host-side) to the same `.env`:
231
+
232
+ ```dotenv
233
+ OWNER_USER_ID=987654321
234
+ ```
235
+
236
+ With the default `CHAT_MODE=both`, an unset `OWNER_USER_ID` just leaves the DM
237
+ surface inert (group-only); set it and the DM lights up alongside the group. Use
238
+ `CHAT_MODE=dm` to skip the group entirely.
239
+
240
+ ### 3. Launch from your projects folder
241
+
242
+ ```bash
243
+ cd ~/projects && charness
244
+ ```
245
+
246
+ That `$PWD` — the parent folder holding your repositories — is the folder the
247
+ bot works in; each topic binds to a subfolder under it. There's nothing else to
248
+ configure for it. A single-instance lockfile (`$DATA_DIR/instance.lock`) stops a
249
+ second bot starting under the same user; stale locks (after `kill -9`) are
250
+ reclaimed automatically.
251
+
252
+ ### 4. Bind a topic and talk to an agent
253
+
254
+ This step is **[manual]** — it all happens inside the Telegram app.
255
+
256
+ **In the group:**
257
+
258
+ 1. Open the group; in the General topic send `/ls` — the bot lists subfolders
259
+ under the work root.
260
+ 2. Create a topic (the `+` button).
261
+ 3. In the new topic: `/bind <subdir>` (auto-bound if the topic name matches a
262
+ subdir).
263
+ 4. `/claude` or `/opencode` → talk to the agent (text or voice).
264
+
265
+ <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>
266
+
267
+ **In the DM:** the same commands, one thread per agent — `/ls`,
268
+ `/bind <subdir>`, then `/claude` or `/opencode`.
269
+
270
+ Then: `/quit` ends the session; bare `/bind` inspects/reconfigures the binding;
271
+ `/clear_messages` deletes the topic's bot messages.
272
+
273
+ > **Continue the same session from a terminal.** No wrapper needed: run
274
+ > `claude --dangerously-skip-permissions` in the project folder. It is the same
275
+ > binary and the same `~/.claude/projects/<cwd-slug>/` session store the bot
276
+ > uses, so a thread started in Telegram can be picked up with `claude --resume`
277
+ > and vice versa.
278
+
279
+ ### Voice input
280
+
281
+ Open **`/stt`** in any topic (General included) to set voice recognition up with buttons: add Groq, OpenAI or your own
282
+ OpenAI-compatible server (the key is pasted into the chat and deleted at once), reorder or switch off engines, set each
283
+ engine's timeout and the language, and enable the [local engine](#local-speech-recognition-gigaam). Every tap is saved to
284
+ the file below and applies to the next voice note. The rest of this section is for hand editing.
285
+
286
+ Voice notes are transcribed by an **ordered chain of speech engines**: the first available one is tried,
287
+ and when it fails (refused connection, timeout, an error answer) the next one answers the same voice note
288
+ at once. Only the last engine retries a temporary failure (after 5 s, then 15 s). An empty transcript is
289
+ final — silence is never "fixed" by asking another engine.
290
+
291
+ Everything lives in one file, **`stt.json`**, in the same directory as your real `.env`
292
+ (`~/.config/charness/stt.json`; for an isolated instance, beside its `ENV_FILE`). If that `.env` is a symlink — the
293
+ keys kept on an encrypted volume, say — `stt.json` goes beside the symlink's **target**, so the API keys stay where
294
+ the rest of your secrets are and never land in the directory that only holds the link. **That directory must be
295
+ writable and survive a restart** — under Docker, mount the config directory, not only the env file
296
+ ([Run with Docker](#run-with-docker)); when the file cannot be written, `/stt` says so and names the path, and the boot
297
+ log says it once (`[STT] … cannot be written`). It holds the settings and the API keys, is written with mode
298
+ `0600`, and is re-read when it changes — an edit applies to the next voice note, no restart. If the file is
299
+ invalid, the bot keeps the last good settings, logs a `[STT]` line naming the field (never a value), and
300
+ refuses voice with a notice when nothing valid was ever loaded. A voice note that no engine can take gets a notice
301
+ that names the cause: nothing set up (or all switched off), the local engine still installing, or engines listed but not
302
+ usable right now.
303
+
304
+ ```json
305
+ {
306
+ "engines": [
307
+ { "id": "q1", "kind": "groq", "apiKey": "gsk_..." },
308
+ { "id": "o1", "kind": "openai", "apiKey": "sk-...", "timeout": { "baseSeconds": 10, "percentOfDuration": 25 } },
309
+ { "id": "u1", "kind": "url", "url": "http://127.0.0.1:8000/v1/audio/transcriptions", "model": "whisper-1" }
310
+ ],
311
+ "language": "ru"
312
+ }
313
+ ```
314
+
315
+ | Field | Meaning |
316
+ |---|---|
317
+ | `engines` | Ordered, at most 6. `kind`: `groq`, `openai`, `url` (any OpenAI-compatible `/audio/transcriptions` endpoint — give the full URL; `apiKey` is optional there) or `gigaam` (the local engine). `id` is 1–8 lowercase letters or digits, unique. `isDisabled: true` keeps an engine listed but unused |
318
+ | `timeout` | Per engine: `baseSeconds` + `percentOfDuration` % of the voice's length (default 10 s + 25 %), so a 30 s voice gets 17.5 s and a 5 min voice 85 s |
319
+ | `language` | Two-letter code (`ru`, `en`, …) sent to the engines as a hint; leave it out to let the engine detect the language |
320
+ | `gigaam` | `idleMinutes` (default 15, `0` = never) and `workers` (1, 2, 4 or 8; default 4) of the local engine |
321
+
322
+ An engine without what it needs (no key, no URL) is skipped and logged once. With no usable engine, a voice
323
+ note is answered with a pointer to this setup.
324
+
325
+ **Coming from `GROQ_API_KEY` / `OPENAI_API_KEY` in `.env`?** On the first start without a `stt.json`, the bot
326
+ writes one from those keys (Groq first, then OpenAI) and logs
327
+ `[deprecated] GROQ_API_KEY/OPENAI_API_KEY in .env is deprecated, voice settings now live in stt.json`. From then
328
+ on the env keys are ignored — edit `stt.json` instead.
329
+
330
+ #### Local speech recognition (GigaAM)
331
+
332
+ Besides the hosted engines, the bot can recognise Russian voice notes **on your own machine** with GigaAM v3 (an
333
+ `onnx-asr` model, int8). It is a `gigaam` engine in `stt.json`, added from `/stt` («➕ Add engine → GigaAM (local)»; when the model is not installed yet, with a confirmation and a live progress message) or
334
+ by adding `{ "id": "g", "kind": "gigaam" }` to `stt.json` by hand — in that case the bot installs it by itself at the
335
+ next start and shows the progress in General (or in its log).
336
+
337
+ - **Needs** `uv` (on `PATH` or in `~/.local/bin`), or `python3` ≥ 3.10 with the venv module (`apt install python3-venv`
338
+ on Debian/Ubuntu). The install builds a private venv — your Python is left alone.
339
+ - **Disk:** about 400 MB under `<DATA_DIR>/stt` (the model is 216 MB; the rest is the Python packages and their cache).
340
+ - **RAM:** nothing while it is not running. The recogniser process starts with the first voice note that needs it (about
341
+ +2–3 s once), uses about 0.5 GB for short voices and up to about 0.9 GB while a 5-minute voice is recognised with the
342
+ default 4 workers, and exits by itself after 15 idle minutes (`gigaam.idleMinutes`, `0` = never). Idle time, workers,
343
+ «Try again now» and removing the model files are on the engine's card in `/stt`, not on the main screen.
344
+ - **Speed** (4 cores, loaded): a short voice in 1–2 s, a 5-minute voice in about 20 s — long voices are cut at pauses and
345
+ recognised in parallel. A hot reload of the bot keeps a running recogniser; stopping or killing the bot takes it down.
346
+ - **Russian only.** English words come out in Cyrillic or garbled; with a `language` other than `ru` set, the engine is
347
+ skipped and the next one in the chain answers.
348
+ - It listens on `127.0.0.1` only, on a port the OS picks, and answers only a per-start secret token.
349
+ - **Docker is not supported yet** (the image has no `python3-venv`).
350
+
351
+ ### Instance settings (`settings.json`)
352
+
353
+ Settings that belong to the whole instance, not to a topic, live in one file, **`settings.json`**, in the same
354
+ directory as your real `.env` and `stt.json` (`~/.config/charness/settings.json`; for an isolated instance, beside its
355
+ `ENV_FILE`; a symlinked `.env` is resolved exactly as for `stt.json`). The directory must be writable and survive
356
+ restarts — under Docker the mounted config directory covers it ([Run with Docker](#run-with-docker)); when it cannot
357
+ be written, the tap that needed it says so and names the path. The file is optional and is re-read when it changes:
358
+ a hand edit applies to the next session start, no restart.
359
+
360
+ ```json
361
+ { "defaultEffort": "medium" }
362
+ ```
363
+
364
+ | Field | Meaning |
365
+ |---|---|
366
+ | `defaultEffort` | The reasoning effort of every topic that has no `/effort` pick of its own: `low`, `medium`, `high`, `xhigh`, `max`, `auto` or `ultracode`. Set it from **`/effort` in the General topic** (a tap writes the file), or by hand. Leave it out and the built-in default, `medium`, applies |
367
+
368
+ What a new Claude session starts with, first that exists: the topic's own `/effort` pick, the platform's default
369
+ (a Jira conversation: `effort` in `jira.json`, `high` when absent — Jira never reads `defaultEffort`), `defaultEffort`,
370
+ then `medium`. A change applies from each topic's next session start
371
+ or wake-up — running sessions are not restarted, and `/status`, the pinned banner and `/effort` show the level a running
372
+ session really uses, with the next start's beside it («xhigh (medium from the next start)»). OpenCode takes the same
373
+ level and runs the nearest variant its model offers (`ultracode` is the model's highest variant, `auto` sends no variant). In a topic, `/effort` shows the level and whether it is the topic's own or «as in General», and «As in
374
+ General» drops the topic's pick (a live session is brought to the default at once).
375
+
376
+ The file is checked strictly: an unknown level or a mistyped key is logged once as `[Settings] settings.json
377
+ defaultEffort: …` (naming the field, never echoing a value) and the built-in defaults apply until it is fixed — a
378
+ stale copy of an earlier valid file is not used. A `/effort` tap in General over a broken file replaces it.
379
+
380
+ ### Required files structure
381
+
382
+ Launch `charness` from the parent folder that holds your project repos —
383
+ that `$PWD` is the work root, and every topic binds to a subfolder under it:
384
+
385
+ ```
386
+ $PWD=/home/user/src ← run the bot here ($PWD = work root)
387
+ ├── projectA/ ← Topic "projectA" (claude)
388
+ ├── projectB/ ← Topic "projB-frontend" (claude)
389
+ │ ← Topic "projB-backend" (opencode) ← one folder, two topics
390
+ │ ← Topic "projB-refactor" (claude)
391
+ └── projectC/ ← Topic "projectC" (opencode)
392
+
393
+ Telegram forum supergroup
394
+ ├── Bot is admin with can_manage_topics + can_delete_messages + can_pin_messages
395
+ └── Routes each topic by (chatId, threadId) → remembered in state.json
396
+ ```
397
+
398
+ **What the bot remembers (`state.json`).** So a restart is seamless, the bot
399
+ saves which topic is bound to which folder, each topic's live agent session (to
400
+ re-attach it after a reboot), your per-topic preferences (agent, model,
401
+ reasoning effort, language, display verbosity), scheduled prompts, and the
402
+ paired group id. This file lives in `DATA_DIR` (default `~/.charness`) and
403
+ is written safely, so a crash can't corrupt it.
404
+
405
+ ### Telegram environment variables
406
+
407
+ The settings of the Telegram surface; the ones every surface shares are under
408
+ [Environment variables](#environment-variables).
409
+
410
+ #### Set Before Launch
411
+
412
+ | Variable | Description |
413
+ |---|---|
414
+ | `TELEGRAM_BOT_TOKEN` | Token from @BotFather. Telegram only: a Jira-only instance refuses to start with it set — see [Jira setup](#jira-setup) |
415
+
416
+ Start from the parent folder containing your projects: `cd ~/projects && charness`.
417
+ That `$PWD` is the folder the bot works in — nothing else to set for it.
418
+
419
+ **Access control.** There is no user allow-list. Whoever is a **creator or
420
+ administrator of the served forum group** may talk to the agent — read live from
421
+ Telegram (`getChatAdministrators`) and cached for 1h. Promote someone in the
422
+ group to grant access; demote/remove them to revoke it — the bot subscribes to
423
+ `chat_member` updates, so an admin change invalidates the cache and takes
424
+ effect immediately (the 1h TTL is only the fallback). The bot must be a group
425
+ admin itself (it already needs that to create topics and pin). Anonymous admins
426
+ can't be matched from their messages, so post non-anonymously. The DM surface
427
+ (when enabled) is gated separately: only the configured `OWNER_USER_ID` may
428
+ talk to the bot there.
429
+
430
+ > **Security model — group admin (or the DM owner) ⇒ shell on the host.** The agents run with
431
+ > permission checks disabled (`--dangerously-skip-permissions` for Claude,
432
+ > auto-approve for OpenCode) and `/terminal` opens a raw `$SHELL` in the bound
433
+ > folder, so anyone who can talk to the bot can execute arbitrary commands as
434
+ > the bot's OS user. Only promote people you would trust with SSH access to
435
+ > that machine, and keep the served group itself private.
436
+
437
+ #### External Optional (Telegram)
438
+
439
+ | Variable | Default | Description |
440
+ |---|---|---|
441
+ | `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 |
442
+ | `CHAT_MODE` | `both` | Which surface(s) this instance serves: `group`, `dm`, or `both` — see [Two surfaces](README.md#two-surfaces-group-topics-bot-dm-or-both) |
443
+ | `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` |
444
+
445
+ **Localized in 12 languages** — the bot UI (commands, buttons, notices) speaks
446
+ 中文, English, Français, ქართული, Deutsch, हिन्दी, 日本語, Português, Русский,
447
+ Español, Українська, Oʻzbekcha. The language is automatic per Telegram chat:
448
+ explicit `/language <locale>` override wins, then the sender's Telegram client
449
+ language, then the last supported Telegram locale seen for that chat, then
450
+ English.
451
+ Supported locales: `en`, `de`, `fr`, `es`, `pt`, `ru`, `zh`, `ja`, `hi`, `uz`,
452
+ `ka`, `uk`. Bare `/language` opens a single-message inline picker with each language
453
+ shown by its own name (endonym), sorted A→Z by English name, and a `🌐 Auto` button
454
+ (all 12 fit at once — no pagination). Tapping a language (or `🌐 Auto`) applies it, then the menu
455
+ disappears and a short confirmation stays in the chosen language;
456
+ `/language auto` returns a DM/group to automatic selection.
457
+
458
+ #### Advanced (Telegram)
459
+
460
+ | Variable | Default | Set only when |
461
+ |---|---|---|
462
+ | `SHELL` | `/bin/bash` | You want `/terminal` to open a different shell than your login shell (host env var, not set by the bot) |
463
+
464
+ ## Jira setup
465
+
466
+ A Jira instance runs on its own — a `DATA_DIR`, a work root, a private tmux server and one env file. How the
467
+ connector behaves is described in [`src/connectors/jira/README.md`](src/connectors/jira/README.md).
468
+
469
+ 1. Create a Jira account for the AI (its API token is the instance's only secret) and note its account id.
470
+ 2. Recommended: create or choose a Jira group of the people whose words may steer the AI (for example
471
+ `ai-trusted`), and give the AI account the *Browse users and groups* global permission so that it can read
472
+ the group. Then the agent reads only the text and files of its members and of Jira's apps and automations,
473
+ and only their hand-overs start it.
474
+ 3. An instance of its own: a `DATA_DIR`, a `WORK_ROOT` with one folder per project, a private tmux server, and
475
+ an env file that is the ONLY configuration it reads:
476
+
477
+ ```bash
478
+ # /path/to/jira-instance.env
479
+ CONNECTORS=jira
480
+ DATA_DIR=/path/to/jira-instance/data
481
+ WORK_ROOT=/path/to/jira-instance/work
482
+ TMUX_SOCKET_NAME=jira-instance
483
+ OPENCODE_URL=http://localhost:4097 # a port of its own — the default 4096 is refused (other instances use it)
484
+ JIRA_AI_API_TOKEN=... # referenced by jira.json below, never written there
485
+ ```
486
+
487
+ 4. `DATA_DIR/jira.json` — the allowlist, the trusted group and the credentials (`${VAR}` expands from the env file; an unknown
488
+ key is an error):
489
+
490
+ ```json
491
+ {
492
+ "site": "example.atlassian.net",
493
+ "email": "ai-account@example.com",
494
+ "apiToken": "${JIRA_AI_API_TOKEN}",
495
+ "accountId": "<the AI account's id>",
496
+ "projects": {
497
+ "PROJ": { "folder": "proj", "triggerStatuses": ["In Progress"], "extraFields": ["customfield_10042"] }
498
+ },
499
+ "trustedGroup": "ai-trusted",
500
+ "wakeOnMention": true,
501
+ "pollIntervalSeconds": 30,
502
+ "runBudgetPer24h": 5,
503
+ "model": "opus",
504
+ "effort": "high"
505
+ }
506
+ ```
507
+
508
+ `trustedGroup` (optional, but without it everyone who can edit or comment steers the agent) is the group's
509
+ NAME — one group per instance; a team with several trust circles runs a second instance. The instance
510
+ resolves it to the group's id at start, so renaming the group while it runs keeps working, and reads its
511
+ members again whenever its last read is older than 5 minutes: a removed member loses the AI's trust within
512
+ that time. List only projects where only your team can write, and where no automation acts on text or
513
+ actions from outside it — an e-mail handler, a portal form, a copy from another project, a rule that
514
+ hands an issue to the AI when someone outside the team acts: an app's or automation's text and hand-over
515
+ are trusted, so such a rule brings that outside text, or that person's hand-over, to the agent. Nor list a
516
+ project where anyone outside the team holds the *Edit all comments* permission: Jira Cloud keeps no history
517
+ of a comment's edits, so a comment is judged by its author and its LAST editor only — an outsider's edit of a
518
+ member's comment, followed by a member's own edit, reads as trusted.
519
+
520
+ `wakeOnMention` (optional) decides whether an @-mention of the AI account in a comment wakes it to answer
521
+ only, without the issue being handed over; absent, it is on exactly when `trustedGroup` is set, and `true`
522
+ without a group is refused — without one, anyone who can comment could start the AI.
523
+
524
+ `projects` maps each served project key to its folder under `WORK_ROOT` and the status names in which an
525
+ assigned issue is a request; the agent loads that folder's project memory (`CLAUDE.md`, `.claude/`, … in it
526
+ and in every parent) like any Claude Code session, while your user-level Claude setup stays out of its
527
+ sessions — except that for a folder under HOME that walk reads `~/.claude/CLAUDE.md` back in.
528
+ `pollIntervalSeconds` is 10–600 (default 30);
529
+ `runBudgetPer24h` defaults to 5; `model` and `effort` set the sessions' Claude model and reasoning effort
530
+ (optional; absent → `opus` and `high`, either key overrides only its own default). `extraFields` (optional,
531
+ none by default) names fields by id (`customfield_…`, or a system one like `duedate`) the agent should see next to the standard
532
+ 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
533
+ 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
534
+ idles) or `claude-per-turn` (stopped after every answer); either way the issue's conversation sleeps between
535
+ requests and the next request resumes it.
536
+
537
+ 5. Start it with a clean environment, so nothing of the calling shell reaches it:
538
+
539
+ ```bash
540
+ env -i HOME="$HOME" PATH="$PATH" LANG=C.UTF-8 ENV_FILE=/path/to/jira-instance.env charness
541
+ ```
542
+
543
+ (In a checkout, `scripts/run-isolated.sh /path/to/jira-instance.env` does the same.)
544
+
545
+ The start refuses a missing `ENV_FILE`, `TMUX_SOCKET_NAME` or `jira.json`, an `OPENCODE_URL` on the default
546
+ port, a `TELEGRAM_BOT_TOKEN` or any `ATLASSIAN_*` variable when Telegram is off, a token that belongs to
547
+ another account, an unknown project or status, a `trustedGroup` the site does not have or the AI account
548
+ may not read, and a `DATA_DIR` holding another surface's state. An unreachable Jira does not refuse the start —
549
+ polling backs off and resumes.
550
+
551
+ The agent sessions of a Jira instance run without the user-level Claude settings, hooks and skills, with the
552
+ bot's MCP server only, without the native question tool, and with an environment reduced to a short allowlist
553
+ — the Jira token is not in the agent's environment. The agent still runs as the instance's OS user and can read
554
+ the instance's env file and `DATA_DIR`, so run the instance under an OS user that holds nothing else, with an AI
555
+ account that sees only the allowlisted projects. **A production Jira account runs only in the Docker container**
556
+ ([Run with Docker](#run-with-docker)): there the agent can still read the instance's env file and `DATA_DIR`,
557
+ but nothing of the host outside the container's mounts. Module details: `src/connectors/jira/README.md`.
558
+
559
+ ## Alternatives
560
+
561
+ The npm install above is the way to run Charness; this is the alternative to it.
562
+
563
+ ### Run with Docker
564
+
565
+ Choose this over the npm install for **isolation** — the agents reach only the folders you mount — or for **a ready
566
+ environment**: the image brings Node 22, tmux, git and ffmpeg, and the bot installs Claude Code and OpenCode the first
567
+ time it starts each, so the host needs only Docker and a clone of this repository.
568
+
569
+ The whole bot runs in one container: the bot itself, tmux, every agent it starts (Claude Code in any backend,
570
+ `/terminal`, OpenCode and its server) and the bot's MCP server. Everything works as on a host; the host only
571
+ lends the container the folders it mounts, so the bot and its agents see nothing else of the machine.
572
+
573
+ ```
574
+ host container charness-<instance>
575
+ your config directory ──────▶ the bot (hot mode) + a restart loop;
576
+ (instance.env and stt.json) the env file is read, stt.json is written
577
+ DATA_DIR, WORK_ROOT ──────▶ tmux, the agents, OpenCode, the bot MCP
578
+ a clone of this repository ──────▶ /opt/charness
579
+ the agents' home folder ──────▶ /home/charness: their Claude / OpenCode
580
+ login, sessions and CLI installs
581
+ ```
582
+
583
+ 1. **Image and checkout.** Clone the repository for the container (a full clone of its own, not a worktree and
584
+ not the checkout a host instance runs from) and build the image from it:
585
+
586
+ ```bash
587
+ git clone https://github.com/olosegres/charness /srv/charness/checkout
588
+ cd /srv/charness/checkout && docker build -t charness -f docker/Dockerfile .
589
+ ```
590
+
591
+ The image holds only system tools (tmux, git, ffmpeg, ripgrep, …) and the yarn the checkout's `package.json`
592
+ names (build from the checkout's root, which is how the build reads it; rebuild after a yarn bump so no
593
+ container has to download it). On first start the container installs the checkout's dependencies for the
594
+ image and builds it; the bot installs Claude Code and OpenCode into the agents' home.
595
+ 2. **Instance config directory**, with the env file as for a host install inside it (`TELEGRAM_BOT_TOKEN`,
596
+ `DATA_DIR`, `WORK_ROOT`, …): `/srv/charness/config/instance.env`. It stays on the host and the **directory** is
597
+ mounted writable, not just the file: the voice settings (`stt.json`, with the speech-engine API keys) and the instance settings (`settings.json`) are saved
598
+ beside the env file, and a container's own disk is gone when the container is re-created. Every host path the
599
+ env file names (`DATA_DIR`, `WORK_ROOT`, `TMUX_TMPDIR`, …) is mounted at the SAME path, so `state.json`,
600
+ sessions and file paths stay valid inside and an instance can move between host and container unchanged.
601
+ 3. **Run** (or use `examples/docker-compose.yml`):
602
+
603
+ ```bash
604
+ docker run -d --name charness-main --hostname charness-main \
605
+ --restart unless-stopped --stop-timeout 60 \
606
+ -v /srv/charness/checkout:/opt/charness \
607
+ -v /srv/charness/home:/home/charness \
608
+ -v /srv/charness/data:/srv/charness/data \
609
+ -v /srv/projects:/srv/projects \
610
+ -v /srv/charness/config:/srv/charness/config \
611
+ -e ENV_FILE=/srv/charness/config/instance.env \
612
+ charness
613
+ ```
614
+
615
+ No port is published: Telegram is long polling, and the bot's MCP server and the OpenCode server listen on the
616
+ container's own loopback. Keep the `--hostname` fixed and unique per instance: the instance lock tells its own
617
+ restarted container from another machine by it.
618
+ 4. **Log the agents in once**, with the account the agents should use (a separate one is best):
619
+ `docker exec -it charness-main claude`, then `/login` (or `/login` in a topic). OpenCode: `/connect` in a
620
+ topic, or `docker exec -it charness-main opencode auth login`. Never copy your own `~/.claude` login in:
621
+ a second holder of its refresh token can log the first one out. For git, put a `.gitconfig` and an SSH key of
622
+ their own into the agents' home.
623
+
624
+ Running it:
625
+
626
+ - **Restart the bot only** (agents keep running and are adopted): `docker exec charness-main
627
+ charness-restart-bot`. A crashed bot is restarted by the container after 20 s.
628
+ - **Update the bot**: `docker exec charness-main /opt/charness/scripts/self-update.sh` (by hand or from
629
+ a timer on the host); hot mode picks the change up. A change to `src/cli.ts`, `src/cli/hot.ts` or `nodemon.json`
630
+ needs the bot restart above. **Update Claude Code / OpenCode**: they update themselves in the agents' home, or
631
+ `docker exec charness-main npm install -g @anthropic-ai/claude-code` / `… opencode-ai`.
632
+ - **A container restart is a reboot**: every agent process ends; each conversation resumes with its next message.
633
+ - **Rootless Docker** (recommended): the container's root is your user, so everything written on the mounts is
634
+ yours. **Rootful Docker**: add `--user "$(id -u):$(id -g)"` and give that user the agents' home.
635
+ - **Upgrading a container made with the single-file mount** (`INSTANCE_ENV_FILE`, the env file mounted `:ro`): move the
636
+ env file into a directory of its own, mount the directory as in step 3 and set `ENV_FILE` to the file inside it. Until
637
+ then `stt.json` sits on the container's own disk and is lost when the container is re-created; the bot says so at boot
638
+ (`[STT] … is on this container's own disk`).
639
+ - **Moving a host instance in**: stop it first (the lock refuses a lock written on another host), mount its
640
+ folders at their paths, and copy its conversations' folders from `~/.claude/projects/` into the agents' home
641
+ `.claude/projects/` — without them each conversation starts a fresh session on its next message.
642
+ - **What the agents can read**: everything mounted, the instance env file with its tokens, `DATA_DIR` and the
643
+ checkout included. The container keeps the rest of the host out of reach; running the agents as a second user
644
+ inside the container would close that gap later.