@shuyhere/takotako 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (653) hide show
  1. package/CONTRIBUTING.md +84 -0
  2. package/LICENSE +21 -0
  3. package/README.md +171 -0
  4. package/dist/agents/communication.d.ts +48 -0
  5. package/dist/agents/communication.d.ts.map +1 -0
  6. package/dist/agents/communication.js +123 -0
  7. package/dist/agents/communication.js.map +1 -0
  8. package/dist/agents/config.d.ts +52 -0
  9. package/dist/agents/config.d.ts.map +1 -0
  10. package/dist/agents/config.js +65 -0
  11. package/dist/agents/config.js.map +1 -0
  12. package/dist/agents/model-catalog.d.ts +49 -0
  13. package/dist/agents/model-catalog.d.ts.map +1 -0
  14. package/dist/agents/model-catalog.js +79 -0
  15. package/dist/agents/model-catalog.js.map +1 -0
  16. package/dist/agents/registry.d.ts +71 -0
  17. package/dist/agents/registry.d.ts.map +1 -0
  18. package/dist/agents/registry.js +297 -0
  19. package/dist/agents/registry.js.map +1 -0
  20. package/dist/agents/roles.d.ts +79 -0
  21. package/dist/agents/roles.d.ts.map +1 -0
  22. package/dist/agents/roles.js +174 -0
  23. package/dist/agents/roles.js.map +1 -0
  24. package/dist/agents/subagent.d.ts +124 -0
  25. package/dist/agents/subagent.d.ts.map +1 -0
  26. package/dist/agents/subagent.js +352 -0
  27. package/dist/agents/subagent.js.map +1 -0
  28. package/dist/agents/templates.d.ts +18 -0
  29. package/dist/agents/templates.d.ts.map +1 -0
  30. package/dist/agents/templates.js +341 -0
  31. package/dist/agents/templates.js.map +1 -0
  32. package/dist/agents/thread-binding.d.ts +77 -0
  33. package/dist/agents/thread-binding.d.ts.map +1 -0
  34. package/dist/agents/thread-binding.js +167 -0
  35. package/dist/agents/thread-binding.js.map +1 -0
  36. package/dist/auth/agent-profiles.d.ts +46 -0
  37. package/dist/auth/agent-profiles.d.ts.map +1 -0
  38. package/dist/auth/agent-profiles.js +97 -0
  39. package/dist/auth/agent-profiles.js.map +1 -0
  40. package/dist/auth/allow-from.d.ts +27 -0
  41. package/dist/auth/allow-from.d.ts.map +1 -0
  42. package/dist/auth/allow-from.js +118 -0
  43. package/dist/auth/allow-from.js.map +1 -0
  44. package/dist/auth/oauth.d.ts +66 -0
  45. package/dist/auth/oauth.d.ts.map +1 -0
  46. package/dist/auth/oauth.js +253 -0
  47. package/dist/auth/oauth.js.map +1 -0
  48. package/dist/auth/storage.d.ts +69 -0
  49. package/dist/auth/storage.d.ts.map +1 -0
  50. package/dist/auth/storage.js +157 -0
  51. package/dist/auth/storage.js.map +1 -0
  52. package/dist/cache/file-cache.d.ts +68 -0
  53. package/dist/cache/file-cache.d.ts.map +1 -0
  54. package/dist/cache/file-cache.js +176 -0
  55. package/dist/cache/file-cache.js.map +1 -0
  56. package/dist/cache/manager.d.ts +69 -0
  57. package/dist/cache/manager.d.ts.map +1 -0
  58. package/dist/cache/manager.js +117 -0
  59. package/dist/cache/manager.js.map +1 -0
  60. package/dist/cache/symbol-index.d.ts +75 -0
  61. package/dist/cache/symbol-index.d.ts.map +1 -0
  62. package/dist/cache/symbol-index.js +267 -0
  63. package/dist/cache/symbol-index.js.map +1 -0
  64. package/dist/cache/tool-cache.d.ts +75 -0
  65. package/dist/cache/tool-cache.d.ts.map +1 -0
  66. package/dist/cache/tool-cache.js +173 -0
  67. package/dist/cache/tool-cache.js.map +1 -0
  68. package/dist/channels/channel.d.ts +156 -0
  69. package/dist/channels/channel.d.ts.map +1 -0
  70. package/dist/channels/channel.js +25 -0
  71. package/dist/channels/channel.js.map +1 -0
  72. package/dist/channels/cli.d.ts +35 -0
  73. package/dist/channels/cli.d.ts.map +1 -0
  74. package/dist/channels/cli.js +94 -0
  75. package/dist/channels/cli.js.map +1 -0
  76. package/dist/channels/delivery-queue.d.ts +31 -0
  77. package/dist/channels/delivery-queue.d.ts.map +1 -0
  78. package/dist/channels/delivery-queue.js +127 -0
  79. package/dist/channels/delivery-queue.js.map +1 -0
  80. package/dist/channels/discord.d.ts +124 -0
  81. package/dist/channels/discord.d.ts.map +1 -0
  82. package/dist/channels/discord.js +664 -0
  83. package/dist/channels/discord.js.map +1 -0
  84. package/dist/channels/retry.d.ts +31 -0
  85. package/dist/channels/retry.d.ts.map +1 -0
  86. package/dist/channels/retry.js +94 -0
  87. package/dist/channels/retry.js.map +1 -0
  88. package/dist/channels/telegram.d.ts +69 -0
  89. package/dist/channels/telegram.d.ts.map +1 -0
  90. package/dist/channels/telegram.js +499 -0
  91. package/dist/channels/telegram.js.map +1 -0
  92. package/dist/channels/tui.d.ts +42 -0
  93. package/dist/channels/tui.d.ts.map +1 -0
  94. package/dist/channels/tui.js +126 -0
  95. package/dist/channels/tui.js.map +1 -0
  96. package/dist/cli/acp.d.ts +10 -0
  97. package/dist/cli/acp.d.ts.map +1 -0
  98. package/dist/cli/acp.js +69 -0
  99. package/dist/cli/acp.js.map +1 -0
  100. package/dist/cli/audit.d.ts +11 -0
  101. package/dist/cli/audit.d.ts.map +1 -0
  102. package/dist/cli/audit.js +55 -0
  103. package/dist/cli/audit.js.map +1 -0
  104. package/dist/cli/cache.d.ts +10 -0
  105. package/dist/cli/cache.d.ts.map +1 -0
  106. package/dist/cli/cache.js +77 -0
  107. package/dist/cli/cache.js.map +1 -0
  108. package/dist/cli/config.d.ts +5 -0
  109. package/dist/cli/config.d.ts.map +1 -0
  110. package/dist/cli/config.js +168 -0
  111. package/dist/cli/config.js.map +1 -0
  112. package/dist/cli/cron.d.ts +5 -0
  113. package/dist/cli/cron.d.ts.map +1 -0
  114. package/dist/cli/cron.js +192 -0
  115. package/dist/cli/cron.js.map +1 -0
  116. package/dist/cli/extensions.d.ts +5 -0
  117. package/dist/cli/extensions.d.ts.map +1 -0
  118. package/dist/cli/extensions.js +53 -0
  119. package/dist/cli/extensions.js.map +1 -0
  120. package/dist/cli/logs.d.ts +5 -0
  121. package/dist/cli/logs.d.ts.map +1 -0
  122. package/dist/cli/logs.js +49 -0
  123. package/dist/cli/logs.js.map +1 -0
  124. package/dist/cli/memory.d.ts +5 -0
  125. package/dist/cli/memory.d.ts.map +1 -0
  126. package/dist/cli/memory.js +78 -0
  127. package/dist/cli/memory.js.map +1 -0
  128. package/dist/cli/message.d.ts +5 -0
  129. package/dist/cli/message.d.ts.map +1 -0
  130. package/dist/cli/message.js +69 -0
  131. package/dist/cli/message.js.map +1 -0
  132. package/dist/cli/service.d.ts +14 -0
  133. package/dist/cli/service.d.ts.map +1 -0
  134. package/dist/cli/service.js +181 -0
  135. package/dist/cli/service.js.map +1 -0
  136. package/dist/cli/symphony.d.ts +5 -0
  137. package/dist/cli/symphony.d.ts.map +1 -0
  138. package/dist/cli/symphony.js +114 -0
  139. package/dist/cli/symphony.js.map +1 -0
  140. package/dist/cli/update.d.ts +5 -0
  141. package/dist/cli/update.d.ts.map +1 -0
  142. package/dist/cli/update.js +48 -0
  143. package/dist/cli/update.js.map +1 -0
  144. package/dist/commands/channel-setup.d.ts +31 -0
  145. package/dist/commands/channel-setup.d.ts.map +1 -0
  146. package/dist/commands/channel-setup.js +138 -0
  147. package/dist/commands/channel-setup.js.map +1 -0
  148. package/dist/commands/dispatch.d.ts +48 -0
  149. package/dist/commands/dispatch.d.ts.map +1 -0
  150. package/dist/commands/dispatch.js +68 -0
  151. package/dist/commands/dispatch.js.map +1 -0
  152. package/dist/commands/model-picker.d.ts +16 -0
  153. package/dist/commands/model-picker.d.ts.map +1 -0
  154. package/dist/commands/model-picker.js +120 -0
  155. package/dist/commands/model-picker.js.map +1 -0
  156. package/dist/commands/parser.d.ts +32 -0
  157. package/dist/commands/parser.d.ts.map +1 -0
  158. package/dist/commands/parser.js +39 -0
  159. package/dist/commands/parser.js.map +1 -0
  160. package/dist/commands/registry.d.ts +76 -0
  161. package/dist/commands/registry.d.ts.map +1 -0
  162. package/dist/commands/registry.js +351 -0
  163. package/dist/commands/registry.js.map +1 -0
  164. package/dist/commands/skill-commands.d.ts +35 -0
  165. package/dist/commands/skill-commands.d.ts.map +1 -0
  166. package/dist/commands/skill-commands.js +61 -0
  167. package/dist/commands/skill-commands.js.map +1 -0
  168. package/dist/config/resolve.d.ts +25 -0
  169. package/dist/config/resolve.d.ts.map +1 -0
  170. package/dist/config/resolve.js +289 -0
  171. package/dist/config/resolve.js.map +1 -0
  172. package/dist/config/schema.d.ts +520 -0
  173. package/dist/config/schema.d.ts.map +1 -0
  174. package/dist/config/schema.js +123 -0
  175. package/dist/config/schema.js.map +1 -0
  176. package/dist/core/agent-loop.d.ts +137 -0
  177. package/dist/core/agent-loop.d.ts.map +1 -0
  178. package/dist/core/agent-loop.js +700 -0
  179. package/dist/core/agent-loop.js.map +1 -0
  180. package/dist/core/audit.d.ts +87 -0
  181. package/dist/core/audit.d.ts.map +1 -0
  182. package/dist/core/audit.js +224 -0
  183. package/dist/core/audit.js.map +1 -0
  184. package/dist/core/bootstrap.d.ts +23 -0
  185. package/dist/core/bootstrap.d.ts.map +1 -0
  186. package/dist/core/bootstrap.js +162 -0
  187. package/dist/core/bootstrap.js.map +1 -0
  188. package/dist/core/context.d.ts +44 -0
  189. package/dist/core/context.d.ts.map +1 -0
  190. package/dist/core/context.js +65 -0
  191. package/dist/core/context.js.map +1 -0
  192. package/dist/core/cron.d.ts +111 -0
  193. package/dist/core/cron.d.ts.map +1 -0
  194. package/dist/core/cron.js +284 -0
  195. package/dist/core/cron.js.map +1 -0
  196. package/dist/core/exec-approvals.d.ts +50 -0
  197. package/dist/core/exec-approvals.d.ts.map +1 -0
  198. package/dist/core/exec-approvals.js +187 -0
  199. package/dist/core/exec-approvals.js.map +1 -0
  200. package/dist/core/heartbeat.d.ts +71 -0
  201. package/dist/core/heartbeat.d.ts.map +1 -0
  202. package/dist/core/heartbeat.js +214 -0
  203. package/dist/core/heartbeat.js.map +1 -0
  204. package/dist/core/message-queue.d.ts +60 -0
  205. package/dist/core/message-queue.d.ts.map +1 -0
  206. package/dist/core/message-queue.js +182 -0
  207. package/dist/core/message-queue.js.map +1 -0
  208. package/dist/core/network-policy.d.ts +39 -0
  209. package/dist/core/network-policy.d.ts.map +1 -0
  210. package/dist/core/network-policy.js +121 -0
  211. package/dist/core/network-policy.js.map +1 -0
  212. package/dist/core/progress.d.ts +48 -0
  213. package/dist/core/progress.d.ts.map +1 -0
  214. package/dist/core/progress.js +81 -0
  215. package/dist/core/progress.js.map +1 -0
  216. package/dist/core/prompt.d.ts +105 -0
  217. package/dist/core/prompt.d.ts.map +1 -0
  218. package/dist/core/prompt.js +411 -0
  219. package/dist/core/prompt.js.map +1 -0
  220. package/dist/core/pruning.d.ts +40 -0
  221. package/dist/core/pruning.d.ts.map +1 -0
  222. package/dist/core/pruning.js +165 -0
  223. package/dist/core/pruning.js.map +1 -0
  224. package/dist/core/rate-limiter.d.ts +64 -0
  225. package/dist/core/rate-limiter.d.ts.map +1 -0
  226. package/dist/core/rate-limiter.js +142 -0
  227. package/dist/core/rate-limiter.js.map +1 -0
  228. package/dist/core/reactions.d.ts +31 -0
  229. package/dist/core/reactions.d.ts.map +1 -0
  230. package/dist/core/reactions.js +67 -0
  231. package/dist/core/reactions.js.map +1 -0
  232. package/dist/core/retry-queue.d.ts +56 -0
  233. package/dist/core/retry-queue.d.ts.map +1 -0
  234. package/dist/core/retry-queue.js +106 -0
  235. package/dist/core/retry-queue.js.map +1 -0
  236. package/dist/core/sanitizer.d.ts +38 -0
  237. package/dist/core/sanitizer.d.ts.map +1 -0
  238. package/dist/core/sanitizer.js +181 -0
  239. package/dist/core/sanitizer.js.map +1 -0
  240. package/dist/core/secret-scanner.d.ts +39 -0
  241. package/dist/core/secret-scanner.d.ts.map +1 -0
  242. package/dist/core/secret-scanner.js +96 -0
  243. package/dist/core/secret-scanner.js.map +1 -0
  244. package/dist/core/secrets.d.ts +38 -0
  245. package/dist/core/secrets.d.ts.map +1 -0
  246. package/dist/core/secrets.js +137 -0
  247. package/dist/core/secrets.js.map +1 -0
  248. package/dist/core/security.d.ts +58 -0
  249. package/dist/core/security.d.ts.map +1 -0
  250. package/dist/core/security.js +120 -0
  251. package/dist/core/security.js.map +1 -0
  252. package/dist/core/self-awareness.d.ts +19 -0
  253. package/dist/core/self-awareness.d.ts.map +1 -0
  254. package/dist/core/self-awareness.js +124 -0
  255. package/dist/core/self-awareness.js.map +1 -0
  256. package/dist/core/session-init.d.ts +34 -0
  257. package/dist/core/session-init.d.ts.map +1 -0
  258. package/dist/core/session-init.js +68 -0
  259. package/dist/core/session-init.js.map +1 -0
  260. package/dist/core/streaming.d.ts +82 -0
  261. package/dist/core/streaming.d.ts.map +1 -0
  262. package/dist/core/streaming.js +264 -0
  263. package/dist/core/streaming.js.map +1 -0
  264. package/dist/core/symphony/orchestrator.d.ts +61 -0
  265. package/dist/core/symphony/orchestrator.d.ts.map +1 -0
  266. package/dist/core/symphony/orchestrator.js +476 -0
  267. package/dist/core/symphony/orchestrator.js.map +1 -0
  268. package/dist/core/symphony/status.d.ts +11 -0
  269. package/dist/core/symphony/status.d.ts.map +1 -0
  270. package/dist/core/symphony/status.js +133 -0
  271. package/dist/core/symphony/status.js.map +1 -0
  272. package/dist/core/symphony/types.d.ts +84 -0
  273. package/dist/core/symphony/types.d.ts.map +1 -0
  274. package/dist/core/symphony/types.js +5 -0
  275. package/dist/core/symphony/types.js.map +1 -0
  276. package/dist/core/symphony/workflow.d.ts +18 -0
  277. package/dist/core/symphony/workflow.d.ts.map +1 -0
  278. package/dist/core/symphony/workflow.js +149 -0
  279. package/dist/core/symphony/workflow.js.map +1 -0
  280. package/dist/core/symphony/workspace.d.ts +24 -0
  281. package/dist/core/symphony/workspace.d.ts.map +1 -0
  282. package/dist/core/symphony/workspace.js +94 -0
  283. package/dist/core/symphony/workspace.js.map +1 -0
  284. package/dist/core/thinking.d.ts +27 -0
  285. package/dist/core/thinking.d.ts.map +1 -0
  286. package/dist/core/thinking.js +83 -0
  287. package/dist/core/thinking.js.map +1 -0
  288. package/dist/core/thread-bindings.d.ts +47 -0
  289. package/dist/core/thread-bindings.d.ts.map +1 -0
  290. package/dist/core/thread-bindings.js +94 -0
  291. package/dist/core/thread-bindings.js.map +1 -0
  292. package/dist/core/timezone.d.ts +28 -0
  293. package/dist/core/timezone.d.ts.map +1 -0
  294. package/dist/core/timezone.js +72 -0
  295. package/dist/core/timezone.js.map +1 -0
  296. package/dist/core/tool-loop-detector.d.ts +41 -0
  297. package/dist/core/tool-loop-detector.d.ts.map +1 -0
  298. package/dist/core/tool-loop-detector.js +83 -0
  299. package/dist/core/tool-loop-detector.js.map +1 -0
  300. package/dist/core/tool-validator.d.ts +44 -0
  301. package/dist/core/tool-validator.d.ts.map +1 -0
  302. package/dist/core/tool-validator.js +175 -0
  303. package/dist/core/tool-validator.js.map +1 -0
  304. package/dist/core/typing.d.ts +25 -0
  305. package/dist/core/typing.d.ts.map +1 -0
  306. package/dist/core/typing.js +48 -0
  307. package/dist/core/typing.js.map +1 -0
  308. package/dist/core/usage-tracker.d.ts +66 -0
  309. package/dist/core/usage-tracker.d.ts.map +1 -0
  310. package/dist/core/usage-tracker.js +163 -0
  311. package/dist/core/usage-tracker.js.map +1 -0
  312. package/dist/daemon/commands.d.ts +16 -0
  313. package/dist/daemon/commands.d.ts.map +1 -0
  314. package/dist/daemon/commands.js +445 -0
  315. package/dist/daemon/commands.js.map +1 -0
  316. package/dist/daemon/pid.d.ts +30 -0
  317. package/dist/daemon/pid.d.ts.map +1 -0
  318. package/dist/daemon/pid.js +62 -0
  319. package/dist/daemon/pid.js.map +1 -0
  320. package/dist/doctor/checks/browser.d.ts +9 -0
  321. package/dist/doctor/checks/browser.d.ts.map +1 -0
  322. package/dist/doctor/checks/browser.js +54 -0
  323. package/dist/doctor/checks/browser.js.map +1 -0
  324. package/dist/doctor/checks/channels.d.ts +9 -0
  325. package/dist/doctor/checks/channels.d.ts.map +1 -0
  326. package/dist/doctor/checks/channels.js +90 -0
  327. package/dist/doctor/checks/channels.js.map +1 -0
  328. package/dist/doctor/checks/config.d.ts +10 -0
  329. package/dist/doctor/checks/config.d.ts.map +1 -0
  330. package/dist/doctor/checks/config.js +89 -0
  331. package/dist/doctor/checks/config.js.map +1 -0
  332. package/dist/doctor/checks/memory.d.ts +10 -0
  333. package/dist/doctor/checks/memory.d.ts.map +1 -0
  334. package/dist/doctor/checks/memory.js +82 -0
  335. package/dist/doctor/checks/memory.js.map +1 -0
  336. package/dist/doctor/checks/permissions.d.ts +9 -0
  337. package/dist/doctor/checks/permissions.d.ts.map +1 -0
  338. package/dist/doctor/checks/permissions.js +53 -0
  339. package/dist/doctor/checks/permissions.js.map +1 -0
  340. package/dist/doctor/checks/providers.d.ts +10 -0
  341. package/dist/doctor/checks/providers.d.ts.map +1 -0
  342. package/dist/doctor/checks/providers.js +93 -0
  343. package/dist/doctor/checks/providers.js.map +1 -0
  344. package/dist/doctor/checks/sessions.d.ts +10 -0
  345. package/dist/doctor/checks/sessions.d.ts.map +1 -0
  346. package/dist/doctor/checks/sessions.js +86 -0
  347. package/dist/doctor/checks/sessions.js.map +1 -0
  348. package/dist/doctor/doctor.d.ts +35 -0
  349. package/dist/doctor/doctor.d.ts.map +1 -0
  350. package/dist/doctor/doctor.js +51 -0
  351. package/dist/doctor/doctor.js.map +1 -0
  352. package/dist/doctor/repairs.d.ts +14 -0
  353. package/dist/doctor/repairs.d.ts.map +1 -0
  354. package/dist/doctor/repairs.js +34 -0
  355. package/dist/doctor/repairs.js.map +1 -0
  356. package/dist/gateway/compaction.d.ts +63 -0
  357. package/dist/gateway/compaction.d.ts.map +1 -0
  358. package/dist/gateway/compaction.js +235 -0
  359. package/dist/gateway/compaction.js.map +1 -0
  360. package/dist/gateway/gateway.d.ts +94 -0
  361. package/dist/gateway/gateway.d.ts.map +1 -0
  362. package/dist/gateway/gateway.js +466 -0
  363. package/dist/gateway/gateway.js.map +1 -0
  364. package/dist/gateway/lock.d.ts +24 -0
  365. package/dist/gateway/lock.d.ts.map +1 -0
  366. package/dist/gateway/lock.js +88 -0
  367. package/dist/gateway/lock.js.map +1 -0
  368. package/dist/gateway/protocol.d.ts +117 -0
  369. package/dist/gateway/protocol.d.ts.map +1 -0
  370. package/dist/gateway/protocol.js +5 -0
  371. package/dist/gateway/protocol.js.map +1 -0
  372. package/dist/gateway/session.d.ts +123 -0
  373. package/dist/gateway/session.d.ts.map +1 -0
  374. package/dist/gateway/session.js +573 -0
  375. package/dist/gateway/session.js.map +1 -0
  376. package/dist/hooks/hooks.d.ts +18 -0
  377. package/dist/hooks/hooks.d.ts.map +1 -0
  378. package/dist/hooks/hooks.js +45 -0
  379. package/dist/hooks/hooks.js.map +1 -0
  380. package/dist/hooks/types.d.ts +112 -0
  381. package/dist/hooks/types.d.ts.map +1 -0
  382. package/dist/hooks/types.js +23 -0
  383. package/dist/hooks/types.js.map +1 -0
  384. package/dist/index.d.ts +27 -0
  385. package/dist/index.d.ts.map +1 -0
  386. package/dist/index.js +2900 -0
  387. package/dist/index.js.map +1 -0
  388. package/dist/media/storage.d.ts +25 -0
  389. package/dist/media/storage.d.ts.map +1 -0
  390. package/dist/media/storage.js +97 -0
  391. package/dist/media/storage.js.map +1 -0
  392. package/dist/memory/embeddings.d.ts +46 -0
  393. package/dist/memory/embeddings.d.ts.map +1 -0
  394. package/dist/memory/embeddings.js +118 -0
  395. package/dist/memory/embeddings.js.map +1 -0
  396. package/dist/memory/hybrid.d.ts +35 -0
  397. package/dist/memory/hybrid.d.ts.map +1 -0
  398. package/dist/memory/hybrid.js +156 -0
  399. package/dist/memory/hybrid.js.map +1 -0
  400. package/dist/memory/markdown.d.ts +48 -0
  401. package/dist/memory/markdown.d.ts.map +1 -0
  402. package/dist/memory/markdown.js +228 -0
  403. package/dist/memory/markdown.js.map +1 -0
  404. package/dist/memory/store.d.ts +88 -0
  405. package/dist/memory/store.d.ts.map +1 -0
  406. package/dist/memory/store.js +21 -0
  407. package/dist/memory/store.js.map +1 -0
  408. package/dist/memory/vector.d.ts +24 -0
  409. package/dist/memory/vector.d.ts.map +1 -0
  410. package/dist/memory/vector.js +63 -0
  411. package/dist/memory/vector.js.map +1 -0
  412. package/dist/mods/mod.d.ts +100 -0
  413. package/dist/mods/mod.d.ts.map +1 -0
  414. package/dist/mods/mod.js +242 -0
  415. package/dist/mods/mod.js.map +1 -0
  416. package/dist/onboard/channels.d.ts +12 -0
  417. package/dist/onboard/channels.d.ts.map +1 -0
  418. package/dist/onboard/channels.js +283 -0
  419. package/dist/onboard/channels.js.map +1 -0
  420. package/dist/onboard/models.d.ts +13 -0
  421. package/dist/onboard/models.d.ts.map +1 -0
  422. package/dist/onboard/models.js +491 -0
  423. package/dist/onboard/models.js.map +1 -0
  424. package/dist/onboard/onboard.d.ts +12 -0
  425. package/dist/onboard/onboard.d.ts.map +1 -0
  426. package/dist/onboard/onboard.js +1137 -0
  427. package/dist/onboard/onboard.js.map +1 -0
  428. package/dist/providers/anthropic.d.ts +83 -0
  429. package/dist/providers/anthropic.d.ts.map +1 -0
  430. package/dist/providers/anthropic.js +583 -0
  431. package/dist/providers/anthropic.js.map +1 -0
  432. package/dist/providers/failover.d.ts +46 -0
  433. package/dist/providers/failover.d.ts.map +1 -0
  434. package/dist/providers/failover.js +149 -0
  435. package/dist/providers/failover.js.map +1 -0
  436. package/dist/providers/litellm.d.ts +38 -0
  437. package/dist/providers/litellm.d.ts.map +1 -0
  438. package/dist/providers/litellm.js +349 -0
  439. package/dist/providers/litellm.js.map +1 -0
  440. package/dist/providers/openai.d.ts +28 -0
  441. package/dist/providers/openai.d.ts.map +1 -0
  442. package/dist/providers/openai.js +321 -0
  443. package/dist/providers/openai.js.map +1 -0
  444. package/dist/providers/prompt-cache.d.ts +50 -0
  445. package/dist/providers/prompt-cache.d.ts.map +1 -0
  446. package/dist/providers/prompt-cache.js +96 -0
  447. package/dist/providers/prompt-cache.js.map +1 -0
  448. package/dist/providers/provider.d.ts +173 -0
  449. package/dist/providers/provider.d.ts.map +1 -0
  450. package/dist/providers/provider.js +22 -0
  451. package/dist/providers/provider.js.map +1 -0
  452. package/dist/sandbox/config.d.ts +42 -0
  453. package/dist/sandbox/config.d.ts.map +1 -0
  454. package/dist/sandbox/config.js +20 -0
  455. package/dist/sandbox/config.js.map +1 -0
  456. package/dist/sandbox/container.d.ts +71 -0
  457. package/dist/sandbox/container.d.ts.map +1 -0
  458. package/dist/sandbox/container.js +193 -0
  459. package/dist/sandbox/container.js.map +1 -0
  460. package/dist/sandbox/sandbox.d.ts +82 -0
  461. package/dist/sandbox/sandbox.d.ts.map +1 -0
  462. package/dist/sandbox/sandbox.js +176 -0
  463. package/dist/sandbox/sandbox.js.map +1 -0
  464. package/dist/skills/channel-loader.d.ts +18 -0
  465. package/dist/skills/channel-loader.d.ts.map +1 -0
  466. package/dist/skills/channel-loader.js +35 -0
  467. package/dist/skills/channel-loader.js.map +1 -0
  468. package/dist/skills/extension-loader.d.ts +15 -0
  469. package/dist/skills/extension-loader.d.ts.map +1 -0
  470. package/dist/skills/extension-loader.js +63 -0
  471. package/dist/skills/extension-loader.js.map +1 -0
  472. package/dist/skills/extension-registry.d.ts +32 -0
  473. package/dist/skills/extension-registry.d.ts.map +1 -0
  474. package/dist/skills/extension-registry.js +57 -0
  475. package/dist/skills/extension-registry.js.map +1 -0
  476. package/dist/skills/extensions.d.ts +91 -0
  477. package/dist/skills/extensions.d.ts.map +1 -0
  478. package/dist/skills/extensions.js +14 -0
  479. package/dist/skills/extensions.js.map +1 -0
  480. package/dist/skills/loader.d.ts +64 -0
  481. package/dist/skills/loader.d.ts.map +1 -0
  482. package/dist/skills/loader.js +382 -0
  483. package/dist/skills/loader.js.map +1 -0
  484. package/dist/skills/marketplace.d.ts +56 -0
  485. package/dist/skills/marketplace.d.ts.map +1 -0
  486. package/dist/skills/marketplace.js +183 -0
  487. package/dist/skills/marketplace.js.map +1 -0
  488. package/dist/skills/types.d.ts +94 -0
  489. package/dist/skills/types.d.ts.map +1 -0
  490. package/dist/skills/types.js +9 -0
  491. package/dist/skills/types.js.map +1 -0
  492. package/dist/tools/acp-sessions.d.ts +89 -0
  493. package/dist/tools/acp-sessions.d.ts.map +1 -0
  494. package/dist/tools/acp-sessions.js +391 -0
  495. package/dist/tools/acp-sessions.js.map +1 -0
  496. package/dist/tools/acp.d.ts +18 -0
  497. package/dist/tools/acp.d.ts.map +1 -0
  498. package/dist/tools/acp.js +102 -0
  499. package/dist/tools/acp.js.map +1 -0
  500. package/dist/tools/agent-tools.d.ts +24 -0
  501. package/dist/tools/agent-tools.d.ts.map +1 -0
  502. package/dist/tools/agent-tools.js +611 -0
  503. package/dist/tools/agent-tools.js.map +1 -0
  504. package/dist/tools/browser.d.ts +26 -0
  505. package/dist/tools/browser.d.ts.map +1 -0
  506. package/dist/tools/browser.js +242 -0
  507. package/dist/tools/browser.js.map +1 -0
  508. package/dist/tools/comms.d.ts +8 -0
  509. package/dist/tools/comms.d.ts.map +1 -0
  510. package/dist/tools/comms.js +39 -0
  511. package/dist/tools/comms.js.map +1 -0
  512. package/dist/tools/cron-tools.d.ts +9 -0
  513. package/dist/tools/cron-tools.d.ts.map +1 -0
  514. package/dist/tools/cron-tools.js +117 -0
  515. package/dist/tools/cron-tools.js.map +1 -0
  516. package/dist/tools/exec-safety.d.ts +71 -0
  517. package/dist/tools/exec-safety.d.ts.map +1 -0
  518. package/dist/tools/exec-safety.js +141 -0
  519. package/dist/tools/exec-safety.js.map +1 -0
  520. package/dist/tools/exec.d.ts +24 -0
  521. package/dist/tools/exec.d.ts.map +1 -0
  522. package/dist/tools/exec.js +191 -0
  523. package/dist/tools/exec.js.map +1 -0
  524. package/dist/tools/fs.d.ts +15 -0
  525. package/dist/tools/fs.d.ts.map +1 -0
  526. package/dist/tools/fs.js +249 -0
  527. package/dist/tools/fs.js.map +1 -0
  528. package/dist/tools/git.d.ts +9 -0
  529. package/dist/tools/git.d.ts.map +1 -0
  530. package/dist/tools/git.js +56 -0
  531. package/dist/tools/git.js.map +1 -0
  532. package/dist/tools/image.d.ts +15 -0
  533. package/dist/tools/image.d.ts.map +1 -0
  534. package/dist/tools/image.js +106 -0
  535. package/dist/tools/image.js.map +1 -0
  536. package/dist/tools/introspect.d.ts +22 -0
  537. package/dist/tools/introspect.d.ts.map +1 -0
  538. package/dist/tools/introspect.js +223 -0
  539. package/dist/tools/introspect.js.map +1 -0
  540. package/dist/tools/memory.d.ts +11 -0
  541. package/dist/tools/memory.d.ts.map +1 -0
  542. package/dist/tools/memory.js +101 -0
  543. package/dist/tools/memory.js.map +1 -0
  544. package/dist/tools/message.d.ts +24 -0
  545. package/dist/tools/message.d.ts.map +1 -0
  546. package/dist/tools/message.js +205 -0
  547. package/dist/tools/message.js.map +1 -0
  548. package/dist/tools/model.d.ts +14 -0
  549. package/dist/tools/model.d.ts.map +1 -0
  550. package/dist/tools/model.js +62 -0
  551. package/dist/tools/model.js.map +1 -0
  552. package/dist/tools/policy.d.ts +101 -0
  553. package/dist/tools/policy.d.ts.map +1 -0
  554. package/dist/tools/policy.js +168 -0
  555. package/dist/tools/policy.js.map +1 -0
  556. package/dist/tools/registry.d.ts +52 -0
  557. package/dist/tools/registry.d.ts.map +1 -0
  558. package/dist/tools/registry.js +154 -0
  559. package/dist/tools/registry.js.map +1 -0
  560. package/dist/tools/search.d.ts +10 -0
  561. package/dist/tools/search.d.ts.map +1 -0
  562. package/dist/tools/search.js +78 -0
  563. package/dist/tools/search.js.map +1 -0
  564. package/dist/tools/session.d.ts +13 -0
  565. package/dist/tools/session.d.ts.map +1 -0
  566. package/dist/tools/session.js +142 -0
  567. package/dist/tools/session.js.map +1 -0
  568. package/dist/tools/spawn.d.ts +10 -0
  569. package/dist/tools/spawn.d.ts.map +1 -0
  570. package/dist/tools/spawn.js +72 -0
  571. package/dist/tools/spawn.js.map +1 -0
  572. package/dist/tools/symphony.d.ts +12 -0
  573. package/dist/tools/symphony.d.ts.map +1 -0
  574. package/dist/tools/symphony.js +142 -0
  575. package/dist/tools/symphony.js.map +1 -0
  576. package/dist/tools/system-tools.d.ts +11 -0
  577. package/dist/tools/system-tools.d.ts.map +1 -0
  578. package/dist/tools/system-tools.js +39 -0
  579. package/dist/tools/system-tools.js.map +1 -0
  580. package/dist/tools/tool.d.ts +119 -0
  581. package/dist/tools/tool.d.ts.map +1 -0
  582. package/dist/tools/tool.js +29 -0
  583. package/dist/tools/tool.js.map +1 -0
  584. package/dist/tools/web.d.ts +10 -0
  585. package/dist/tools/web.d.ts.map +1 -0
  586. package/dist/tools/web.js +105 -0
  587. package/dist/tools/web.js.map +1 -0
  588. package/dist/tui/App.d.ts +43 -0
  589. package/dist/tui/App.d.ts.map +1 -0
  590. package/dist/tui/App.js +265 -0
  591. package/dist/tui/App.js.map +1 -0
  592. package/dist/tui/bridge.d.ts +40 -0
  593. package/dist/tui/bridge.d.ts.map +1 -0
  594. package/dist/tui/bridge.js +29 -0
  595. package/dist/tui/bridge.js.map +1 -0
  596. package/dist/tui/components/Header.d.ts +14 -0
  597. package/dist/tui/components/Header.d.ts.map +1 -0
  598. package/dist/tui/components/Header.js +7 -0
  599. package/dist/tui/components/Header.js.map +1 -0
  600. package/dist/tui/components/InputBar.d.ts +10 -0
  601. package/dist/tui/components/InputBar.d.ts.map +1 -0
  602. package/dist/tui/components/InputBar.js +121 -0
  603. package/dist/tui/components/InputBar.js.map +1 -0
  604. package/dist/tui/components/MessageList.d.ts +18 -0
  605. package/dist/tui/components/MessageList.d.ts.map +1 -0
  606. package/dist/tui/components/MessageList.js +34 -0
  607. package/dist/tui/components/MessageList.js.map +1 -0
  608. package/dist/tui/components/Spinner.d.ts +9 -0
  609. package/dist/tui/components/Spinner.d.ts.map +1 -0
  610. package/dist/tui/components/Spinner.js +18 -0
  611. package/dist/tui/components/Spinner.js.map +1 -0
  612. package/dist/tui/components/StatusBar.d.ts +16 -0
  613. package/dist/tui/components/StatusBar.d.ts.map +1 -0
  614. package/dist/tui/components/StatusBar.js +15 -0
  615. package/dist/tui/components/StatusBar.js.map +1 -0
  616. package/dist/tui/components/ToolCallBox.d.ts +12 -0
  617. package/dist/tui/components/ToolCallBox.d.ts.map +1 -0
  618. package/dist/tui/components/ToolCallBox.js +12 -0
  619. package/dist/tui/components/ToolCallBox.js.map +1 -0
  620. package/dist/tui/theme.d.ts +58 -0
  621. package/dist/tui/theme.d.ts.map +1 -0
  622. package/dist/tui/theme.js +80 -0
  623. package/dist/tui/theme.js.map +1 -0
  624. package/dist/utils/logger.d.ts +16 -0
  625. package/dist/utils/logger.d.ts.map +1 -0
  626. package/dist/utils/logger.js +70 -0
  627. package/dist/utils/logger.js.map +1 -0
  628. package/docs/DEVELOPMENT.md +74 -0
  629. package/docs/INSTALL.md +161 -0
  630. package/docs/USAGE.md +94 -0
  631. package/docs/architecture.md +128 -0
  632. package/docs/channels.md +140 -0
  633. package/docs/configuration.md +209 -0
  634. package/docs/io-system.md +430 -0
  635. package/docs/providers.md +99 -0
  636. package/docs/skill-channels.md +113 -0
  637. package/docs/skills.md +246 -0
  638. package/package.json +89 -0
  639. package/skills/acp-router/SKILL.md +41 -0
  640. package/skills/acp-router/tools/acp-router.mjs +239 -0
  641. package/skills/find-skills/SKILL.md +133 -0
  642. package/skills/security-audit/SKILL.md +181 -0
  643. package/skills/security-audit/audit.sh +67 -0
  644. package/skills/skill-creator/SKILL.md +479 -0
  645. package/skills/skill-security-audit/.clawhub/origin.json +7 -0
  646. package/skills/skill-security-audit/SKILL.md +196 -0
  647. package/skills/skill-security-audit/_meta.json +6 -0
  648. package/skills/skill-security-audit/references/prompt-injection-patterns.md +276 -0
  649. package/skills/skill-security-audit/references/vulnerability-patterns.md +348 -0
  650. package/skills/symphony/README.md +53 -0
  651. package/skills/symphony/SKILL.md +75 -0
  652. package/skills/symphony/tools/symphony-orchestrator.ts +8 -0
  653. package/tako.example.json +33 -0
package/dist/index.js ADDED
@@ -0,0 +1,2900 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Tako 🐙 — Agent-as-CPU OS: minimal core + pluggable skill arms.
4
+ *
5
+ * CLI entry point with subcommands:
6
+ * tako Start interactive agent (alias for `tako start`)
7
+ * tako start Start the agent in foreground (default)
8
+ * tako start -d Start as background daemon
9
+ * tako stop Stop the daemon
10
+ * tako restart Restart the daemon
11
+ * tako status Show daemon & runtime status
12
+ * tako tui Attach TUI to running daemon
13
+ * tako dev Development mode (watch + auto-restart)
14
+ * tako onboard Interactive first-time setup wizard
15
+ * tako doctor Run health checks
16
+ * tako models Model management (list, set, auth, status)
17
+ * tako channels Channel management (list, add, remove, status)
18
+ * tako skills list List discovered skills
19
+ * tako skills install <name> Install a skill from the ecosystem
20
+ * tako skills info <name> Show skill details
21
+ * tako agents Agent management (list, add, remove, info)
22
+ * tako sessions Session management (list, history)
23
+ * tako version Print version
24
+ * tako help Show help
25
+ */
26
+ import { join } from 'node:path';
27
+ import { homedir } from 'node:os';
28
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
29
+ import { readFile, writeFile, rename } from 'node:fs/promises';
30
+ import { resolveConfig, hasConfig } from './config/resolve.js';
31
+ import { runOnboard } from './onboard/onboard.js';
32
+ import { runModels } from './onboard/models.js';
33
+ import { runChannels } from './onboard/channels.js';
34
+ import { runStartDaemon, runStop, runRestart, runStatus, runTui, runDev } from './daemon/commands.js';
35
+ import { writePidFile, removePidFile } from './daemon/pid.js';
36
+ import { CLIChannel } from './channels/cli.js';
37
+ import { TUIChannel } from './channels/tui.js';
38
+ import { DiscordChannel } from './channels/discord.js';
39
+ import { TelegramChannel } from './channels/telegram.js';
40
+ import { AnthropicProvider } from './providers/anthropic.js';
41
+ import { OpenAIProvider } from './providers/openai.js';
42
+ import { LiteLLMProvider } from './providers/litellm.js';
43
+ import { FailoverProvider } from './providers/failover.js';
44
+ import { RetryQueue } from './core/retry-queue.js';
45
+ import { MessageQueue } from './core/message-queue.js';
46
+ import { ToolRegistry } from './tools/registry.js';
47
+ import { ToolPolicy } from './tools/policy.js';
48
+ import { configureExecSafety } from './tools/exec.js';
49
+ import { fsTools } from './tools/fs.js';
50
+ import { searchTools } from './tools/search.js';
51
+ import { execTools } from './tools/exec.js';
52
+ import { webTools } from './tools/web.js';
53
+ import { createModelTool } from './tools/model.js';
54
+ import { imageTools } from './tools/image.js';
55
+ import { gitTools } from './tools/git.js';
56
+ import { AgentLoop } from './core/agent-loop.js';
57
+ import { PromptBuilder } from './core/prompt.js';
58
+ import { ContextManager } from './core/context.js';
59
+ import { SessionManager } from './gateway/session.js';
60
+ import { Gateway } from './gateway/gateway.js';
61
+ import { TakoHookSystem } from './hooks/hooks.js';
62
+ import { HybridMemoryStore } from './memory/hybrid.js';
63
+ import { createEmbeddingProvider } from './memory/embeddings.js';
64
+ import { createMemoryTools } from './tools/memory.js';
65
+ import { createSessionTools } from './tools/session.js';
66
+ import { createBrowserTools } from './tools/browser.js';
67
+ import { SkillLoader } from './skills/loader.js';
68
+ import { loadChannelFromSkill } from './skills/channel-loader.js';
69
+ import { bootstrapWorkspace, ensureDailyMemory } from './core/bootstrap.js';
70
+ import { Doctor } from './doctor/doctor.js';
71
+ import { checkConfig } from './doctor/checks/config.js';
72
+ import { checkProviders } from './doctor/checks/providers.js';
73
+ import { checkChannels } from './doctor/checks/channels.js';
74
+ import { checkMemory } from './doctor/checks/memory.js';
75
+ import { checkSessions } from './doctor/checks/sessions.js';
76
+ import { checkPermissions } from './doctor/checks/permissions.js';
77
+ import { checkBrowser } from './doctor/checks/browser.js';
78
+ import { SandboxManager } from './sandbox/sandbox.js';
79
+ import { DockerContainer } from './sandbox/container.js';
80
+ import { AgentRegistry } from './agents/registry.js';
81
+ import { resolveAgentForChannel } from './agents/config.js';
82
+ import { SubAgentOrchestrator } from './agents/subagent.js';
83
+ import { createAgentTools } from './tools/agent-tools.js';
84
+ import { createMessageTools } from './tools/message.js';
85
+ import { ThreadBindingManager } from './core/thread-bindings.js';
86
+ import { CommandRegistry } from './commands/registry.js';
87
+ import { buildSkillCommands } from './commands/skill-commands.js';
88
+ import { showModelPicker } from './commands/model-picker.js';
89
+ import { installFileLogger } from './utils/logger.js';
90
+ import { createIntrospectTools } from './tools/introspect.js';
91
+ import { handleSetupCommand, handleAgentSelect, handleChannelTypeButton, handleModalSubmit, } from './commands/channel-setup.js';
92
+ import { isUserAllowed, createAllowFromTools } from './auth/allow-from.js';
93
+ import { DeliveryQueue } from './channels/delivery-queue.js';
94
+ import { initMediaStorage, persistAttachments } from './media/storage.js';
95
+ import { runConfig } from './cli/config.js';
96
+ import { runCron } from './cli/cron.js';
97
+ import { runMemory } from './cli/memory.js';
98
+ import { runLogs } from './cli/logs.js';
99
+ import { runMessage } from './cli/message.js';
100
+ import { runUpdate } from './cli/update.js';
101
+ import { runService } from './cli/service.js';
102
+ import { runCache } from './cli/cache.js';
103
+ import { runAudit } from './cli/audit.js';
104
+ import { runAcp } from './cli/acp.js';
105
+ import { runExtensions } from './cli/extensions.js';
106
+ import { initSecurity } from './core/security.js';
107
+ import { CacheManager } from './cache/manager.js';
108
+ import { setFsCacheManager } from './tools/fs.js';
109
+ import { setExecCacheManager } from './tools/exec.js';
110
+ import { setImageProvider } from './tools/image.js';
111
+ import { runSymphony } from './cli/symphony.js';
112
+ import { ExtensionRegistry } from './skills/extension-registry.js';
113
+ import { loadExtension, getSkillsWithExtension } from './skills/extension-loader.js';
114
+ const VERSION = '0.0.1';
115
+ async function main() {
116
+ // Prevent crashes from killing the entire process
117
+ process.on('uncaughtException', (error) => {
118
+ console.error('[tako] Uncaught exception:', error.message);
119
+ console.error(error.stack);
120
+ // Don't exit — let the process continue serving other requests
121
+ });
122
+ process.on('unhandledRejection', (reason) => {
123
+ console.error('[tako] Unhandled rejection:', reason);
124
+ // Don't exit — log and continue
125
+ });
126
+ const args = process.argv.slice(2);
127
+ const command = args[0] ?? 'start';
128
+ switch (command) {
129
+ case 'start':
130
+ // If no config exists, prompt to run onboard first
131
+ if (!hasConfig()) {
132
+ console.log('No tako.json found. Run `tako onboard` to set up Tako first.\n');
133
+ await runOnboard();
134
+ return;
135
+ }
136
+ // Daemon mode: fork to background
137
+ if (args.includes('--daemon') || args.includes('-d')) {
138
+ await runStartDaemon();
139
+ }
140
+ else if (args.includes('--background')) {
141
+ // Internal: forked child runs the actual server
142
+ await runStart();
143
+ }
144
+ else {
145
+ await runStart();
146
+ }
147
+ break;
148
+ case 'stop':
149
+ await runStop();
150
+ break;
151
+ case 'restart':
152
+ await runRestart();
153
+ break;
154
+ case 'tui':
155
+ case 'chat':
156
+ await runTui();
157
+ break;
158
+ case 'dev':
159
+ await runDev();
160
+ break;
161
+ case 'onboard':
162
+ case 'setup':
163
+ case 'configure':
164
+ await runOnboard();
165
+ break;
166
+ case 'models':
167
+ await runModels(args.slice(1));
168
+ break;
169
+ case 'channels':
170
+ await runChannels(args.slice(1));
171
+ break;
172
+ case 'mod':
173
+ case 'mods':
174
+ await runMod(args.slice(1));
175
+ break;
176
+ case 'doctor':
177
+ await runDoctor(args.slice(1));
178
+ break;
179
+ case 'skills':
180
+ await runSkills(args.slice(1));
181
+ break;
182
+ case 'sandbox':
183
+ await runSandbox(args.slice(1));
184
+ break;
185
+ case 'agents':
186
+ await runAgents(args.slice(1));
187
+ break;
188
+ case 'sessions':
189
+ await runSessions(args.slice(1));
190
+ break;
191
+ case 'extensions':
192
+ await runExtensions(args.slice(1));
193
+ break;
194
+ case 'symphony':
195
+ await runSymphony(args.slice(1));
196
+ break;
197
+ case 'status':
198
+ await runStatus();
199
+ break;
200
+ case 'nuke':
201
+ await runNuke(args.slice(1));
202
+ break;
203
+ case 'config':
204
+ await runConfig(args.slice(1));
205
+ break;
206
+ case 'cache':
207
+ await runCache(args.slice(1));
208
+ break;
209
+ case 'audit':
210
+ await runAudit(args.slice(1));
211
+ break;
212
+ case 'acp':
213
+ await runAcp(args.slice(1));
214
+ break;
215
+ case 'cron':
216
+ await runCron(args.slice(1));
217
+ break;
218
+ case 'memory':
219
+ await runMemory(args.slice(1));
220
+ break;
221
+ case 'logs':
222
+ case 'log':
223
+ await runLogs(args.slice(1));
224
+ break;
225
+ case 'message':
226
+ case 'msg':
227
+ await runMessage(args.slice(1));
228
+ break;
229
+ case 'update':
230
+ await runUpdate(args.slice(1));
231
+ break;
232
+ case 'service':
233
+ await runService(args.slice(1));
234
+ break;
235
+ case 'version':
236
+ case '--version':
237
+ case '-v':
238
+ console.log(`tako v${VERSION}`);
239
+ break;
240
+ case 'help':
241
+ case '--help':
242
+ case '-h':
243
+ printHelp();
244
+ break;
245
+ default:
246
+ console.error(`Unknown command: ${command}`);
247
+ printHelp();
248
+ process.exit(1);
249
+ }
250
+ }
251
+ function printHelp() {
252
+ console.log(`Tako 🐙 — Agent-as-CPU OS
253
+
254
+ Usage: tako [command] [options]
255
+
256
+ Commands:
257
+ start Start Tako (foreground, default)
258
+ start -d Start Tako as background daemon
259
+ stop Stop the daemon
260
+ restart Restart the daemon
261
+ status Show daemon status & runtime info
262
+ tui Attach TUI to running daemon
263
+ dev Development mode (watch + auto-restart)
264
+ onboard Interactive first-time setup wizard
265
+
266
+ config file Print active config file path
267
+ config get <path> Get config value by dot path
268
+ config set <p> <v> Set config value by dot path
269
+ config unset <p> Remove config value
270
+ config validate Validate config against schema
271
+
272
+ models list List available models
273
+ models set <model> Set active model
274
+ models auth login Interactive provider auth
275
+ models status Show current model & provider
276
+ models refresh Re-fetch models from providers
277
+ models fallbacks Show/set fallback model chain
278
+ models aliases Manage model aliases
279
+
280
+ channels list List configured channels
281
+ channels add Add a channel (discord, telegram)
282
+ channels remove Remove a channel
283
+ channels status Show channel connection status
284
+
285
+ agents list List all configured agents
286
+ agents add <name> Create a new agent with workspace
287
+ agents remove <n> Remove an agent
288
+ agents info <name> Show agent details
289
+ agents bind Bind agent to a channel
290
+ agents unbind Unbind agent from a channel
291
+ agents bindings List all agent-channel bindings
292
+ agents set-identity Update agent display name/emoji
293
+
294
+ sessions list List all active sessions
295
+ sessions history Show session history
296
+ sessions inspect Show full session metadata
297
+ sessions compact Compact a session
298
+ sessions clear Archive a session
299
+
300
+ cron list List all cron jobs
301
+ cron add Add a scheduled job
302
+ cron remove <id> Remove a cron job
303
+ cron enable/disable Toggle a cron job
304
+ cron run <id> Run a job immediately
305
+ cron runs Show cron run history
306
+
307
+ memory search <q> Search memory files
308
+ memory status Show memory index status
309
+
310
+ logs View today's log (--lines N, --follow, --grep)
311
+
312
+ message send Send message to a channel
313
+ message broadcast Broadcast to all channels
314
+
315
+ skills list List discovered skills
316
+ skills install <n> Install a skill
317
+ skills info <name> Show skill details
318
+ skills check Check skill readiness
319
+ skills audit <n> Security audit a skill
320
+
321
+ symphony start Start project orchestrator
322
+ symphony stop Stop orchestrator
323
+ symphony status Show orchestrator dashboard
324
+ symphony history Show completed runs
325
+ symphony config Show current config
326
+
327
+ sandbox status Show sandbox status
328
+ sandbox explain <t> Explain tool permissions
329
+ sandbox cleanup Remove sandbox containers
330
+
331
+ update check Check for new version
332
+ update install Self-update via npm
333
+
334
+ service install Install systemd user service
335
+ service uninstall Remove systemd user service
336
+ service start Start systemd service
337
+ service stop Stop systemd service
338
+ service restart Restart systemd service
339
+ service status Show service status
340
+ service logs Show service logs
341
+
342
+ doctor Run health checks
343
+ version Print version
344
+ help Show this help
345
+
346
+ Daemon examples:
347
+ tako start -d Start as background daemon
348
+ tako tui Attach TUI to running daemon
349
+ tako stop Stop the daemon
350
+ tako restart Restart the daemon
351
+ tako dev Watch mode for development
352
+
353
+ Auth examples:
354
+ tako models auth login --provider anthropic API key or setup-token
355
+ tako models auth login --provider openai API key or OAuth
356
+ tako models auth login --provider openai-codex OAuth flow
357
+ tako models auth status Check all providers
358
+ tako models auth logout --provider anthropic Remove stored auth
359
+
360
+ Config resolution:
361
+ 1. --config <path> (explicit override)
362
+ 2. ~/.tako/tako.json (user home)
363
+
364
+ Examples:
365
+ tako onboard First-time setup
366
+ tako Start interactive agent
367
+ tako models set anthropic/claude-opus-4-6
368
+ tako channels add discord
369
+ tako doctor Check system health
370
+ `);
371
+ }
372
+ // ─── tako start ──────────────────────────────────────────────────────
373
+ async function runStart() {
374
+ // Install file logger early so all console output is captured
375
+ installFileLogger();
376
+ const config = await resolveConfig();
377
+ // Bootstrap workspace
378
+ await bootstrapWorkspace(config.memory.workspace);
379
+ await ensureDailyMemory(config.memory.workspace);
380
+ // Initialize security modules
381
+ initSecurity(config.security, config.memory.workspace);
382
+ // Initialize cache
383
+ const cacheManager = new CacheManager(config.cache);
384
+ cacheManager.startAutoClean();
385
+ setFsCacheManager(cacheManager);
386
+ setExecCacheManager(cacheManager);
387
+ // Initialize subsystems
388
+ const hooks = new TakoHookSystem();
389
+ const sessions = new SessionManager();
390
+ // Thread bindings (Discord thread → sub-agent session routing)
391
+ const { homedir } = await import('node:os');
392
+ const threadBindings = new ThreadBindingManager(join(homedir(), '.tako', 'thread-bindings.json'));
393
+ await threadBindings.load();
394
+ // Memory
395
+ const embeddingProvider = createEmbeddingProvider(config.memory.embeddings);
396
+ const memoryStore = new HybridMemoryStore(config.memory.workspace, embeddingProvider ?? undefined);
397
+ await memoryStore.initialize();
398
+ const promptBuilder = new PromptBuilder(config.memory.workspace);
399
+ promptBuilder.setSandboxInfo(config.sandbox.mode, config.sandbox.workspaceAccess);
400
+ const contextManager = new ContextManager();
401
+ // Provider
402
+ const [providerName] = config.providers.primary.split('/');
403
+ let provider;
404
+ let resolvedProviderLabel = config.providers.primary;
405
+ switch (providerName) {
406
+ case 'anthropic':
407
+ provider = new AnthropicProvider();
408
+ break;
409
+ case 'openai':
410
+ provider = new OpenAIProvider();
411
+ break;
412
+ case 'litellm':
413
+ if (config.providers.litellm?.baseUrl) {
414
+ provider = LiteLLMProvider.fromConfig(config.providers.litellm);
415
+ }
416
+ else {
417
+ console.error('[litellm] ✗ No LiteLLM endpoint configured!');
418
+ console.error('[litellm] Your primary model is litellm/* but no baseUrl is set.');
419
+ console.error('[litellm] Run `tako onboard` and configure LiteLLM, or switch provider.');
420
+ console.error('[litellm] Falling back to Anthropic provider.');
421
+ provider = new AnthropicProvider();
422
+ resolvedProviderLabel = `anthropic (fallback — litellm misconfigured)`;
423
+ }
424
+ break;
425
+ default:
426
+ provider = new AnthropicProvider();
427
+ resolvedProviderLabel = `anthropic (fallback — unknown provider '${providerName}')`;
428
+ }
429
+ // Wrap provider in FailoverProvider for automatic fallback
430
+ const fallbackChain = [config.providers.primary, ...(config.providers.fallback ?? [])];
431
+ const providerMap = new Map();
432
+ providerMap.set(providerName, provider);
433
+ // Create additional provider instances for fallback models
434
+ for (const ref of fallbackChain) {
435
+ const [pid] = ref.split('/');
436
+ if (!providerMap.has(pid)) {
437
+ switch (pid) {
438
+ case 'anthropic':
439
+ providerMap.set(pid, new AnthropicProvider());
440
+ break;
441
+ case 'openai':
442
+ providerMap.set(pid, new OpenAIProvider());
443
+ break;
444
+ case 'litellm':
445
+ if (config.providers.litellm?.baseUrl) {
446
+ providerMap.set(pid, LiteLLMProvider.fromConfig(config.providers.litellm));
447
+ }
448
+ break;
449
+ }
450
+ }
451
+ }
452
+ const failoverProvider = new FailoverProvider({
453
+ providers: providerMap,
454
+ chain: fallbackChain,
455
+ cooldownMs: (config.providers.cooldownSeconds ?? 60) * 1000,
456
+ });
457
+ // Wire image tool to use the active provider for vision API
458
+ setImageProvider(failoverProvider, config.providers.primary);
459
+ // Sandbox manager
460
+ const sandboxManager = new SandboxManager(config.sandbox);
461
+ const sandboxActive = config.sandbox.mode !== 'off';
462
+ if (sandboxActive) {
463
+ const dockerOk = await sandboxManager.checkDocker();
464
+ if (!dockerOk) {
465
+ console.warn('[tako] Warning: Sandbox enabled but Docker is not available. Falling back to host execution.');
466
+ }
467
+ }
468
+ // Tool policy
469
+ const toolPolicy = new ToolPolicy({
470
+ profile: config.tools.profile,
471
+ allow: config.tools.allow,
472
+ deny: config.tools.deny,
473
+ sandbox: config.tools.sandbox,
474
+ exec: config.tools.exec ? {
475
+ security: config.tools.exec.security,
476
+ allowlist: config.tools.exec.allowlist,
477
+ timeout: config.tools.exec.timeout,
478
+ maxOutputSize: config.tools.exec.maxOutputSize,
479
+ } : undefined,
480
+ });
481
+ // Exec safety
482
+ configureExecSafety({
483
+ workspaceRoot: config.memory.workspace,
484
+ workDir: process.cwd(),
485
+ maxTimeout: config.tools.exec?.timeout ?? 120_000,
486
+ defaultTimeout: 30_000,
487
+ maxOutputSize: config.tools.exec?.maxOutputSize ?? 1024 * 1024,
488
+ });
489
+ // Tool registry
490
+ const toolRegistry = new ToolRegistry();
491
+ toolRegistry.setProfile(config.tools.profile);
492
+ toolRegistry.setDenyList(config.tools.deny);
493
+ if (config.tools.allow)
494
+ toolRegistry.setAllowList(config.tools.allow);
495
+ toolRegistry.setToolPolicy(toolPolicy);
496
+ // Register kernel tools
497
+ toolRegistry.registerAll(fsTools);
498
+ toolRegistry.registerAll(searchTools);
499
+ toolRegistry.registerAll(execTools);
500
+ toolRegistry.registerAll(webTools);
501
+ toolRegistry.registerAll(createBrowserTools({
502
+ enabled: config.tools.browser?.enabled ?? true,
503
+ headless: config.tools.browser?.headless ?? true,
504
+ idleTimeoutMs: config.tools.browser?.idleTimeoutMs ?? 300_000,
505
+ }));
506
+ toolRegistry.registerAll(imageTools);
507
+ toolRegistry.registerAll(gitTools);
508
+ toolRegistry.registerAll(createMemoryTools(memoryStore));
509
+ toolRegistry.registerAll(createSessionTools(sessions));
510
+ toolRegistry.registerAll(createAllowFromTools());
511
+ // Symphony tools (project orchestration)
512
+ const { symphonyTools } = await import('./tools/symphony.js');
513
+ toolRegistry.registerAll(symphonyTools);
514
+ // System tools (restart, etc.)
515
+ const { registerSystemTools } = await import('./tools/system-tools.js');
516
+ registerSystemTools(toolRegistry, {
517
+ gatewayPort: config.gateway.port,
518
+ gatewayBind: config.gateway.bind,
519
+ });
520
+ // ─── Agent registry ────────────────────────────────────────────────
521
+ const agentRegistry = new AgentRegistry(config.agents, config.providers.primary);
522
+ await agentRegistry.loadDynamic();
523
+ await agentRegistry.initialize();
524
+ // Enable per-agent session persistence (each agent stores sessions in its own dir)
525
+ const agentSessionDirs = new Map();
526
+ for (const agent of agentRegistry.list()) {
527
+ agentSessionDirs.set(agent.id, agent.sessionDir);
528
+ }
529
+ await sessions.enablePersistence(agentSessionDirs);
530
+ // Load skills — dirs come from config (already resolved/expanded)
531
+ const skillLoader = new SkillLoader(config.skills.dirs);
532
+ const skillManifests = await skillLoader.discover();
533
+ for (const manifest of skillManifests) {
534
+ const loaded = await skillLoader.load(manifest);
535
+ skillLoader.registerTools(loaded, toolRegistry);
536
+ skillLoader.registerHooks(loaded, hooks);
537
+ promptBuilder.addSkillInstructions(loaded.instructions);
538
+ }
539
+ // Build initial skill command specs (used by agent-loop dispatch + channel slash registration)
540
+ const skillCommandSpecs = buildSkillCommands(skillLoader.getAll());
541
+ // Hot reload — re-register tools, hooks, AND slash commands on skill changes
542
+ skillLoader.startWatching(async (reloadedSkills) => {
543
+ for (const skill of reloadedSkills) {
544
+ skillLoader.registerTools(skill, toolRegistry);
545
+ skillLoader.registerHooks(skill, hooks);
546
+ }
547
+ console.log(`[tako] Skills reloaded: ${reloadedSkills.length} skills`);
548
+ // Re-build and re-register skill commands with Discord
549
+ try {
550
+ const rebuiltSpecs = buildSkillCommands(reloadedSkills);
551
+ skillCommandSpecs.splice(0, skillCommandSpecs.length, ...rebuiltSpecs);
552
+ for (const dc of discordChannels) {
553
+ await dc.registerSkillCommands(skillCommandSpecs, async (commandName, channelId, author, guildId) => {
554
+ const agentId = dc.agentId ?? resolveAgentForChannel(agentRegistry.list(), 'discord', channelId);
555
+ return handleSlashCommand(commandName, channelId, author, agentId, dc);
556
+ });
557
+ }
558
+ if (discordChannels.length > 0) {
559
+ console.log(`[tako] Re-registered ${skillCommandSpecs.length} skill commands with Discord (${discordChannels.length} bot(s))`);
560
+ }
561
+ }
562
+ catch (err) {
563
+ console.error('[tako] Failed to re-register skill commands:', err instanceof Error ? err.message : err);
564
+ }
565
+ });
566
+ // Retry queue for failed messages (all fallbacks exhausted)
567
+ const retryQueue = new RetryQueue(config.retryQueue);
568
+ // Message queue for batching rapid inbound messages
569
+ // The processor callback is wired after the agent loop is created (see below).
570
+ let messageQueueProcessor = null;
571
+ const messageQueue = new MessageQueue(config.queue, async (sessionId, messages) => {
572
+ if (messageQueueProcessor)
573
+ await messageQueueProcessor(sessionId, messages);
574
+ });
575
+ // Typing indicators + reaction feedback
576
+ const { TypingManager } = await import('./core/typing.js');
577
+ const { ReactionManager } = await import('./core/reactions.js');
578
+ const typingManager = new TypingManager(config.typing ?? { enabled: true, intervalMs: 5000 });
579
+ const reactionManager = new ReactionManager(config.reactions ?? { enabled: true, reactions: {
580
+ received: '👋', processing: '💭', completed: '👍', failed: '❌', retrying: '🔄', thinking: '🧐',
581
+ } });
582
+ // Agent loop with skill loader for dynamic injection
583
+ const agentLoop = new AgentLoop({ provider: failoverProvider, toolRegistry, promptBuilder, contextManager, hooks, skillLoader, skillCommandSpecs, model: config.providers.primary, workspaceRoot: config.memory.workspace, retryQueue, typingManager, reactionManager, streamingConfig: config.agent.streaming }, {
584
+ timeout: config.agent.timeout,
585
+ ...(config.agent.maxOutputChars != null && { maxOutputChars: config.agent.maxOutputChars }),
586
+ ...(config.agent.maxTurns != null && { maxTurns: config.agent.maxTurns }),
587
+ ...(config.agent.maxToolCalls != null && { maxToolCalls: config.agent.maxToolCalls }),
588
+ ...(config.agent.maxTokens != null && { maxTokens: config.agent.maxTokens }),
589
+ });
590
+ // Set retry runner — re-invokes agent loop for a session
591
+ retryQueue.setRunner(async (sessionId, userMessage) => {
592
+ const session = sessions.get(sessionId);
593
+ if (!session)
594
+ throw new Error(`Session ${sessionId} not found for retry`);
595
+ let result = '';
596
+ for await (const chunk of agentLoop.run(session, userMessage)) {
597
+ result += chunk;
598
+ }
599
+ return result;
600
+ });
601
+ // Per-agent loops: each agent gets its own PromptBuilder (workspace) but shares
602
+ // the same provider (auth), toolRegistry, contextManager, and hooks.
603
+ // Agents with per-agent skill dirs get their own SkillLoader; otherwise they
604
+ // share the global skillLoader and skillCommandSpecs.
605
+ const agentLoops = new Map();
606
+ // Track per-agent skill command specs for channel slash-command registration.
607
+ const agentSkillCommandSpecsMap = new Map();
608
+ for (const agent of agentRegistry.list()) {
609
+ if (agent.isMain)
610
+ continue;
611
+ const agentPromptBuilder = new PromptBuilder(agent.workspace);
612
+ agentPromptBuilder.setSandboxInfo(config.sandbox.mode, config.sandbox.workspaceAccess);
613
+ const agentModel = agent.model ?? config.providers.primary;
614
+ // Build per-agent skill loader when agent has extra skill dirs
615
+ let agentSkillLoader = skillLoader;
616
+ let agentSkillCommandSpecs = skillCommandSpecs;
617
+ if (agent.skills?.dirs && agent.skills.dirs.length > 0) {
618
+ const agentSkillDirs = [...config.skills.dirs, ...agent.skills.dirs];
619
+ agentSkillLoader = new SkillLoader(agentSkillDirs);
620
+ const agentSkillManifests = await agentSkillLoader.discover();
621
+ for (const manifest of agentSkillManifests) {
622
+ const loaded = await agentSkillLoader.load(manifest);
623
+ agentSkillLoader.registerTools(loaded, toolRegistry);
624
+ agentSkillLoader.registerHooks(loaded, hooks);
625
+ }
626
+ agentSkillCommandSpecs = buildSkillCommands(agentSkillLoader.getAll());
627
+ console.log(`[tako] Agent "${agent.id}" using ${agentSkillDirs.length} skill dir(s), ${agentSkillCommandSpecs.length} skill command(s)`);
628
+ }
629
+ // Store per-agent specs so channel setup below can use them
630
+ agentSkillCommandSpecsMap.set(agent.id, agentSkillCommandSpecs);
631
+ const loop = new AgentLoop({
632
+ provider: failoverProvider,
633
+ toolRegistry,
634
+ promptBuilder: agentPromptBuilder,
635
+ contextManager,
636
+ hooks,
637
+ skillLoader: agentSkillLoader,
638
+ skillCommandSpecs: agentSkillCommandSpecs,
639
+ model: agentModel,
640
+ workspaceRoot: agent.workspace,
641
+ agentId: agent.id,
642
+ agentRole: agent.role,
643
+ retryQueue,
644
+ typingManager,
645
+ reactionManager,
646
+ streamingConfig: config.agent.streaming,
647
+ }, {
648
+ timeout: config.agent.timeout,
649
+ ...(config.agent.maxOutputChars != null && { maxOutputChars: config.agent.maxOutputChars }),
650
+ ...(config.agent.maxTurns != null && { maxTurns: config.agent.maxTurns }),
651
+ ...(config.agent.maxToolCalls != null && { maxToolCalls: config.agent.maxToolCalls }),
652
+ ...(config.agent.maxTokens != null && { maxTokens: config.agent.maxTokens }),
653
+ });
654
+ agentLoops.set(agent.id, loop);
655
+ }
656
+ /** Get the correct AgentLoop for a given agentId. */
657
+ function getAgentLoop(agentId) {
658
+ if (agentId && agentLoops.has(agentId))
659
+ return agentLoops.get(agentId);
660
+ return agentLoop;
661
+ }
662
+ // Register set_model tool
663
+ toolRegistry.register(createModelTool({
664
+ setModel: (ref) => {
665
+ agentLoop.setModel(ref);
666
+ config.providers.primary = ref;
667
+ import('./config/resolve.js').then(m => m.patchConfig({ providers: { primary: ref } })).catch(() => { });
668
+ },
669
+ getModel: () => agentLoop.getModel(),
670
+ }));
671
+ // ─── Sub-agent orchestrator ────────────────────────────────────────
672
+ const subAgentOrchestrator = new SubAgentOrchestrator(sessions, agentLoop);
673
+ // Notify parent sessions when sub-agents complete — deliver through channels
674
+ subAgentOrchestrator.onCompletion(async (event) => {
675
+ const parentSession = sessions.get(event.parentSessionId);
676
+ if (!parentSession)
677
+ return;
678
+ const statusEmoji = event.status === 'completed' ? '👍' : event.status === 'timeout' ? '⏱' : '❌';
679
+ const label = event.runId.slice(0, 8);
680
+ const summary = event.status === 'completed'
681
+ ? (event.result ?? '').slice(0, 1000)
682
+ : (event.error ?? 'Unknown error');
683
+ const announcement = `${statusEmoji} Sub-agent \`${label}\` ${event.status}\n\n${summary}`;
684
+ // Add to session messages
685
+ sessions.addMessage(event.parentSessionId, {
686
+ role: 'system',
687
+ content: announcement,
688
+ });
689
+ // Deliver through the channel that created this session
690
+ const channelType = parentSession.metadata.channelType;
691
+ const channelTarget = parentSession.metadata.channelTarget;
692
+ if (channelType && channelTarget) {
693
+ const channel = channels.find((ch) => ch.id === channelType);
694
+ if (channel) {
695
+ try {
696
+ await channel.send({ content: announcement, target: channelTarget });
697
+ }
698
+ catch (err) {
699
+ console.error(`[subagent] Failed to deliver completion to ${channelType}: ${err instanceof Error ? err.message : err}`);
700
+ }
701
+ }
702
+ }
703
+ // Also output to CLI/TUI if that's the parent channel
704
+ if (!channelType || channelType === 'cli' || channelType === 'tui') {
705
+ console.log(`\n${announcement}\n`);
706
+ }
707
+ });
708
+ // Register agent tools (agents_list, sessions_spawn, sessions_history, subagents)
709
+ toolRegistry.registerAll(createAgentTools({
710
+ registry: agentRegistry,
711
+ orchestrator: subAgentOrchestrator,
712
+ sessions,
713
+ threadBindings,
714
+ }));
715
+ // ─── Command registry ────────────────────────────────────────────
716
+ const startTime = Date.now();
717
+ const defaultModel = config.providers.primary;
718
+ const commandRegistry = new CommandRegistry({
719
+ getModel: () => agentLoop.getModel(),
720
+ setModel: (ref) => {
721
+ agentLoop.setModel(ref);
722
+ config.providers.primary = ref;
723
+ // Persist to tako.json so it survives restart
724
+ import('./config/resolve.js').then(m => m.patchConfig({ providers: { primary: ref } })).catch(() => { });
725
+ },
726
+ getDefaultModel: () => defaultModel,
727
+ getFallbackModels: () => config.providers.fallback ?? [],
728
+ listAgents: () => agentRegistry.list().map((a) => ({
729
+ id: a.id,
730
+ description: a.description,
731
+ role: a.role,
732
+ })),
733
+ compactSession: (sessionId, keepLast) => sessions.compact(sessionId, keepLast),
734
+ resetSession: (sessionId) => sessions.resetSession(sessionId),
735
+ estimateTokens: (session) => contextManager.estimateTokens(session.messages),
736
+ startTime,
737
+ getWorkspaceRoot: () => config.memory.workspace,
738
+ getSessionCount: () => sessions.list().length,
739
+ getChannelNames: () => channels.map(ch => ch.id),
740
+ getSkillCount: () => skillManifests.length,
741
+ getToolCount: () => toolRegistry.getActiveTools().length,
742
+ getQueueMode: () => messageQueue.getConfig().mode,
743
+ setQueueMode: (mode) => messageQueue.setMode(mode),
744
+ getQueueStatus: () => messageQueue.status(),
745
+ });
746
+ // ─── Multi-channel routing ────────────────────────────────────────
747
+ function getSession(msg, channel) {
748
+ // If the channel has a bound agentId, use it directly; otherwise resolve from bindings
749
+ const channelType = msg.channelId.split(':')[0] ?? 'cli';
750
+ const channelTarget = msg.channelId.includes(':')
751
+ ? msg.channelId.split(':').slice(1).join(':')
752
+ : msg.channelId;
753
+ // Check thread bindings first — if this message is in a bound thread,
754
+ // route to the sub-agent session instead of normal routing.
755
+ const binding = threadBindings.getBinding(channelTarget);
756
+ if (binding) {
757
+ threadBindings.touch(channelTarget);
758
+ const session = sessions.getOrCreate(binding.sessionKey, {
759
+ name: `${binding.agentId}/thread:${channelTarget}`,
760
+ metadata: {
761
+ agentId: binding.agentId,
762
+ channelType,
763
+ channelTarget,
764
+ authorId: msg.author.id,
765
+ threadBinding: true,
766
+ },
767
+ });
768
+ // Attach runtime-only ref (not persisted)
769
+ session.metadata.channelRef = channel;
770
+ return session;
771
+ }
772
+ const guildId = msg.author.meta?.guildId;
773
+ const agentId = channel?.agentId ?? resolveAgentForChannel(agentRegistry.list(), channelType, channelTarget, guildId);
774
+ // Build structured session key matching reference runtime's format:
775
+ // agent:<agentId>:<platform>:<type>:<target>
776
+ let key;
777
+ const chatType = msg.author.meta?.chatType;
778
+ if (channelType === 'discord') {
779
+ const guildId = msg.author.meta?.guildId;
780
+ if (guildId) {
781
+ key = `agent:${agentId}:discord:channel:${channelTarget}`;
782
+ }
783
+ else {
784
+ // No guild = DM
785
+ key = `agent:${agentId}:discord:dm:${msg.author.id}`;
786
+ }
787
+ }
788
+ else if (channelType === 'telegram') {
789
+ if (chatType === 'private') {
790
+ key = `agent:${agentId}:telegram:dm:${channelTarget}`;
791
+ }
792
+ else {
793
+ key = `agent:${agentId}:telegram:group:${channelTarget}`;
794
+ }
795
+ // Telegram topic: separate session per forum topic
796
+ if (msg.threadId) {
797
+ key += `:topic:${msg.threadId}`;
798
+ }
799
+ }
800
+ else if (channelType === 'cli') {
801
+ key = `agent:${agentId}:cli:main`;
802
+ }
803
+ else {
804
+ key = `agent:${agentId}:${msg.channelId}`;
805
+ }
806
+ const session = sessions.getOrCreate(key, {
807
+ name: `${agentId}/${msg.channelId}/${msg.author.name}`,
808
+ metadata: {
809
+ agentId, channelType, channelTarget, authorId: msg.author.id,
810
+ ...(msg.threadId ? { threadId: msg.threadId } : {}),
811
+ },
812
+ });
813
+ // Attach runtime-only ref (not persisted — stripped in appendToJSONL)
814
+ session.metadata.channelRef = channel;
815
+ return session;
816
+ }
817
+ function wireChannel(channel) {
818
+ deliveryQueue.registerChannel(channel);
819
+ channel.onMessage(async (msg) => {
820
+ try {
821
+ await hooks.emit('message_received', {
822
+ event: 'message_received',
823
+ data: { channelId: msg.channelId, authorId: msg.author.id, content: msg.content },
824
+ timestamp: Date.now(),
825
+ });
826
+ if (channel.id === 'cli' && (msg.content === '/quit' || msg.content === '/exit')) {
827
+ await shutdown();
828
+ process.exit(0);
829
+ }
830
+ // ─── AllowFrom ACL check ─────────────────────────────────────
831
+ const aclAgentId = channel.agentId ?? 'main';
832
+ const aclChannel = channel.id;
833
+ if (aclChannel !== 'cli' && aclChannel !== 'tui') {
834
+ const allowed = await isUserAllowed(aclChannel, aclAgentId, msg.author.id);
835
+ if (!allowed)
836
+ return; // silently ignore
837
+ }
838
+ const session = getSession(msg, channel);
839
+ // Update per-message runtime metadata used by typing/reactions/rate-limits
840
+ session.metadata.channelId = msg.channelId;
841
+ session.metadata.channelRef = channel;
842
+ session.metadata.channelTarget = msg.channelId.includes(':')
843
+ ? msg.channelId.split(':').slice(1).join(':')
844
+ : msg.channelId;
845
+ session.metadata.authorId = msg.author.id;
846
+ session.metadata.authorName = msg.author.name;
847
+ session.metadata.messageId = msg.id;
848
+ // Extract platform-specific target for typing/reactions
849
+ const target = session.metadata.channelTarget;
850
+ // ─── First-time channel intro (only on genuine first contact, not restarts) ──
851
+ // Track introduced channels persistently so we never re-intro after restart.
852
+ if (session.isNew && channel.id !== 'cli' && channel.id !== 'tui') {
853
+ const introKey = `${channel.agentId ?? 'main'}:${msg.channelId}`;
854
+ if (!introducedChannels.has(introKey)) {
855
+ introducedChannels.add(introKey);
856
+ saveIntroducedChannels();
857
+ const agentName = channel.agentId ?? 'Tako';
858
+ const intro = `👋 **${agentName}** is now active in this channel! Type \`/help\` for commands, or just @mention me to chat.`;
859
+ try {
860
+ await channel.send({ target, content: intro });
861
+ }
862
+ catch { /* may fail if no send permission */ }
863
+ }
864
+ }
865
+ // ─── Slash command handling (local, no LLM) ──────────────────
866
+ if (msg.content.trim().startsWith('/')) {
867
+ const channelType = msg.channelId.split(':')[0] ?? 'cli';
868
+ const channelTarget = msg.channelId.includes(':')
869
+ ? msg.channelId.split(':').slice(1).join(':')
870
+ : msg.channelId;
871
+ const agentId = channel.agentId ?? resolveAgentForChannel(agentRegistry.list(), channelType, channelTarget);
872
+ // reference runtime-like command reactions: received -> processing -> done/failed
873
+ if (channel.addReaction)
874
+ channel.addReaction(target, msg.id, '👋').catch(() => { });
875
+ if (channel.addReaction)
876
+ channel.addReaction(target, msg.id, '🧐').catch(() => { });
877
+ if (channel.removeReaction)
878
+ channel.removeReaction(target, msg.id, '👋').catch(() => { });
879
+ try {
880
+ const cmdResult = await commandRegistry.handle(msg.content, {
881
+ channelId: msg.channelId,
882
+ authorId: msg.author.id,
883
+ authorName: msg.author.name,
884
+ session,
885
+ agentId,
886
+ });
887
+ if (cmdResult) {
888
+ if (channel.id === 'cli') {
889
+ process.stdout.write(cmdResult + '\n');
890
+ }
891
+ else {
892
+ await channel.send({ target, content: cmdResult, replyTo: msg.id });
893
+ }
894
+ if (channel.removeReaction)
895
+ channel.removeReaction(target, msg.id, '🧐').catch(() => { });
896
+ if (channel.addReaction)
897
+ channel.addReaction(target, msg.id, '👍').catch(() => { });
898
+ return;
899
+ }
900
+ // Unknown command (not handled by registry)
901
+ if (channel.removeReaction)
902
+ channel.removeReaction(target, msg.id, '🧐').catch(() => { });
903
+ if (channel.addReaction)
904
+ channel.addReaction(target, msg.id, '🤷').catch(() => { });
905
+ }
906
+ catch (err) {
907
+ if (channel.removeReaction)
908
+ channel.removeReaction(target, msg.id, '🧐').catch(() => { });
909
+ if (channel.addReaction)
910
+ channel.addReaction(target, msg.id, '😅').catch(() => { });
911
+ throw err;
912
+ }
913
+ }
914
+ // ─── Message queue: collect/debounce rapid messages ─────────
915
+ if (messageQueue.getConfig().mode !== 'off' && channel.id !== 'cli' && channel.id !== 'tui') {
916
+ const queued = messageQueue.enqueue(session.id, {
917
+ content: msg.content,
918
+ channelId: msg.channelId,
919
+ authorId: msg.author.id,
920
+ timestamp: Date.now(),
921
+ messageId: msg.id,
922
+ });
923
+ if (queued) {
924
+ // Immediate feedback while waiting for queue flush
925
+ if (channel.sendTyping)
926
+ channel.sendTyping(target).catch(() => { });
927
+ if (channel.addReaction)
928
+ channel.addReaction(target, msg.id, '💭').catch(() => { });
929
+ return; // message was queued, will be batch-processed later
930
+ }
931
+ }
932
+ // ─── Typing indicator setup ──────────────────────────────────
933
+ const typingMode = config.agent.typingMode ?? 'instant';
934
+ const typingIntervalMs = (config.agent.typingIntervalSeconds ?? 6) * 1000;
935
+ let typingInterval = null;
936
+ if (typingMode === 'instant' && channel.sendTyping) {
937
+ channel.sendTyping(target).catch(() => { });
938
+ typingInterval = setInterval(() => {
939
+ channel.sendTyping(target).catch(() => { });
940
+ }, typingIntervalMs);
941
+ }
942
+ // ─── Reaction feedback: react with 🤔 while processing ──────
943
+ if (channel.addReaction) {
944
+ channel.addReaction(target, msg.id, '🧐').catch(() => { });
945
+ }
946
+ let response = '';
947
+ let hadError = false;
948
+ // Prepend sender context so the agent knows who it's talking to
949
+ const senderPrefix = channel.id !== 'cli' && msg.author?.name
950
+ ? `[From: ${msg.author.name}]\n`
951
+ : '';
952
+ const userMessage = senderPrefix + msg.content;
953
+ // Use the correct agent loop — per-agent loops have their own PromptBuilder
954
+ // (workspace/identity) but share the same provider (auth/API keys).
955
+ const activeLoop = getAgentLoop(channel.agentId ?? session.metadata?.agentId);
956
+ try {
957
+ // Set active channel for typing/reactions
958
+ activeLoop.setChannel(channel);
959
+ // Persist inbound media attachments locally
960
+ const attachments = msg.attachments?.length
961
+ ? await persistAttachments(msg.attachments)
962
+ : msg.attachments;
963
+ for await (const chunk of activeLoop.run(session, userMessage, attachments)) {
964
+ if (channel.id === 'cli') {
965
+ process.stdout.write(chunk);
966
+ }
967
+ response += chunk;
968
+ }
969
+ }
970
+ catch (err) {
971
+ hadError = true;
972
+ const errMsg = err instanceof Error ? err.message : String(err);
973
+ console.error(`[tako] Error: ${errMsg}`);
974
+ // Auto-fallback: if model not found (404), try fallback chain or reset to default
975
+ const is404 = errMsg.includes('404') || errMsg.includes('not_found');
976
+ if (is404 && !response) {
977
+ const currentModel = activeLoop.getModel();
978
+ const fallbacks = config.providers.fallback ?? [];
979
+ const nextFallback = fallbacks.find(f => f !== currentModel);
980
+ if (nextFallback) {
981
+ activeLoop.setModel(nextFallback);
982
+ response = `⚠️ Model \`${currentModel}\` not found. Auto-switched to fallback: \`${nextFallback}\`\n\nPlease resend your message, or use \`/model default\` to reset.`;
983
+ }
984
+ else {
985
+ activeLoop.setModel(defaultModel);
986
+ response = `⚠️ Model \`${currentModel}\` not found. Reset to default: \`${defaultModel}\`\n\nPlease resend your message.`;
987
+ }
988
+ }
989
+ else if (!response) {
990
+ response = `⚠️ Error: ${errMsg.slice(0, 500)}`;
991
+ }
992
+ }
993
+ // ─── Send remaining text, then clean up ──────
994
+ if (typingInterval) {
995
+ clearInterval(typingInterval);
996
+ typingInterval = null;
997
+ }
998
+ if (channel.id === 'cli') {
999
+ if (response && !response.endsWith('\n')) {
1000
+ process.stdout.write('\n');
1001
+ }
1002
+ }
1003
+ else if (channel.id === 'tui') {
1004
+ if (response) {
1005
+ await channel.send({ target: msg.channelId, content: response, replyTo: msg.id });
1006
+ }
1007
+ }
1008
+ else if (response.trim()) {
1009
+ hooks.emit('message_sending', {
1010
+ event: 'message_sending',
1011
+ data: { channelId: msg.channelId, content: response },
1012
+ timestamp: Date.now(),
1013
+ }).catch(() => { });
1014
+ const outMsg = { target, content: response.trim(), replyTo: msg.id };
1015
+ try {
1016
+ await channel.send(outMsg);
1017
+ }
1018
+ catch (sendErr) {
1019
+ await deliveryQueue.enqueue(channel.id, outMsg, sendErr instanceof Error ? sendErr.message : String(sendErr));
1020
+ }
1021
+ }
1022
+ else {
1023
+ // Empty response — fallback
1024
+ console.error(`[${channel.id}] Empty response for: "${msg.content.slice(0, 50)}" (session ${session.id}, msgs: ${session.messages.length})`);
1025
+ const fallbackMsg = { target, content: '🤔 I processed your message but had nothing to say. Try rephrasing?', replyTo: msg.id };
1026
+ try {
1027
+ await channel.send(fallbackMsg);
1028
+ }
1029
+ catch (sendErr) {
1030
+ await deliveryQueue.enqueue(channel.id, fallbackMsg, sendErr instanceof Error ? sendErr.message : String(sendErr));
1031
+ }
1032
+ }
1033
+ // ─── Persist session to disk (agent loop pushes directly to session.messages) ──
1034
+ sessions.markSessionDirty(session.id);
1035
+ // ─── Reaction cleanup AFTER messages are sent ──────
1036
+ if (channel.removeReaction) {
1037
+ channel.removeReaction(target, msg.id, '🧐').catch(() => { });
1038
+ }
1039
+ if (channel.addReaction) {
1040
+ channel.addReaction(target, msg.id, hadError ? '😅' : '👍').catch(() => { });
1041
+ }
1042
+ }
1043
+ catch (outerErr) {
1044
+ // Per-message error isolation: log and continue, don't kill the process
1045
+ const errMsg = outerErr instanceof Error ? outerErr.message : String(outerErr);
1046
+ console.error(`[tako] Error processing message in ${channel.id}: ${errMsg}`);
1047
+ if (outerErr instanceof Error && outerErr.stack) {
1048
+ console.error(outerErr.stack);
1049
+ }
1050
+ // Try to send error reaction if possible
1051
+ if (channel.addReaction) {
1052
+ channel.addReaction(msg.channelId.includes(':') ? msg.channelId.split(':').slice(1).join(':') : msg.channelId, msg.id, '😅').catch(() => { });
1053
+ }
1054
+ }
1055
+ });
1056
+ }
1057
+ // ─── Message queue processor ────────────────────────────────────
1058
+ // Wire the processor callback now that wireChannel, sessions, and agentLoop exist.
1059
+ messageQueueProcessor = async (sessionId, messages) => {
1060
+ const session = sessions.get(sessionId);
1061
+ if (!session) {
1062
+ console.warn(`[message-queue] Session ${sessionId} not found, dropping ${messages.length} messages`);
1063
+ return;
1064
+ }
1065
+ const merged = MessageQueue.mergeMessages(messages);
1066
+ const channelRef = session.metadata?.channelRef;
1067
+ if (!channelRef) {
1068
+ console.warn(`[message-queue] No channel ref for session ${sessionId}, processing without channel`);
1069
+ }
1070
+ const activeLoop = getAgentLoop(session.metadata?.agentId);
1071
+ // Ensure loop has channel reference + latest message metadata for typing/reactions
1072
+ if (channelRef) {
1073
+ activeLoop.setChannel(channelRef);
1074
+ session.metadata.channelRef = channelRef;
1075
+ }
1076
+ const lastMsgId = messages[messages.length - 1]?.messageId;
1077
+ if (lastMsgId) {
1078
+ session.metadata.messageId = lastMsgId;
1079
+ }
1080
+ // Determine target for sending response
1081
+ const channelTarget = session.metadata?.channelTarget ?? '';
1082
+ const target = channelTarget;
1083
+ // Prepend sender context for merged messages
1084
+ const userMessage = merged;
1085
+ let response = '';
1086
+ let hadError = false;
1087
+ try {
1088
+ for await (const chunk of activeLoop.run(session, userMessage)) {
1089
+ response += chunk;
1090
+ }
1091
+ }
1092
+ catch (err) {
1093
+ hadError = true;
1094
+ const errMsg = err instanceof Error ? err.message : String(err);
1095
+ console.error(`[message-queue] Error processing batch for session ${sessionId}: ${errMsg}`);
1096
+ if (!response)
1097
+ response = `⚠️ Error: ${errMsg.slice(0, 500)}`;
1098
+ }
1099
+ // Send response through the channel
1100
+ if (channelRef && target && response.trim()) {
1101
+ const lastMsgId = messages[messages.length - 1].messageId;
1102
+ try {
1103
+ await channelRef.send({ target, content: response.trim(), replyTo: lastMsgId });
1104
+ }
1105
+ catch (sendErr) {
1106
+ console.error(`[message-queue] Send error:`, sendErr instanceof Error ? sendErr.message : sendErr);
1107
+ }
1108
+ // Queue reaction lifecycle: ⏳ -> ✅/⚠️
1109
+ if (lastMsgId) {
1110
+ if (channelRef.removeReaction)
1111
+ channelRef.removeReaction(target, lastMsgId, '💭').catch(() => { });
1112
+ if (channelRef.addReaction)
1113
+ channelRef.addReaction(target, lastMsgId, hadError ? '😅' : '👍').catch(() => { });
1114
+ }
1115
+ }
1116
+ sessions.markSessionDirty(sessionId);
1117
+ };
1118
+ // ─── Media storage ────────────────────────────────────────────────
1119
+ await initMediaStorage();
1120
+ // ─── Delivery queue ───────────────────────────────────────────────
1121
+ const deliveryQueue = new DeliveryQueue();
1122
+ await deliveryQueue.start();
1123
+ // ─── Initialize channels ──────────────────────────────────────────
1124
+ const channels = [];
1125
+ let discordChannel;
1126
+ const discordChannels = [];
1127
+ let telegramChannel;
1128
+ // Track which channels have received an intro (persistent across restarts)
1129
+ const introFilePath = join(homedir(), '.tako', 'introduced-channels.json');
1130
+ const introducedChannels = new Set();
1131
+ try {
1132
+ const raw = readFileSync(introFilePath, 'utf-8');
1133
+ const arr = JSON.parse(raw);
1134
+ if (Array.isArray(arr))
1135
+ arr.forEach((k) => introducedChannels.add(k));
1136
+ }
1137
+ catch { /* no file yet */ }
1138
+ function saveIntroducedChannels() {
1139
+ try {
1140
+ writeFileSync(introFilePath, JSON.stringify([...introducedChannels]), 'utf-8');
1141
+ }
1142
+ catch { /* non-critical */ }
1143
+ }
1144
+ const useTui = process.argv.includes('--tui') && process.stdout.isTTY;
1145
+ // Build available models list from config (primary + fallbacks + litellm + provider models)
1146
+ const availableModels = [config.providers.primary];
1147
+ if (config.providers.fallback) {
1148
+ for (const fb of config.providers.fallback) {
1149
+ if (!availableModels.includes(fb))
1150
+ availableModels.push(fb);
1151
+ }
1152
+ }
1153
+ if (config.providers.litellm?.models) {
1154
+ for (const m of config.providers.litellm.models) {
1155
+ const ref = `litellm/${m}`;
1156
+ if (!availableModels.includes(ref))
1157
+ availableModels.push(ref);
1158
+ }
1159
+ }
1160
+ // Add known provider models from registered providers
1161
+ for (const prov of [provider]) {
1162
+ for (const m of prov.models()) {
1163
+ const ref = `${m.provider}/${m.id}`;
1164
+ if (!availableModels.includes(ref))
1165
+ availableModels.push(ref);
1166
+ }
1167
+ }
1168
+ if (useTui) {
1169
+ const tui = new TUIChannel({
1170
+ version: VERSION,
1171
+ model: config.providers.primary,
1172
+ toolCount: toolRegistry.getAllTools().length,
1173
+ skillCount: skillManifests.length,
1174
+ toolProfile: config.tools.profile,
1175
+ memoryStatus: embeddingProvider ? 'hybrid' : 'BM25-only',
1176
+ availableModels,
1177
+ agents: agentRegistry.list().map((a) => ({
1178
+ id: a.id,
1179
+ description: a.description,
1180
+ role: a.role,
1181
+ isMain: a.isMain,
1182
+ })),
1183
+ onModelSwitch: (modelRef) => {
1184
+ // Update the agent loop's model at runtime
1185
+ agentLoop.setModel(modelRef);
1186
+ config.providers.primary = modelRef;
1187
+ },
1188
+ onAgentSwitch: (agentId) => {
1189
+ const agent = agentRegistry.get(agentId);
1190
+ if (agent) {
1191
+ // Switch workspace — prompt builder reads SOUL.md, AGENTS.md, etc. from here
1192
+ promptBuilder.setWorkspace(agent.workspace);
1193
+ // Switch model if agent has a different one
1194
+ if (agent.model && agent.model !== agentLoop.getModel()) {
1195
+ agentLoop.setModel(agent.model);
1196
+ }
1197
+ // Update working dir for tools
1198
+ if (agent.workspace) {
1199
+ promptBuilder.setWorkingDir(agent.workspace);
1200
+ }
1201
+ console.log(`[tako] Switched to agent: ${agentId} (role=${agent.role}, workspace=${agent.workspace})`);
1202
+ }
1203
+ },
1204
+ });
1205
+ channels.push(tui);
1206
+ wireChannel(tui);
1207
+ // Hook tool calls to show in TUI with proper colors
1208
+ hooks.on('before_tool_call', (event) => {
1209
+ const tuiBridge = globalThis.__takoTui;
1210
+ if (tuiBridge) {
1211
+ tuiBridge.addMessage({
1212
+ id: crypto.randomUUID(),
1213
+ role: 'tool',
1214
+ content: `Running...`,
1215
+ toolName: event.data.toolName,
1216
+ timestamp: new Date().toISOString(),
1217
+ });
1218
+ }
1219
+ });
1220
+ hooks.on('after_tool_call', (event) => {
1221
+ const tuiBridge = globalThis.__takoTui;
1222
+ if (tuiBridge) {
1223
+ const result = event.data.result;
1224
+ const output = result.output?.slice(0, 200) ?? '';
1225
+ const icon = result.success ? '[✓]' : '[✗]';
1226
+ tuiBridge.addMessage({
1227
+ id: crypto.randomUUID(),
1228
+ role: 'tool',
1229
+ content: `${icon} ${output}${output.length >= 200 ? '...' : ''}`,
1230
+ toolName: event.data.toolName,
1231
+ timestamp: new Date().toISOString(),
1232
+ });
1233
+ }
1234
+ });
1235
+ }
1236
+ else {
1237
+ const cli = new CLIChannel(config.channels.cli);
1238
+ channels.push(cli);
1239
+ wireChannel(cli);
1240
+ }
1241
+ // If other channels exist, don't let CLI stdin close kill the process
1242
+ if (config.channels.discord?.token || config.channels.telegram?.token) {
1243
+ process.env['TAKO_KEEP_ALIVE'] = '1';
1244
+ }
1245
+ const isSkillSlashCommand = (name) => skillCommandSpecs.some((s) => s.name === name);
1246
+ const handleSlashCommand = async (commandName, channelId, author, agentId, boundChannel) => {
1247
+ const channelKey = `discord:${channelId}`;
1248
+ const sessionKey = `agent:${agentId}:${channelKey}`;
1249
+ const session = sessions.getOrCreate(sessionKey, {
1250
+ name: `${agentId}/${channelKey}/${author.name}`,
1251
+ metadata: { agentId, channelType: 'discord', channelTarget: channelId, authorId: author.id },
1252
+ });
1253
+ const cmdResult = await commandRegistry.handle('/' + commandName, {
1254
+ channelId: channelKey,
1255
+ authorId: author.id,
1256
+ authorName: author.name,
1257
+ session,
1258
+ agentId,
1259
+ });
1260
+ if (cmdResult !== null)
1261
+ return cmdResult;
1262
+ // If this slash command came from a user-invocable skill, run it through AgentLoop
1263
+ if (!isSkillSlashCommand(commandName))
1264
+ return null;
1265
+ const activeLoop = getAgentLoop(agentId);
1266
+ activeLoop.setChannel(boundChannel);
1267
+ let response = '';
1268
+ for await (const chunk of activeLoop.run(session, `/${commandName}`)) {
1269
+ response += chunk;
1270
+ }
1271
+ return response || 'Done.';
1272
+ };
1273
+ // Build native command list from the command registry
1274
+ const nativeCommandList = [
1275
+ ...commandRegistry.list(),
1276
+ { name: 'setup', description: 'Configure agent channels (Discord/Telegram)' },
1277
+ ];
1278
+ if (config.channels.discord?.token) {
1279
+ discordChannel = new DiscordChannel({
1280
+ token: config.channels.discord.token,
1281
+ guilds: config.channels.discord.guilds,
1282
+ });
1283
+ // Register native Discord slash commands
1284
+ discordChannel.setSlashCommands(nativeCommandList, async (commandName, channelId, author, guildId) => {
1285
+ const agentId = resolveAgentForChannel(agentRegistry.list(), 'discord', channelId);
1286
+ return handleSlashCommand(commandName, channelId, author, agentId, discordChannel);
1287
+ });
1288
+ // Merge user-invocable skills into slash commands before connect (single registration on ready)
1289
+ await discordChannel.registerSkillCommands(skillCommandSpecs, async (commandName, channelId, author, guildId) => {
1290
+ const agentId = resolveAgentForChannel(agentRegistry.list(), 'discord', channelId);
1291
+ return handleSlashCommand(commandName, channelId, author, agentId, discordChannel);
1292
+ });
1293
+ // Register interactive model picker for Discord /model command
1294
+ discordChannel.setInteractiveHandler('model', async (interaction) => {
1295
+ // Build provider → models map from all known providers
1296
+ const providerModelsMap = {};
1297
+ // Anthropic models (always available)
1298
+ const anthropicProvider = new AnthropicProvider();
1299
+ providerModelsMap['anthropic'] = anthropicProvider.models().map((m) => m.id);
1300
+ // OpenAI models (always available)
1301
+ const openaiProvider = new OpenAIProvider();
1302
+ providerModelsMap['openai'] = openaiProvider.models().map((m) => m.id);
1303
+ // LiteLLM models (from config or active provider)
1304
+ if (config.providers.litellm?.baseUrl) {
1305
+ const litellmModels = config.providers.litellm.models ?? [];
1306
+ if (litellmModels.length > 0) {
1307
+ providerModelsMap['litellm'] = litellmModels;
1308
+ }
1309
+ else if (provider.id === 'litellm') {
1310
+ providerModelsMap['litellm'] = provider.models().map((m) => m.id);
1311
+ }
1312
+ }
1313
+ const providers = Object.keys(providerModelsMap);
1314
+ if (providers.length === 0)
1315
+ return false;
1316
+ await showModelPicker(interaction, {
1317
+ getModel: () => agentLoop.getModel(),
1318
+ setModel: (ref) => {
1319
+ agentLoop.setModel(ref);
1320
+ config.providers.primary = ref;
1321
+ import('./config/resolve.js').then(m => m.patchConfig({ providers: { primary: ref } })).catch(() => { });
1322
+ },
1323
+ getDefaultModel: () => defaultModel,
1324
+ getProviders: () => providers,
1325
+ getModelsForProvider: (p) => providerModelsMap[p] ?? [],
1326
+ });
1327
+ return true;
1328
+ });
1329
+ // Register interactive /setup command for channel configuration
1330
+ const setupDeps = {
1331
+ listAgents: () => agentRegistry.list().map((a) => ({ id: a.id, description: a.description })),
1332
+ saveChannelConfig: (agentId, channelType, cfg) => agentRegistry.saveChannelConfig(agentId, channelType, cfg),
1333
+ };
1334
+ discordChannel.setInteractiveHandler('setup', async (interaction) => {
1335
+ await handleSetupCommand(interaction, setupDeps);
1336
+ return true;
1337
+ });
1338
+ discordChannel.onSelectMenu(async (interaction) => {
1339
+ if (interaction.customId === 'setup_agent_select') {
1340
+ await handleAgentSelect(interaction);
1341
+ return true;
1342
+ }
1343
+ return false;
1344
+ });
1345
+ discordChannel.onButton(async (interaction) => {
1346
+ if (interaction.customId.startsWith('setup_type_') || interaction.customId === 'setup_cancel') {
1347
+ await handleChannelTypeButton(interaction);
1348
+ return true;
1349
+ }
1350
+ return false;
1351
+ });
1352
+ discordChannel.onModalSubmit(async (interaction) => {
1353
+ if (interaction.customId.startsWith('setup_modal_')) {
1354
+ await handleModalSubmit(interaction, setupDeps);
1355
+ return true;
1356
+ }
1357
+ return false;
1358
+ });
1359
+ discordChannels.push(discordChannel);
1360
+ channels.push(discordChannel);
1361
+ wireChannel(discordChannel);
1362
+ }
1363
+ if (config.channels.telegram?.token) {
1364
+ telegramChannel = new TelegramChannel({
1365
+ token: config.channels.telegram.token,
1366
+ allowedUsers: config.channels.telegram.allowedUsers,
1367
+ });
1368
+ // Register native Telegram command handlers
1369
+ telegramChannel.setCommands(nativeCommandList, async (commandName, chatId, author) => {
1370
+ const channelKey = `telegram:${chatId}`;
1371
+ const agentId = resolveAgentForChannel(agentRegistry.list(), 'telegram', chatId);
1372
+ const sessionKey = `agent:${agentId}:${channelKey}`;
1373
+ const session = sessions.getOrCreate(sessionKey, {
1374
+ name: `${agentId}/${channelKey}/${author.name}`,
1375
+ metadata: { agentId, channelType: 'telegram', channelTarget: chatId, authorId: author.id },
1376
+ });
1377
+ return commandRegistry.handle('/' + commandName, {
1378
+ channelId: channelKey,
1379
+ authorId: author.id,
1380
+ authorName: author.name,
1381
+ session,
1382
+ agentId,
1383
+ });
1384
+ });
1385
+ channels.push(telegramChannel);
1386
+ wireChannel(telegramChannel);
1387
+ }
1388
+ // ─── Per-agent channel setup ─────────────────────────────────────
1389
+ // Scan all non-main agents for channels.json and create separate
1390
+ // Discord/Telegram client instances bound to each agent.
1391
+ for (const agent of agentRegistry.list()) {
1392
+ if (agent.isMain)
1393
+ continue;
1394
+ const channelConfig = await agentRegistry.loadChannelConfig(agent.id);
1395
+ if (!channelConfig)
1396
+ continue;
1397
+ // Discord
1398
+ const discord = channelConfig.discord;
1399
+ if (discord?.enabled && discord?.token) {
1400
+ const agentDiscord = new DiscordChannel({
1401
+ token: discord.token,
1402
+ guilds: discord.guilds,
1403
+ });
1404
+ agentDiscord.agentId = agent.id;
1405
+ // Register slash commands for this agent's bot too
1406
+ agentDiscord.setSlashCommands(nativeCommandList, async (commandName, channelId, author, guildId) => {
1407
+ return handleSlashCommand(commandName, channelId, author, agent.id, agentDiscord);
1408
+ });
1409
+ // Merge user-invocable skill commands before connect (use agent-specific specs if available)
1410
+ const agentSpecificSkillSpecs = agentSkillCommandSpecsMap.get(agent.id) ?? skillCommandSpecs;
1411
+ await agentDiscord.registerSkillCommands(agentSpecificSkillSpecs, async (commandName, channelId, author, guildId) => {
1412
+ return handleSlashCommand(commandName, channelId, author, agent.id, agentDiscord);
1413
+ });
1414
+ discordChannels.push(agentDiscord);
1415
+ channels.push(agentDiscord);
1416
+ wireChannel(agentDiscord);
1417
+ console.log(`[tako] Agent "${agent.id}" Discord channel configured`);
1418
+ }
1419
+ // Telegram
1420
+ const telegram = channelConfig.telegram;
1421
+ if (telegram?.enabled && telegram?.token) {
1422
+ const agentTelegram = new TelegramChannel({
1423
+ token: telegram.token,
1424
+ allowedUsers: telegram.allowedUsers,
1425
+ });
1426
+ agentTelegram.agentId = agent.id;
1427
+ // Register commands for this agent's Telegram bot
1428
+ agentTelegram.setCommands(nativeCommandList, async (commandName, chatId, author) => {
1429
+ const channelKey = `telegram:${chatId}`;
1430
+ const sessionKey = `agent:${agent.id}:${channelKey}`;
1431
+ const session = sessions.getOrCreate(sessionKey, {
1432
+ name: `${agent.id}/${channelKey}/${author.name}`,
1433
+ metadata: { agentId: agent.id, channelType: 'telegram', channelTarget: chatId, authorId: author.id },
1434
+ });
1435
+ return commandRegistry.handle('/' + commandName, {
1436
+ channelId: channelKey,
1437
+ authorId: author.id,
1438
+ authorName: author.name,
1439
+ session,
1440
+ agentId: agent.id,
1441
+ });
1442
+ });
1443
+ channels.push(agentTelegram);
1444
+ wireChannel(agentTelegram);
1445
+ console.log(`[tako] Agent "${agent.id}" Telegram channel configured`);
1446
+ }
1447
+ }
1448
+ // ─── Skill-provided channels ─────────────────────────────────────
1449
+ // Load and register channels provided by skills (plugin pattern).
1450
+ const loadedSkills = skillLoader.getAll();
1451
+ for (const skill of loadedSkills) {
1452
+ if (skill.manifest.hasChannel) {
1453
+ const channelConfig = config.skillChannels?.[skill.manifest.name] ?? {};
1454
+ const channel = await loadChannelFromSkill(skill, channelConfig);
1455
+ if (channel) {
1456
+ channels.push(channel);
1457
+ wireChannel(channel);
1458
+ console.log(`[tako] Loaded skill channel: ${channel.id} (from skill: ${skill.manifest.name})`);
1459
+ }
1460
+ }
1461
+ }
1462
+ // ─── Skill extensions (unified subsystem plugins) ──────────────────
1463
+ const extensionRegistry = new ExtensionRegistry();
1464
+ // Load provider extensions
1465
+ for (const skill of getSkillsWithExtension(loadedSkills, 'provider')) {
1466
+ const providerConfig = config.skillExtensions?.[skill.manifest.name]?.provider ?? {};
1467
+ const provider = await loadExtension(skill, 'provider', providerConfig);
1468
+ if (provider) {
1469
+ extensionRegistry.register('provider', skill.manifest.name, provider);
1470
+ }
1471
+ }
1472
+ // Load memory extensions
1473
+ for (const skill of getSkillsWithExtension(loadedSkills, 'memory')) {
1474
+ const memConfig = config.skillExtensions?.[skill.manifest.name]?.memory ?? {};
1475
+ const memStore = await loadExtension(skill, 'memory', memConfig);
1476
+ if (memStore) {
1477
+ extensionRegistry.register('memory', skill.manifest.name, memStore);
1478
+ }
1479
+ }
1480
+ // Load channel extensions (via unified loader — supplements legacy hasChannel)
1481
+ for (const skill of getSkillsWithExtension(loadedSkills, 'channel')) {
1482
+ if (skill.manifest.hasChannel)
1483
+ continue; // Already loaded above via legacy path
1484
+ const chConfig = config.skillExtensions?.[skill.manifest.name]?.channel ?? {};
1485
+ const channel = await loadExtension(skill, 'channel', chConfig);
1486
+ if (channel) {
1487
+ extensionRegistry.register('channel', skill.manifest.name, channel);
1488
+ channels.push(channel);
1489
+ wireChannel(channel);
1490
+ console.log(`[tako] Loaded extension channel: ${channel.id} (from skill: ${skill.manifest.name})`);
1491
+ }
1492
+ }
1493
+ // Load network extensions
1494
+ for (const skill of getSkillsWithExtension(loadedSkills, 'network')) {
1495
+ const netConfig = config.skillExtensions?.[skill.manifest.name]?.network ?? {};
1496
+ const adapter = await loadExtension(skill, 'network', netConfig);
1497
+ if (adapter) {
1498
+ extensionRegistry.register('network', skill.manifest.name, adapter);
1499
+ }
1500
+ }
1501
+ // Register message tools (channel management: send, create/delete channels, threads, react)
1502
+ toolRegistry.registerAll(createMessageTools({
1503
+ discord: discordChannel,
1504
+ telegram: telegramChannel,
1505
+ }));
1506
+ // Register introspection tools (tako_status, tako_config, tako_logs, session_transcript)
1507
+ toolRegistry.registerAll(createIntrospectTools({
1508
+ config,
1509
+ sessions,
1510
+ startTime,
1511
+ channels,
1512
+ agentIds: agentRegistry.list().map((a) => a.id),
1513
+ skillCount: skillManifests.length,
1514
+ version: VERSION,
1515
+ }));
1516
+ // ─── Start Gateway ────────────────────────────────────────────────
1517
+ // Allow env override for gateway bind (needed for Docker: bind 0.0.0.0 inside container)
1518
+ if (process.env['TAKO_GATEWAY_BIND']) {
1519
+ config.gateway.bind = process.env['TAKO_GATEWAY_BIND'];
1520
+ }
1521
+ if (process.env['TAKO_GATEWAY_PORT']) {
1522
+ config.gateway.port = parseInt(process.env['TAKO_GATEWAY_PORT'], 10);
1523
+ }
1524
+ const gateway = new Gateway(config.gateway, { sessions, agentLoop, hooks, sandboxManager, retryQueue });
1525
+ await gateway.start();
1526
+ // Set status info for TUI clients
1527
+ gateway.setStatusInfo({
1528
+ model: config.providers.primary,
1529
+ tools: toolRegistry.getActiveTools().length,
1530
+ skills: skillManifests.length,
1531
+ channels: channels.map((c) => c.id),
1532
+ });
1533
+ // Write PID file for daemon management
1534
+ await writePidFile({
1535
+ pid: process.pid,
1536
+ startedAt: new Date().toISOString(),
1537
+ port: config.gateway.port,
1538
+ bind: config.gateway.bind,
1539
+ configPath: config._configPath,
1540
+ });
1541
+ // ─── SIGUSR1 — Graceful config reload ──────────────────────────
1542
+ process.on('SIGUSR1', async () => {
1543
+ console.log('[tako] Received SIGUSR1 — reloading config...');
1544
+ try {
1545
+ const newConfig = await resolveConfig();
1546
+ // Update model
1547
+ if (newConfig.providers.primary !== config.providers.primary) {
1548
+ agentLoop.setModel(newConfig.providers.primary);
1549
+ config.providers.primary = newConfig.providers.primary;
1550
+ console.log(`[tako] Model updated to: ${newConfig.providers.primary}`);
1551
+ }
1552
+ // Update tool profile
1553
+ if (newConfig.tools.profile !== config.tools.profile) {
1554
+ toolRegistry.setProfile(newConfig.tools.profile);
1555
+ config.tools.profile = newConfig.tools.profile;
1556
+ console.log(`[tako] Tool profile updated to: ${newConfig.tools.profile}`);
1557
+ }
1558
+ // Reload skills
1559
+ const newManifests = await skillLoader.discover();
1560
+ for (const manifest of newManifests) {
1561
+ const loaded = await skillLoader.load(manifest);
1562
+ skillLoader.registerTools(loaded, toolRegistry);
1563
+ skillLoader.registerHooks(loaded, hooks);
1564
+ }
1565
+ console.log(`[tako] Config reload complete. Skills: ${newManifests.length}`);
1566
+ // Re-register skill commands with Discord after reload
1567
+ if (discordChannels.length > 0) {
1568
+ try {
1569
+ const loadedAfterReload = skillLoader.getAll();
1570
+ const rebuiltSpecs = buildSkillCommands(loadedAfterReload);
1571
+ skillCommandSpecs.splice(0, skillCommandSpecs.length, ...rebuiltSpecs);
1572
+ for (const dc of discordChannels) {
1573
+ await dc.registerSkillCommands(skillCommandSpecs, async (commandName, channelId, author, guildId) => {
1574
+ const agentId = dc.agentId ?? resolveAgentForChannel(agentRegistry.list(), 'discord', channelId);
1575
+ return handleSlashCommand(commandName, channelId, author, agentId, dc);
1576
+ });
1577
+ }
1578
+ console.log(`[tako] Re-registered ${skillCommandSpecs.length} skill commands with Discord (${discordChannels.length} bot(s))`);
1579
+ }
1580
+ catch (err) {
1581
+ console.error('[tako] Failed to re-register commands:', err instanceof Error ? err.message : err);
1582
+ }
1583
+ }
1584
+ // Update gateway status info
1585
+ gateway.setStatusInfo({
1586
+ model: config.providers.primary,
1587
+ tools: toolRegistry.getActiveTools().length,
1588
+ skills: newManifests.length,
1589
+ channels: channels.map((c) => c.id),
1590
+ });
1591
+ }
1592
+ catch (err) {
1593
+ console.error('[tako] Config reload failed:', err instanceof Error ? err.message : err);
1594
+ }
1595
+ });
1596
+ // ─── Cron Scheduler ────────────────────────────────────────────────
1597
+ const { CronScheduler } = await import('./core/cron.js');
1598
+ const { createCronTools } = await import('./tools/cron-tools.js');
1599
+ const cronScheduler = new CronScheduler();
1600
+ cronScheduler.setHandlers({
1601
+ agentTurn: async (message, model) => {
1602
+ const cronSession = sessions.create({ name: 'cron', metadata: { isCron: true } });
1603
+ let response = '';
1604
+ for await (const chunk of agentLoop.run(cronSession, message)) {
1605
+ response += chunk;
1606
+ }
1607
+ return response;
1608
+ },
1609
+ systemEvent: (text) => {
1610
+ // Inject into main session
1611
+ const mainSession = sessions.get('main') ?? sessions.create({ name: 'main' });
1612
+ sessions.addMessage(mainSession.id, { role: 'system', content: text });
1613
+ },
1614
+ delivery: (result, delivery) => {
1615
+ if (delivery.mode === 'announce' && delivery.channel) {
1616
+ const ch = channels.find((c) => c.id === delivery.channel || c.id.startsWith(delivery.channel));
1617
+ if (ch) {
1618
+ ch.send({ target: delivery.to ?? '', content: `📋 **${result.jobName}**\n${result.response.slice(0, 1500)}` });
1619
+ }
1620
+ }
1621
+ },
1622
+ });
1623
+ toolRegistry.registerAll(createCronTools(cronScheduler));
1624
+ await cronScheduler.start();
1625
+ // ─── Session idle sweep ──────────────────────────────────────────
1626
+ // Every 2 minutes, check for sessions idle > 24h and archive them.
1627
+ // Files stay on disk — only removed from active maps.
1628
+ const idleSweepTimer = setInterval(async () => {
1629
+ const expired = sessions.sweepIdle();
1630
+ for (const session of expired) {
1631
+ const channelType = session.metadata.channelType;
1632
+ const target = session.metadata.channelTarget;
1633
+ if (channelType && target) {
1634
+ const channel = channels.find((ch) => ch.id === channelType);
1635
+ if (channel) {
1636
+ await channel.send({
1637
+ target,
1638
+ content: '⚙️ Session ended automatically after 24h of inactivity.',
1639
+ }).catch(() => { });
1640
+ }
1641
+ }
1642
+ sessions.archiveSession(session.id);
1643
+ }
1644
+ if (expired.length > 0) {
1645
+ console.log(`[tako] Archived ${expired.length} idle session(s)`);
1646
+ }
1647
+ // Sweep expired thread bindings (24h idle)
1648
+ const expiredBindings = threadBindings.sweepExpired();
1649
+ for (const binding of expiredBindings) {
1650
+ const discordCh = channels.find((ch) => ch.id === 'discord');
1651
+ if (discordCh) {
1652
+ await discordCh.send({
1653
+ target: binding.threadId,
1654
+ content: '⚙️ Session ended automatically after 24h of inactivity. Messages here will no longer be routed.',
1655
+ }).catch(() => { });
1656
+ // Archive the thread
1657
+ if ('archiveThread' in discordCh && typeof discordCh.archiveThread === 'function') {
1658
+ await discordCh.archiveThread(binding.threadId).catch(() => { });
1659
+ }
1660
+ }
1661
+ }
1662
+ if (expiredBindings.length > 0) {
1663
+ await threadBindings.save();
1664
+ console.log(`[tako] Swept ${expiredBindings.length} expired thread binding(s)`);
1665
+ }
1666
+ }, 120_000);
1667
+ // ─── Daily 4 AM session rotation ──────────────────────────────────
1668
+ // Start fresh sessions every day at 4:00 AM local time.
1669
+ // Old session files stay on disk for history.
1670
+ let rotationTimeout = null;
1671
+ function scheduleNextRotation() {
1672
+ const now = new Date();
1673
+ const next4am = new Date(now);
1674
+ next4am.setHours(4, 0, 0, 0);
1675
+ if (next4am <= now) {
1676
+ next4am.setDate(next4am.getDate() + 1);
1677
+ }
1678
+ const delay = next4am.getTime() - now.getTime();
1679
+ console.log(`[tako] Next session rotation at 4:00 AM (in ${Math.round(delay / 60000)}min)`);
1680
+ rotationTimeout = setTimeout(async () => {
1681
+ console.log('[tako] Running daily 4 AM session rotation...');
1682
+ try {
1683
+ const result = await sessions.rotateAllSessions();
1684
+ console.log(`[tako] Rotated: ${result.archived.length} archived, ${result.created.length} created`);
1685
+ }
1686
+ catch (err) {
1687
+ console.error('[tako] Rotation error:', err instanceof Error ? err.message : err);
1688
+ }
1689
+ scheduleNextRotation();
1690
+ }, delay);
1691
+ }
1692
+ scheduleNextRotation();
1693
+ // ─── Shutdown ─────────────────────────────────────────────────────
1694
+ const blockingIds = new Set(['cli', 'tui']);
1695
+ async function shutdown() {
1696
+ console.log('\n[tako] Shutting down...');
1697
+ // Log shutdown — don't broadcast to channels (too noisy on restarts)
1698
+ console.log('⚙️ Tako going offline.');
1699
+ clearInterval(idleSweepTimer);
1700
+ if (rotationTimeout)
1701
+ clearTimeout(rotationTimeout);
1702
+ messageQueue.clear();
1703
+ await threadBindings.save();
1704
+ cronScheduler.stop();
1705
+ skillLoader.stopWatching();
1706
+ deliveryQueue.stop();
1707
+ for (const ch of channels) {
1708
+ await ch.disconnect().catch(() => { });
1709
+ }
1710
+ await gateway.stop();
1711
+ await sandboxManager.shutdown();
1712
+ await sessions.shutdown();
1713
+ await removePidFile();
1714
+ }
1715
+ process.on('SIGINT', async () => { await shutdown(); process.exit(0); });
1716
+ process.on('SIGTERM', async () => { await shutdown(); process.exit(0); });
1717
+ // ─── Print startup banner ─────────────────────────────────────────
1718
+ const embeddingStatus = embeddingProvider ? 'vector+BM25' : 'BM25-only';
1719
+ const channelNames = channels.map((c) => c.id).join(', ');
1720
+ const loadedSkillNames = skillLoader.getAll().map((s) => s.manifest.name);
1721
+ // TUI has its own header — skip the text banner
1722
+ if (!useTui) {
1723
+ console.log(`Tako 🐙 v${VERSION}`);
1724
+ console.log(`Provider: ${resolvedProviderLabel}`);
1725
+ console.log(`Tools: ${toolRegistry.getActiveTools().length} active (profile: ${config.tools.profile})`);
1726
+ console.log(`Memory: ${embeddingStatus}`);
1727
+ console.log(`Skills: ${loadedSkillNames.length} loaded (${loadedSkillNames.join(', ') || 'none'})`);
1728
+ console.log(`Channels: ${channelNames}`);
1729
+ console.log(`Sandbox: ${config.sandbox.mode}${config.sandbox.mode !== 'off' ? ` (scope: ${config.sandbox.scope}, workspace: ${config.sandbox.workspaceAccess})` : ''}`);
1730
+ console.log(`Agents: ${agentRegistry.list().length} registered (${agentRegistry.list().map((a) => a.id).join(', ')})`);
1731
+ console.log(`Gateway: ws://${config.gateway.bind}:${config.gateway.port}`);
1732
+ console.log('Type /quit to exit.\n');
1733
+ }
1734
+ // Connect channels (CLI/TUI last since they block on input)
1735
+ for (const ch of channels) {
1736
+ if (!blockingIds.has(ch.id)) {
1737
+ try {
1738
+ await ch.connect();
1739
+ }
1740
+ catch (err) {
1741
+ console.error(`[${ch.id}] ✗ Failed to connect: ${err instanceof Error ? err.message : err}`);
1742
+ console.error(`[${ch.id}] Check your token/config with \`tako onboard\``);
1743
+ }
1744
+ }
1745
+ }
1746
+ // Helper: send a system message to all connected non-blocking channels
1747
+ async function broadcastToChannels(text, includeAgentChannels = false) {
1748
+ for (const ch of channels) {
1749
+ if (blockingIds.has(ch.id))
1750
+ continue;
1751
+ if (!includeAgentChannels && ch.agentId)
1752
+ continue;
1753
+ try {
1754
+ if (ch.broadcast) {
1755
+ await ch.broadcast(text);
1756
+ }
1757
+ }
1758
+ catch { /* channel may not be connected yet */ }
1759
+ }
1760
+ }
1761
+ // Wait a moment for channels to fully connect (Discord ClientReady, etc.)
1762
+ const hasExternalChannels = channels.some((ch) => !blockingIds.has(ch.id));
1763
+ if (hasExternalChannels) {
1764
+ await new Promise((r) => setTimeout(r, 2000));
1765
+ }
1766
+ // Log startup — don't broadcast to channels (too noisy on restarts)
1767
+ console.log(`🐙 Tako online — model: ${config.providers.primary}`);
1768
+ // Deliver restart note if one exists (from a prior system_restart call)
1769
+ try {
1770
+ const { readFileSync, unlinkSync } = await import('node:fs');
1771
+ const { homedir } = await import('node:os');
1772
+ const restartNotePath = join(homedir(), '.tako', 'restart-note.json');
1773
+ const raw = readFileSync(restartNotePath, 'utf-8');
1774
+ const restartNote = JSON.parse(raw);
1775
+ unlinkSync(restartNotePath);
1776
+ const noteText = `⚙️ ${restartNote.note}`;
1777
+ console.log(`[tako] Post-restart: ${restartNote.note}`);
1778
+ // Deliver to connected non-blocking channels (Discord, Telegram)
1779
+ await broadcastToChannels(noteText);
1780
+ }
1781
+ catch { /* no restart note, normal boot */ }
1782
+ // Connect the blocking channel (CLI or TUI) last
1783
+ const blockingChannel = channels.find((ch) => blockingIds.has(ch.id));
1784
+ if (blockingChannel) {
1785
+ await blockingChannel.connect();
1786
+ }
1787
+ }
1788
+ // ─── tako start --daemon ─────────────────────────────────────────────
1789
+ // ─── tako stop ──────────────────────────────────────────────────────
1790
+ // ─── tako restart ───────────────────────────────────────────────────
1791
+ // ─── tako tui ───────────────────────────────────────────────────────
1792
+ // ─── tako dev ───────────────────────────────────────────────────────
1793
+ // ─── tako doctor ─────────────────────────────────────────────────────
1794
+ async function runNuke(args) {
1795
+ const { homedir } = await import('node:os');
1796
+ const { rm, readdir } = await import('node:fs/promises');
1797
+ const { join } = await import('node:path');
1798
+ const readline = await import('node:readline');
1799
+ const takoDir = join(homedir(), '.tako');
1800
+ console.log('');
1801
+ console.log(' ⚠️ ⚠️ ⚠️ TAKO NUKE ⚠️ ⚠️ ⚠️');
1802
+ console.log('');
1803
+ console.log(' This will PERMANENTLY DELETE:');
1804
+ console.log('');
1805
+ // Show what exists
1806
+ const targets = [];
1807
+ const checks = [
1808
+ { name: 'Config', path: join(takoDir, 'tako.json'), description: 'tako.json (provider, channel, agent config)' },
1809
+ { name: 'Auth', path: join(takoDir, 'auth'), description: 'auth/ (API keys, OAuth tokens)' },
1810
+ { name: 'Workspace', path: join(takoDir, 'workspace'), description: 'workspace/ (SOUL.md, AGENTS.md, memory, files)' },
1811
+ { name: 'Agents', path: join(takoDir, 'agents'), description: 'agents/ (all agent configs and state)' },
1812
+ { name: 'Sessions', path: join(takoDir, 'sessions'), description: 'sessions/ (conversation history)' },
1813
+ { name: 'Mods', path: join(takoDir, 'mods'), description: 'mods/ (installed mods and their workspaces)' },
1814
+ { name: 'Cron', path: join(takoDir, 'cron'), description: 'cron/ (scheduled jobs)' },
1815
+ { name: 'PID', path: join(takoDir, 'tako.pid'), description: 'tako.pid (daemon PID file)' },
1816
+ ];
1817
+ const { existsSync } = await import('node:fs');
1818
+ for (const check of checks) {
1819
+ if (existsSync(check.path)) {
1820
+ targets.push(check);
1821
+ console.log(` ✗ ${check.description}`);
1822
+ }
1823
+ }
1824
+ if (targets.length === 0) {
1825
+ console.log(' (nothing found — ~/.tako/ is already clean)');
1826
+ return;
1827
+ }
1828
+ console.log('');
1829
+ console.log(` Location: ${takoDir}`);
1830
+ console.log('');
1831
+ // Triple confirmation
1832
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1833
+ const ask = (q) => new Promise((res) => rl.question(q, res));
1834
+ const answer1 = await ask(' Type "nuke" to confirm: ');
1835
+ if (answer1.trim().toLowerCase() !== 'nuke') {
1836
+ console.log(' Cancelled.');
1837
+ rl.close();
1838
+ return;
1839
+ }
1840
+ const answer2 = await ask(' Are you SURE? This cannot be undone. Type "yes i am sure": ');
1841
+ if (answer2.trim().toLowerCase() !== 'yes i am sure') {
1842
+ console.log(' Cancelled.');
1843
+ rl.close();
1844
+ return;
1845
+ }
1846
+ rl.close();
1847
+ console.log('');
1848
+ console.log(' Nuking...');
1849
+ // Stop daemon first if running
1850
+ try {
1851
+ const { getDaemonStatus, removePidFile } = await import('./daemon/pid.js');
1852
+ const status = await getDaemonStatus();
1853
+ if (status.running && status.info) {
1854
+ console.log(` Stopping daemon (PID: ${status.info.pid})...`);
1855
+ process.kill(status.info.pid, 'SIGTERM');
1856
+ await new Promise((r) => setTimeout(r, 2000));
1857
+ }
1858
+ await removePidFile();
1859
+ }
1860
+ catch { /* not running */ }
1861
+ // Delete everything
1862
+ for (const target of targets) {
1863
+ try {
1864
+ await rm(target.path, { recursive: true, force: true });
1865
+ console.log(` ✓ Deleted ${target.name}`);
1866
+ }
1867
+ catch (err) {
1868
+ console.error(` ✗ Failed to delete ${target.name}: ${err instanceof Error ? err.message : err}`);
1869
+ }
1870
+ }
1871
+ console.log('');
1872
+ console.log(' 🐙 Tako has been reset to factory defaults.');
1873
+ console.log(' Run `tako onboard` to set up again.');
1874
+ console.log('');
1875
+ }
1876
+ async function runMod(args) {
1877
+ const { ModManager } = await import('./mods/mod.js');
1878
+ const mods = new ModManager();
1879
+ const sub = args[0];
1880
+ switch (sub) {
1881
+ case 'list':
1882
+ case 'ls': {
1883
+ const all = await mods.list();
1884
+ const active = await mods.getActive();
1885
+ console.log(`Active: ${active}\n`);
1886
+ if (all.length === 0) {
1887
+ console.log('No mods installed.');
1888
+ console.log(' tako mod create <name> "description" Create a new mod');
1889
+ console.log(' tako mod install <path|git-url> Install a mod');
1890
+ }
1891
+ else {
1892
+ for (const mod of all) {
1893
+ const marker = mod.isActive ? ' ← active' : '';
1894
+ console.log(` ${mod.name} v${mod.manifest.version}${marker}`);
1895
+ if (mod.manifest.description)
1896
+ console.log(` ${mod.manifest.description}`);
1897
+ if (mod.manifest.author)
1898
+ console.log(` by ${mod.manifest.author}`);
1899
+ }
1900
+ }
1901
+ break;
1902
+ }
1903
+ case 'use':
1904
+ case 'switch': {
1905
+ const name = args[1];
1906
+ if (!name) {
1907
+ console.log('Usage: tako mod use <name|main>');
1908
+ process.exit(1);
1909
+ }
1910
+ const result = await mods.use(name);
1911
+ console.log(result.message);
1912
+ if (!result.success)
1913
+ process.exit(1);
1914
+ break;
1915
+ }
1916
+ case 'install':
1917
+ case 'add': {
1918
+ const source = args[1];
1919
+ if (!source) {
1920
+ console.log('Usage: tako mod install <path|git-url>');
1921
+ process.exit(1);
1922
+ }
1923
+ const result = source.includes('://') || source.endsWith('.git')
1924
+ ? await mods.installFromGit(source)
1925
+ : await mods.install(source);
1926
+ console.log(result.message);
1927
+ if (!result.success)
1928
+ process.exit(1);
1929
+ break;
1930
+ }
1931
+ case 'create':
1932
+ case 'new': {
1933
+ const name = args[1];
1934
+ const desc = args.slice(2).join(' ') || 'A Tako mod';
1935
+ if (!name) {
1936
+ console.log('Usage: tako mod create <name> [description]');
1937
+ process.exit(1);
1938
+ }
1939
+ const result = await mods.create(name, desc);
1940
+ console.log(result.message);
1941
+ break;
1942
+ }
1943
+ case 'remove':
1944
+ case 'rm': {
1945
+ const name = args[1];
1946
+ if (!name) {
1947
+ console.log('Usage: tako mod remove <name>');
1948
+ process.exit(1);
1949
+ }
1950
+ const result = await mods.remove(name);
1951
+ console.log(result.message);
1952
+ if (!result.success)
1953
+ process.exit(1);
1954
+ break;
1955
+ }
1956
+ case 'info': {
1957
+ const name = args[1] ?? await mods.getActive();
1958
+ const all = await mods.list();
1959
+ const mod = all.find((m) => m.name === name);
1960
+ if (!mod) {
1961
+ console.log(`Mod "${name}" not found.`);
1962
+ process.exit(1);
1963
+ }
1964
+ console.log(`${mod.manifest.name} v${mod.manifest.version}`);
1965
+ if (mod.manifest.description)
1966
+ console.log(`Description: ${mod.manifest.description}`);
1967
+ if (mod.manifest.author)
1968
+ console.log(`Author: ${mod.manifest.author}`);
1969
+ if (mod.manifest.source)
1970
+ console.log(`Source: ${mod.manifest.source}`);
1971
+ if (mod.manifest.tags?.length)
1972
+ console.log(`Tags: ${mod.manifest.tags.join(', ')}`);
1973
+ console.log(`Path: ${mod.path}`);
1974
+ console.log(`Active: ${mod.isActive}`);
1975
+ if (mod.config.provider)
1976
+ console.log(`Provider: ${mod.config.provider}`);
1977
+ break;
1978
+ }
1979
+ default:
1980
+ console.log('Tako Mod Hub 🐙\n');
1981
+ console.log('Usage: tako mod <command>\n');
1982
+ console.log('Commands:');
1983
+ console.log(' list List installed mods');
1984
+ console.log(' use <name|main> Switch to a mod (or back to main)');
1985
+ console.log(' install <path|git-url> Install a mod from local dir or git');
1986
+ console.log(' create <name> [desc] Create a new empty mod');
1987
+ console.log(' remove <name> Remove an installed mod');
1988
+ console.log(' info [name] Show mod details');
1989
+ console.log('');
1990
+ console.log('Mods are stored at: ~/.tako/mods/');
1991
+ console.log('');
1992
+ console.log('A mod packages: identity (SOUL.md), skills, workspace templates,');
1993
+ console.log('and config overrides — everything except your API keys and bot tokens.');
1994
+ console.log('');
1995
+ console.log('⚠️ After switching mods, restart Tako and reconnect channels if needed.');
1996
+ break;
1997
+ }
1998
+ }
1999
+ async function runDoctor(args) {
2000
+ const config = await resolveConfig();
2001
+ const doctor = new Doctor();
2002
+ doctor.addCheck(checkConfig);
2003
+ doctor.addCheck(checkProviders);
2004
+ doctor.addCheck(checkChannels);
2005
+ doctor.addCheck(checkMemory);
2006
+ doctor.addCheck(checkSessions);
2007
+ doctor.addCheck(checkPermissions);
2008
+ doctor.addCheck(checkBrowser);
2009
+ console.log('Tako Doctor — running health checks...\n');
2010
+ const results = await doctor.run(config, {
2011
+ autoRepair: args.includes('--yes') || args.includes('-y'),
2012
+ deep: args.includes('--deep'),
2013
+ });
2014
+ doctor.printResults(results);
2015
+ const hasErrors = results.some((r) => r.status === 'error');
2016
+ process.exit(hasErrors ? 1 : 0);
2017
+ }
2018
+ // ─── tako skills ─────────────────────────────────────────────────────
2019
+ async function runSkills(args) {
2020
+ const subcommand = args[0];
2021
+ if (!subcommand || subcommand === 'list') {
2022
+ await skillsList();
2023
+ }
2024
+ else if (subcommand === 'install') {
2025
+ const name = args[1];
2026
+ if (!name) {
2027
+ console.error('Usage: tako skills install <name>');
2028
+ console.error(' Example: tako skills install vercel-labs/agent-skills@find-skills');
2029
+ process.exit(1);
2030
+ }
2031
+ await skillsInstall(name);
2032
+ }
2033
+ else if (subcommand === 'info') {
2034
+ const name = args[1];
2035
+ if (!name) {
2036
+ console.error('Usage: tako skills info <name>');
2037
+ process.exit(1);
2038
+ }
2039
+ await skillsInfo(name);
2040
+ }
2041
+ else if (subcommand === 'check') {
2042
+ await skillsCheck();
2043
+ }
2044
+ else if (subcommand === 'audit') {
2045
+ const name = args[1];
2046
+ if (!name) {
2047
+ console.error('Usage: tako skills audit <name>');
2048
+ process.exit(1);
2049
+ }
2050
+ await skillsAudit(name);
2051
+ }
2052
+ else if (subcommand === 'search') {
2053
+ const query = args.slice(1).join(' ');
2054
+ if (!query) {
2055
+ console.error('Usage: tako skills search <query>');
2056
+ process.exit(1);
2057
+ }
2058
+ const { SkillMarketplace } = await import('./skills/marketplace.js');
2059
+ const marketplace = new SkillMarketplace();
2060
+ const results = await marketplace.search(query);
2061
+ if (results.length === 0) {
2062
+ console.log('No skills found matching your query.');
2063
+ return;
2064
+ }
2065
+ console.log(`Found ${results.length} skill(s):\n`);
2066
+ for (const r of results) {
2067
+ console.log(` ${r.fullName} (${r.stars} stars)`);
2068
+ console.log(` ${r.description || '(no description)'}`);
2069
+ console.log(` Install: tako skills install ${r.fullName}`);
2070
+ console.log();
2071
+ }
2072
+ }
2073
+ else if (subcommand === 'update') {
2074
+ const name = args[1];
2075
+ const { SkillMarketplace } = await import('./skills/marketplace.js');
2076
+ const marketplace = new SkillMarketplace();
2077
+ const updated = await marketplace.update(name);
2078
+ console.log(`Updated ${updated.length} skill(s):`);
2079
+ for (const s of updated) {
2080
+ console.log(` ${s.name} → ${s.version ?? 'latest'}`);
2081
+ }
2082
+ }
2083
+ else if (subcommand === 'remove' || subcommand === 'rm') {
2084
+ const name = args[1];
2085
+ if (!name) {
2086
+ console.error('Usage: tako skills remove <name>');
2087
+ process.exit(1);
2088
+ }
2089
+ const { SkillMarketplace } = await import('./skills/marketplace.js');
2090
+ const marketplace = new SkillMarketplace();
2091
+ const removed = await marketplace.remove(name);
2092
+ if (removed) {
2093
+ console.log(`Removed skill: ${name}`);
2094
+ }
2095
+ else {
2096
+ console.error(`Skill not found: ${name}`);
2097
+ process.exit(1);
2098
+ }
2099
+ }
2100
+ else {
2101
+ console.error(`Unknown skills subcommand: ${subcommand}`);
2102
+ console.error('Available: list, install, search, update, remove, info, check, audit');
2103
+ process.exit(1);
2104
+ }
2105
+ }
2106
+ async function skillsList() {
2107
+ const config = await resolveConfig();
2108
+ const loader = new SkillLoader(config.skills.dirs);
2109
+ const manifests = await loader.discover();
2110
+ if (manifests.length === 0) {
2111
+ console.log('No skills installed.');
2112
+ console.log('\nInstall skills with: tako skills install <name>');
2113
+ console.log('Browse available skills at: https://skills.sh/');
2114
+ return;
2115
+ }
2116
+ console.log(`Discovered ${manifests.length} skill(s):\n`);
2117
+ for (const m of manifests) {
2118
+ const triggers = m.triggers
2119
+ ? m.triggers.map((t) => t.type === 'keyword' ? t.value : t.type).join(', ')
2120
+ : 'always';
2121
+ console.log(` ${m.name} (v${m.version})`);
2122
+ console.log(` ${m.description.slice(0, 80)}${m.description.length > 80 ? '...' : ''}`);
2123
+ console.log(` Triggers: ${triggers}`);
2124
+ console.log(` Path: ${m.rootDir}`);
2125
+ console.log();
2126
+ }
2127
+ }
2128
+ async function skillsInstall(nameOrRef) {
2129
+ const { execSync } = await import('node:child_process');
2130
+ console.log(`Installing skill: ${nameOrRef}...`);
2131
+ try {
2132
+ execSync(`npx skills add ${nameOrRef} -y`, { stdio: 'inherit', cwd: process.cwd() });
2133
+ console.log('\nSkill installed. Run `tako skills list` to verify.');
2134
+ }
2135
+ catch {
2136
+ console.error('\nFailed to install skill. Make sure `npx skills` is available.');
2137
+ console.error('You can also manually create a skill directory in ./skills/ with a SKILL.md file.');
2138
+ process.exit(1);
2139
+ }
2140
+ }
2141
+ async function skillsInfo(name) {
2142
+ const config = await resolveConfig();
2143
+ const loader = new SkillLoader(config.skills.dirs);
2144
+ const manifests = await loader.discover();
2145
+ const manifest = manifests.find((m) => m.name === name);
2146
+ if (!manifest) {
2147
+ console.error(`Skill not found: ${name}`);
2148
+ console.error(`Available skills: ${manifests.map((m) => m.name).join(', ') || 'none'}`);
2149
+ process.exit(1);
2150
+ }
2151
+ const loaded = await loader.load(manifest);
2152
+ console.log(`Skill: ${manifest.name}`);
2153
+ console.log(`Version: ${manifest.version}`);
2154
+ if (manifest.author)
2155
+ console.log(`Author: ${manifest.author}`);
2156
+ console.log(`Description: ${manifest.description}`);
2157
+ console.log(`Path: ${manifest.rootDir}`);
2158
+ console.log(`SKILL.md: ${manifest.skillPath}`);
2159
+ if (manifest.triggers && manifest.triggers.length > 0) {
2160
+ console.log(`\nTriggers:`);
2161
+ for (const t of manifest.triggers) {
2162
+ console.log(` - ${t.type}${t.value ? `: ${t.value}` : ''}`);
2163
+ }
2164
+ }
2165
+ if (loaded.tools.length > 0) {
2166
+ console.log(`\nTools (${loaded.tools.length}):`);
2167
+ for (const t of loaded.tools) {
2168
+ console.log(` - ${t.name}: ${t.description}`);
2169
+ }
2170
+ }
2171
+ const preview = loaded.instructions.slice(0, 500);
2172
+ console.log(`\nInstructions (${loaded.instructions.length} chars):`);
2173
+ console.log(preview + (loaded.instructions.length > 500 ? '\n ...' : ''));
2174
+ }
2175
+ async function skillsCheck() {
2176
+ const config = await resolveConfig();
2177
+ const loader = new SkillLoader(config.skills.dirs);
2178
+ const manifests = await loader.discover();
2179
+ if (manifests.length === 0) {
2180
+ console.log('No skills discovered.');
2181
+ return;
2182
+ }
2183
+ console.log(`Checking ${manifests.length} skill(s):\n`);
2184
+ let ready = 0;
2185
+ let failed = 0;
2186
+ for (const m of manifests) {
2187
+ try {
2188
+ const loaded = await loader.load(m);
2189
+ console.log(` ✓ ${m.name} (v${m.version}) — ${loaded.tools.length} tool(s)`);
2190
+ ready++;
2191
+ }
2192
+ catch (err) {
2193
+ console.log(` ✗ ${m.name} (v${m.version}) — ${err instanceof Error ? err.message : 'load failed'}`);
2194
+ failed++;
2195
+ }
2196
+ }
2197
+ console.log(`\n${ready} ready, ${failed} failed`);
2198
+ }
2199
+ async function skillsAudit(name) {
2200
+ const config = await resolveConfig();
2201
+ const loader = new SkillLoader(config.skills.dirs);
2202
+ const manifests = await loader.discover();
2203
+ const manifest = manifests.find((m) => m.name === name);
2204
+ if (!manifest) {
2205
+ console.error(`Skill not found: ${name}`);
2206
+ console.error(`Available skills: ${manifests.map((m) => m.name).join(', ') || 'none'}`);
2207
+ process.exit(1);
2208
+ }
2209
+ const loaded = await loader.load(manifest);
2210
+ console.log(`Security Audit: ${manifest.name} (v${manifest.version})\n`);
2211
+ console.log(`Author: ${manifest.author ?? 'unknown'}`);
2212
+ console.log(`Path: ${manifest.rootDir}`);
2213
+ // Tools analysis
2214
+ console.log(`\nTools (${loaded.tools.length}):`);
2215
+ for (const t of loaded.tools) {
2216
+ const params = t.parameters ? Object.keys(t.parameters.properties ?? {}).join(', ') : 'none';
2217
+ console.log(` ${t.name}: ${t.description}`);
2218
+ console.log(` Parameters: ${params}`);
2219
+ }
2220
+ // Triggers analysis
2221
+ if (manifest.triggers && manifest.triggers.length > 0) {
2222
+ console.log(`\nTriggers (${manifest.triggers.length}):`);
2223
+ for (const t of manifest.triggers) {
2224
+ console.log(` - ${t.type}${t.value ? `: ${t.value}` : ''}`);
2225
+ }
2226
+ }
2227
+ else {
2228
+ console.log('\nTriggers: always active (no triggers defined)');
2229
+ }
2230
+ // Instruction size
2231
+ console.log(`\nInstruction size: ${loaded.instructions.length} chars`);
2232
+ // Warnings
2233
+ const warnings = [];
2234
+ if (!manifest.author)
2235
+ warnings.push('No author specified');
2236
+ if (!manifest.triggers || manifest.triggers.length === 0)
2237
+ warnings.push('Always active (no trigger gating)');
2238
+ if (loaded.instructions.length > 10000)
2239
+ warnings.push(`Large instructions (${loaded.instructions.length} chars may impact context)`);
2240
+ if (loaded.tools.length > 5)
2241
+ warnings.push(`Many tools (${loaded.tools.length}) — consider splitting`);
2242
+ if (warnings.length > 0) {
2243
+ console.log(`\nWarnings:`);
2244
+ for (const w of warnings) {
2245
+ console.log(` ⚠ ${w}`);
2246
+ }
2247
+ }
2248
+ else {
2249
+ console.log('\nNo warnings.');
2250
+ }
2251
+ }
2252
+ // ─── tako sandbox ────────────────────────────────────────────────────
2253
+ async function runSandbox(args) {
2254
+ const subcommand = args[0] ?? 'status';
2255
+ const config = await resolveConfig();
2256
+ switch (subcommand) {
2257
+ case 'status': {
2258
+ const manager = new SandboxManager(config.sandbox);
2259
+ const status = await manager.getStatus();
2260
+ console.log('Tako Sandbox Status\n');
2261
+ console.log(`Mode: ${status.mode}`);
2262
+ console.log(`Docker: ${status.dockerAvailable ? 'available' : 'NOT available'}`);
2263
+ console.log(`Scope: ${config.sandbox.scope}`);
2264
+ console.log(`Workspace access: ${config.sandbox.workspaceAccess}`);
2265
+ console.log(`Image: ${config.sandbox.docker?.image ?? 'tako-sandbox:bookworm-slim'}`);
2266
+ console.log(`Network: ${config.sandbox.docker?.network ?? 'none'}`);
2267
+ if (status.dockerAvailable) {
2268
+ const containers = await DockerContainer.listSandboxContainers();
2269
+ if (containers.length > 0) {
2270
+ console.log(`\nActive sandbox containers (${containers.length}):`);
2271
+ for (const c of containers) {
2272
+ console.log(` ${c.id} ${c.name} (${c.running ? 'running' : 'stopped'})`);
2273
+ }
2274
+ }
2275
+ else {
2276
+ console.log('\nNo active sandbox containers.');
2277
+ }
2278
+ }
2279
+ // Exec policy
2280
+ if (config.tools.exec) {
2281
+ console.log(`\nExec policy:`);
2282
+ console.log(` Security: ${config.tools.exec.security}`);
2283
+ if (config.tools.exec.allowlist) {
2284
+ console.log(` Allowlist: ${config.tools.exec.allowlist.length} patterns`);
2285
+ }
2286
+ if (config.tools.exec.timeout) {
2287
+ console.log(` Timeout: ${config.tools.exec.timeout}ms`);
2288
+ }
2289
+ }
2290
+ else {
2291
+ console.log(`\nExec policy: full (no restrictions)`);
2292
+ }
2293
+ break;
2294
+ }
2295
+ case 'explain': {
2296
+ const toolName = args[1];
2297
+ if (!toolName) {
2298
+ console.error('Usage: tako sandbox explain <tool-name>');
2299
+ console.error(' Example: tako sandbox explain exec');
2300
+ process.exit(1);
2301
+ }
2302
+ // Sandbox explanation
2303
+ const manager = new SandboxManager(config.sandbox);
2304
+ console.log(manager.explain(toolName, true));
2305
+ console.log();
2306
+ // Tool policy explanation
2307
+ const toolPolicy = new ToolPolicy({
2308
+ profile: config.tools.profile,
2309
+ allow: config.tools.allow,
2310
+ deny: config.tools.deny,
2311
+ sandbox: config.tools.sandbox,
2312
+ exec: config.tools.exec ? {
2313
+ security: config.tools.exec.security,
2314
+ allowlist: config.tools.exec.allowlist,
2315
+ timeout: config.tools.exec.timeout,
2316
+ maxOutputSize: config.tools.exec.maxOutputSize,
2317
+ } : undefined,
2318
+ });
2319
+ console.log('Tool Policy:');
2320
+ console.log(toolPolicy.explain(toolName, config.sandbox.mode !== 'off'));
2321
+ break;
2322
+ }
2323
+ case 'cleanup': {
2324
+ const dockerOk = await DockerContainer.isDockerAvailable();
2325
+ if (!dockerOk) {
2326
+ console.log('Docker is not available. Nothing to clean up.');
2327
+ return;
2328
+ }
2329
+ const count = await DockerContainer.cleanupAll();
2330
+ console.log(`Removed ${count} sandbox container(s).`);
2331
+ break;
2332
+ }
2333
+ default:
2334
+ console.error(`Unknown sandbox subcommand: ${subcommand}`);
2335
+ console.error('Available: status, explain <tool>, cleanup');
2336
+ process.exit(1);
2337
+ }
2338
+ }
2339
+ // ─── tako agents ─────────────────────────────────────────────────────
2340
+ async function runAgents(args) {
2341
+ const config = await resolveConfig();
2342
+ const registry = new AgentRegistry(config.agents, config.providers.primary);
2343
+ await registry.loadDynamic();
2344
+ const subcommand = args[0] ?? 'list';
2345
+ switch (subcommand) {
2346
+ case 'list': {
2347
+ const showBindings = args.includes('--bindings');
2348
+ const agents = registry.list();
2349
+ if (agents.length === 0) {
2350
+ console.log('No agents configured (only default "main" agent).');
2351
+ return;
2352
+ }
2353
+ console.log(`Agents (${agents.length}):\n`);
2354
+ for (const agent of agents) {
2355
+ console.log(` ${agent.id}${agent.isMain ? ' (main)' : ''}`);
2356
+ console.log(` Workspace: ${agent.workspace}`);
2357
+ console.log(` Model: ${agent.model}`);
2358
+ if (agent.description)
2359
+ console.log(` Description: ${agent.description}`);
2360
+ if (agent.canSpawn.length > 0)
2361
+ console.log(` Can spawn: ${agent.canSpawn.join(', ')}`);
2362
+ if (showBindings && Object.keys(agent.bindings).length > 0) {
2363
+ console.log(` Bindings: ${JSON.stringify(agent.bindings)}`);
2364
+ }
2365
+ console.log();
2366
+ }
2367
+ break;
2368
+ }
2369
+ case 'add': {
2370
+ const nameArg = args[1];
2371
+ const hasFlags = args.some((a) => a.startsWith('--'));
2372
+ const isInteractive = !nameArg && !hasFlags;
2373
+ let agentName;
2374
+ let workspace;
2375
+ let model;
2376
+ let description;
2377
+ let discordChannels;
2378
+ let telegramUsers;
2379
+ if (isInteractive) {
2380
+ // ─── Interactive wizard ────────────────────────────────────
2381
+ const p = await import('@clack/prompts');
2382
+ p.intro('Tako 🐙 — New Agent Setup');
2383
+ const nameResult = await p.text({
2384
+ message: 'Agent name (lowercase, hyphens ok)',
2385
+ placeholder: 'code-agent',
2386
+ validate: (v) => {
2387
+ if (!v)
2388
+ return 'Name is required';
2389
+ if (!/^[a-z][a-z0-9-]*$/.test(v))
2390
+ return 'Use lowercase letters, numbers, and hyphens';
2391
+ if (v === 'main')
2392
+ return '"main" is reserved';
2393
+ if (registry.has(v))
2394
+ return `Agent "${v}" already exists`;
2395
+ return undefined;
2396
+ },
2397
+ });
2398
+ if (p.isCancel(nameResult)) {
2399
+ p.cancel('Cancelled.');
2400
+ return;
2401
+ }
2402
+ agentName = nameResult;
2403
+ const descResult = await p.text({
2404
+ message: 'Description (what does this agent do?)',
2405
+ placeholder: 'Handles code review and refactoring tasks',
2406
+ });
2407
+ if (p.isCancel(descResult)) {
2408
+ p.cancel('Cancelled.');
2409
+ return;
2410
+ }
2411
+ description = descResult || undefined;
2412
+ const wsResult = await p.text({
2413
+ message: 'Workspace path',
2414
+ placeholder: `~/.tako/workspace-${agentName}`,
2415
+ defaultValue: `~/.tako/workspace-${agentName}`,
2416
+ });
2417
+ if (p.isCancel(wsResult)) {
2418
+ p.cancel('Cancelled.');
2419
+ return;
2420
+ }
2421
+ workspace = wsResult || `~/.tako/workspace-${agentName}`;
2422
+ const modelResult = await p.select({
2423
+ message: 'Model',
2424
+ options: [
2425
+ { value: '', label: `Inherit from main (${config.providers.primary})`, hint: 'recommended' },
2426
+ { value: 'anthropic/claude-sonnet-4-6', label: 'claude-sonnet-4-6', hint: 'fast, balanced' },
2427
+ { value: 'anthropic/claude-opus-4-6', label: 'claude-opus-4-6', hint: 'powerful, slower' },
2428
+ { value: 'anthropic/claude-haiku-4-5', label: 'claude-haiku-4-5', hint: 'fastest, cheapest' },
2429
+ ],
2430
+ });
2431
+ if (p.isCancel(modelResult)) {
2432
+ p.cancel('Cancelled.');
2433
+ return;
2434
+ }
2435
+ model = modelResult || undefined;
2436
+ // Channel bindings
2437
+ const bindResult = await p.confirm({
2438
+ message: 'Set up channel bindings? (route specific channels to this agent)',
2439
+ initialValue: false,
2440
+ });
2441
+ if (p.isCancel(bindResult)) {
2442
+ p.cancel('Cancelled.');
2443
+ return;
2444
+ }
2445
+ if (bindResult) {
2446
+ if (config.channels.discord?.token) {
2447
+ const dcResult = await p.text({
2448
+ message: 'Discord channel names (comma-separated, or empty to skip)',
2449
+ placeholder: 'coding, code-review',
2450
+ });
2451
+ if (p.isCancel(dcResult)) {
2452
+ p.cancel('Cancelled.');
2453
+ return;
2454
+ }
2455
+ if (dcResult) {
2456
+ discordChannels = dcResult.split(',').map((s) => s.trim()).filter(Boolean);
2457
+ }
2458
+ }
2459
+ if (config.channels.telegram?.token) {
2460
+ const tgResult = await p.text({
2461
+ message: 'Telegram user IDs to route (comma-separated, or empty to skip)',
2462
+ placeholder: '123456789',
2463
+ });
2464
+ if (p.isCancel(tgResult)) {
2465
+ p.cancel('Cancelled.');
2466
+ return;
2467
+ }
2468
+ if (tgResult) {
2469
+ telegramUsers = tgResult.split(',').map((s) => s.trim()).filter(Boolean);
2470
+ }
2471
+ }
2472
+ }
2473
+ // Confirmation
2474
+ p.note([
2475
+ `Name: ${agentName}`,
2476
+ `Description: ${description || '(none)'}`,
2477
+ `Workspace: ${workspace}`,
2478
+ `Model: ${model || `inherit (${config.providers.primary})`}`,
2479
+ discordChannels ? `Discord: ${discordChannels.join(', ')}` : null,
2480
+ telegramUsers ? `Telegram: ${telegramUsers.join(', ')}` : null,
2481
+ ].filter(Boolean).join('\n'), 'New Agent');
2482
+ const confirmResult = await p.confirm({ message: 'Create this agent?', initialValue: true });
2483
+ if (p.isCancel(confirmResult) || !confirmResult) {
2484
+ p.cancel('Cancelled.');
2485
+ return;
2486
+ }
2487
+ }
2488
+ else {
2489
+ // ─── Non-interactive (flags) ──────────────────────────────
2490
+ if (!nameArg) {
2491
+ console.error('Usage: tako agents add <name> [--workspace <path>] [--model <model>] [--description <desc>]');
2492
+ console.error(' tako agents add (interactive wizard)');
2493
+ process.exit(1);
2494
+ }
2495
+ agentName = nameArg;
2496
+ const workspaceIdx = args.indexOf('--workspace');
2497
+ const modelIdx = args.indexOf('--model');
2498
+ const descIdx = args.indexOf('--description');
2499
+ const discordIdx = args.indexOf('--discord-channels');
2500
+ const telegramIdx = args.indexOf('--telegram-users');
2501
+ workspace = workspaceIdx >= 0 ? args[workspaceIdx + 1] : undefined;
2502
+ model = modelIdx >= 0 ? args[modelIdx + 1] : undefined;
2503
+ description = descIdx >= 0 ? args[descIdx + 1] : undefined;
2504
+ if (discordIdx >= 0 && args[discordIdx + 1]) {
2505
+ discordChannels = args[discordIdx + 1].split(',').map((s) => s.trim());
2506
+ }
2507
+ if (telegramIdx >= 0 && args[telegramIdx + 1]) {
2508
+ telegramUsers = args[telegramIdx + 1].split(',').map((s) => s.trim());
2509
+ }
2510
+ }
2511
+ // Build bindings
2512
+ const bindings = {};
2513
+ if (discordChannels && discordChannels.length > 0) {
2514
+ bindings.discord = { channels: discordChannels };
2515
+ }
2516
+ if (telegramUsers && telegramUsers.length > 0) {
2517
+ bindings.telegram = { users: telegramUsers };
2518
+ }
2519
+ try {
2520
+ const agent = await registry.add({
2521
+ id: agentName,
2522
+ workspace,
2523
+ model: model ? { primary: model } : undefined,
2524
+ description,
2525
+ bindings: Object.keys(bindings).length > 0 ? bindings : undefined,
2526
+ });
2527
+ console.log(`\nAgent created: ${agent.id}`);
2528
+ console.log(` Workspace: ${agent.workspace}`);
2529
+ console.log(` State dir: ${agent.stateDir}`);
2530
+ console.log(` Sessions: ${agent.sessionDir}`);
2531
+ console.log(` Model: ${agent.model}`);
2532
+ if (agent.description)
2533
+ console.log(` Description: ${agent.description}`);
2534
+ if (Object.keys(agent.bindings).length > 0) {
2535
+ console.log(` Bindings: ${JSON.stringify(agent.bindings)}`);
2536
+ }
2537
+ console.log(`\nWorkspace files created:`);
2538
+ console.log(` AGENTS.md — Operating instructions`);
2539
+ console.log(` SOUL.md — Personality & values`);
2540
+ console.log(` IDENTITY.md — Name, capabilities`);
2541
+ console.log(` USER.md — User profile (empty)`);
2542
+ console.log(` TOOLS.md — Tool learnings (empty)`);
2543
+ console.log(` HEARTBEAT.md — Status update behavior`);
2544
+ console.log(` BOOTSTRAP.md — First-run ritual`);
2545
+ console.log(` memory/MEMORY.md — Long-term memory`);
2546
+ }
2547
+ catch (err) {
2548
+ console.error(`Failed to create agent: ${err instanceof Error ? err.message : err}`);
2549
+ process.exit(1);
2550
+ }
2551
+ break;
2552
+ }
2553
+ case 'remove': {
2554
+ const name = args[1];
2555
+ if (!name) {
2556
+ console.error('Usage: tako agents remove <name>');
2557
+ process.exit(1);
2558
+ }
2559
+ try {
2560
+ const removed = await registry.remove(name);
2561
+ if (removed) {
2562
+ console.log(`Agent removed: ${name}`);
2563
+ console.log('Note: agent workspace was preserved (only state directory was removed).');
2564
+ }
2565
+ else {
2566
+ console.error(`Agent not found: ${name}`);
2567
+ process.exit(1);
2568
+ }
2569
+ }
2570
+ catch (err) {
2571
+ console.error(`Failed to remove agent: ${err instanceof Error ? err.message : err}`);
2572
+ process.exit(1);
2573
+ }
2574
+ break;
2575
+ }
2576
+ case 'info': {
2577
+ const name = args[1];
2578
+ if (!name) {
2579
+ console.error('Usage: tako agents info <name>');
2580
+ process.exit(1);
2581
+ }
2582
+ const info = registry.info(name);
2583
+ if (!info) {
2584
+ console.error(`Agent not found: ${name}`);
2585
+ console.error(`Available agents: ${registry.list().map((a) => a.id).join(', ')}`);
2586
+ process.exit(1);
2587
+ }
2588
+ console.log(`Agent: ${info.id}`);
2589
+ console.log(JSON.stringify(info, null, 2));
2590
+ break;
2591
+ }
2592
+ case 'bind': {
2593
+ const agentId = args[1];
2594
+ const channel = args.includes('--channel') ? args[args.indexOf('--channel') + 1] : undefined;
2595
+ const target = args.includes('--target') ? args[args.indexOf('--target') + 1] : undefined;
2596
+ if (!agentId || !channel || !target) {
2597
+ console.error('Usage: tako agents bind <agentId> --channel <discord|telegram> --target <channelId>');
2598
+ process.exit(1);
2599
+ }
2600
+ const agent = registry.get(agentId);
2601
+ if (!agent) {
2602
+ console.error(`Agent not found: ${agentId}`);
2603
+ process.exit(1);
2604
+ }
2605
+ // Update bindings
2606
+ const bindings = { ...agent.bindings };
2607
+ if (channel === 'discord') {
2608
+ const existing = bindings.discord?.channels ?? [];
2609
+ if (!existing.includes(target)) {
2610
+ bindings.discord = { channels: [...existing, target] };
2611
+ }
2612
+ }
2613
+ else if (channel === 'telegram') {
2614
+ const existing = bindings.telegram?.users ?? [];
2615
+ if (!existing.includes(target)) {
2616
+ bindings.telegram = { users: [...existing, target] };
2617
+ }
2618
+ }
2619
+ else {
2620
+ console.error(`Unknown channel type: ${channel}. Use "discord" or "telegram".`);
2621
+ process.exit(1);
2622
+ }
2623
+ // Persist to agent.json
2624
+ const agentJsonPath = join(homedir(), '.tako', 'agents', agentId, 'agent.json');
2625
+ if (existsSync(agentJsonPath)) {
2626
+ const raw = await readFile(agentJsonPath, 'utf-8');
2627
+ const entry = JSON.parse(raw);
2628
+ entry.bindings = bindings;
2629
+ await writeFile(agentJsonPath, JSON.stringify(entry, null, 2), 'utf-8');
2630
+ }
2631
+ console.log(`Bound ${channel}/${target} → agent ${agentId}`);
2632
+ break;
2633
+ }
2634
+ case 'unbind': {
2635
+ const agentId = args[1];
2636
+ const channel = args.includes('--channel') ? args[args.indexOf('--channel') + 1] : undefined;
2637
+ const target = args.includes('--target') ? args[args.indexOf('--target') + 1] : undefined;
2638
+ if (!agentId || !channel || !target) {
2639
+ console.error('Usage: tako agents unbind <agentId> --channel <discord|telegram> --target <channelId>');
2640
+ process.exit(1);
2641
+ }
2642
+ const agent = registry.get(agentId);
2643
+ if (!agent) {
2644
+ console.error(`Agent not found: ${agentId}`);
2645
+ process.exit(1);
2646
+ }
2647
+ const bindings = { ...agent.bindings };
2648
+ if (channel === 'discord' && bindings.discord) {
2649
+ bindings.discord.channels = bindings.discord.channels.filter((c) => c !== target);
2650
+ }
2651
+ else if (channel === 'telegram' && bindings.telegram) {
2652
+ bindings.telegram.users = (bindings.telegram.users ?? []).filter((u) => u !== target);
2653
+ }
2654
+ const agentJsonPath = join(homedir(), '.tako', 'agents', agentId, 'agent.json');
2655
+ if (existsSync(agentJsonPath)) {
2656
+ const raw = await readFile(agentJsonPath, 'utf-8');
2657
+ const entry = JSON.parse(raw);
2658
+ entry.bindings = bindings;
2659
+ await writeFile(agentJsonPath, JSON.stringify(entry, null, 2), 'utf-8');
2660
+ }
2661
+ console.log(`Unbound ${channel}/${target} from agent ${agentId}`);
2662
+ break;
2663
+ }
2664
+ case 'bindings': {
2665
+ const agents = registry.list();
2666
+ let hasBindings = false;
2667
+ console.log('Agent Bindings:\n');
2668
+ for (const agent of agents) {
2669
+ const b = agent.bindings;
2670
+ if (!b || (Object.keys(b).length === 0))
2671
+ continue;
2672
+ hasBindings = true;
2673
+ console.log(` ${agent.id}:`);
2674
+ if (b.discord?.channels?.length) {
2675
+ console.log(` Discord: ${b.discord.channels.join(', ')}`);
2676
+ }
2677
+ if (b.telegram?.users?.length) {
2678
+ console.log(` Telegram users: ${b.telegram.users.join(', ')}`);
2679
+ }
2680
+ if (b.telegram?.groups?.length) {
2681
+ console.log(` Telegram groups: ${b.telegram.groups.join(', ')}`);
2682
+ }
2683
+ if (b.cli) {
2684
+ console.log(` CLI: bound`);
2685
+ }
2686
+ console.log();
2687
+ }
2688
+ if (!hasBindings) {
2689
+ console.log(' No bindings configured.');
2690
+ console.log('\n Add bindings with: tako agents bind <agentId> --channel discord --target <channelId>');
2691
+ }
2692
+ break;
2693
+ }
2694
+ case 'set-identity': {
2695
+ const agentId = args[1];
2696
+ if (!agentId) {
2697
+ console.error('Usage: tako agents set-identity <agentId> --name <name> [--emoji <emoji>]');
2698
+ process.exit(1);
2699
+ }
2700
+ const agent = registry.get(agentId);
2701
+ if (!agent) {
2702
+ console.error(`Agent not found: ${agentId}`);
2703
+ process.exit(1);
2704
+ }
2705
+ const nameArg = args.includes('--name') ? args[args.indexOf('--name') + 1] : undefined;
2706
+ const emojiArg = args.includes('--emoji') ? args[args.indexOf('--emoji') + 1] : undefined;
2707
+ if (!nameArg && !emojiArg) {
2708
+ console.error('Provide at least --name or --emoji');
2709
+ process.exit(1);
2710
+ }
2711
+ const agentJsonPath = join(homedir(), '.tako', 'agents', agentId, 'agent.json');
2712
+ let entry = {};
2713
+ if (existsSync(agentJsonPath)) {
2714
+ const raw = await readFile(agentJsonPath, 'utf-8');
2715
+ entry = JSON.parse(raw);
2716
+ }
2717
+ if (nameArg)
2718
+ entry.displayName = nameArg;
2719
+ if (emojiArg)
2720
+ entry.emoji = emojiArg;
2721
+ await writeFile(agentJsonPath, JSON.stringify(entry, null, 2), 'utf-8');
2722
+ console.log(`Updated identity for agent ${agentId}:`);
2723
+ if (nameArg)
2724
+ console.log(` Name: ${nameArg}`);
2725
+ if (emojiArg)
2726
+ console.log(` Emoji: ${emojiArg}`);
2727
+ break;
2728
+ }
2729
+ default:
2730
+ console.error(`Unknown agents subcommand: ${subcommand}`);
2731
+ console.error('Available: list, add, remove, info, bind, unbind, bindings, set-identity');
2732
+ process.exit(1);
2733
+ }
2734
+ }
2735
+ // ─── tako sessions ──────────────────────────────────────────────────
2736
+ async function runSessions(args) {
2737
+ const config = await resolveConfig();
2738
+ const sessions = new SessionManager();
2739
+ const agentRegistry = new AgentRegistry(config.agents, config.providers.primary);
2740
+ await agentRegistry.loadDynamic();
2741
+ const agentSessionDirs = new Map();
2742
+ for (const agent of agentRegistry.list()) {
2743
+ agentSessionDirs.set(agent.id, agent.sessionDir);
2744
+ }
2745
+ await sessions.enablePersistence(agentSessionDirs);
2746
+ const subcommand = args[0] ?? 'list';
2747
+ switch (subcommand) {
2748
+ case 'list': {
2749
+ const agentFilter = args.includes('--agent') ? args[args.indexOf('--agent') + 1] : undefined;
2750
+ let allSessions = sessions.list();
2751
+ if (agentFilter) {
2752
+ allSessions = allSessions.filter((s) => (s.metadata.agentId ?? 'main') === agentFilter);
2753
+ }
2754
+ // Sort by most recent
2755
+ allSessions.sort((a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime());
2756
+ if (allSessions.length === 0) {
2757
+ console.log('No sessions found.');
2758
+ sessions.stopPersistence();
2759
+ return;
2760
+ }
2761
+ console.log(`Sessions (${allSessions.length}):\n`);
2762
+ for (const s of allSessions.slice(0, 50)) {
2763
+ const agentId = s.metadata.agentId ?? 'main';
2764
+ const isSubAgent = s.metadata.isSubAgent ? ' [sub-agent]' : '';
2765
+ console.log(` ${s.id.slice(0, 8)} ${s.name} (${agentId}${isSubAgent})`);
2766
+ console.log(` Messages: ${s.messages.length} Last active: ${s.lastActiveAt.toISOString()}`);
2767
+ }
2768
+ break;
2769
+ }
2770
+ case 'history': {
2771
+ const sessionId = args[1];
2772
+ if (!sessionId) {
2773
+ console.error('Usage: tako sessions history <session-id> [--limit <n>]');
2774
+ process.exit(1);
2775
+ }
2776
+ // Support partial session ID match
2777
+ let session = sessions.get(sessionId);
2778
+ if (!session) {
2779
+ const match = sessions.list().find((s) => s.id.startsWith(sessionId));
2780
+ if (match)
2781
+ session = match;
2782
+ }
2783
+ if (!session) {
2784
+ console.error(`Session not found: ${sessionId}`);
2785
+ sessions.stopPersistence();
2786
+ process.exit(1);
2787
+ }
2788
+ const limitIdx = args.indexOf('--limit');
2789
+ const limit = limitIdx >= 0 ? parseInt(args[limitIdx + 1] ?? '20', 10) : 20;
2790
+ const messages = session.messages.slice(-limit);
2791
+ console.log(`Session: ${session.name} (${session.id})`);
2792
+ console.log(`Total messages: ${session.messages.length}\n`);
2793
+ for (const msg of messages) {
2794
+ const content = typeof msg.content === 'string'
2795
+ ? msg.content.slice(0, 200)
2796
+ : '[complex content]';
2797
+ console.log(` [${msg.role}] ${content}`);
2798
+ }
2799
+ break;
2800
+ }
2801
+ case 'inspect': {
2802
+ const sessionId = args[1];
2803
+ if (!sessionId) {
2804
+ console.error('Usage: tako sessions inspect <session-id>');
2805
+ process.exit(1);
2806
+ }
2807
+ let session = sessions.get(sessionId);
2808
+ if (!session) {
2809
+ const match = sessions.list().find((s) => s.id.startsWith(sessionId));
2810
+ if (match)
2811
+ session = match;
2812
+ }
2813
+ if (!session) {
2814
+ console.error(`Session not found: ${sessionId}`);
2815
+ sessions.stopPersistence();
2816
+ process.exit(1);
2817
+ }
2818
+ console.log(`Session: ${session.name}`);
2819
+ console.log(` ID: ${session.id}`);
2820
+ console.log(` Created: ${session.createdAt.toISOString()}`);
2821
+ console.log(` Last active: ${session.lastActiveAt.toISOString()}`);
2822
+ console.log(` Messages: ${session.messages.length}`);
2823
+ console.log(` Agent: ${session.metadata.agentId ?? 'main'}`);
2824
+ if (session.metadata.isSubAgent)
2825
+ console.log(` Sub-agent: yes`);
2826
+ if (Object.keys(session.metadata).length > 0) {
2827
+ console.log(` Metadata: ${JSON.stringify(session.metadata, null, 2)}`);
2828
+ }
2829
+ // Show last 10 messages
2830
+ const recent = session.messages.slice(-10);
2831
+ if (recent.length > 0) {
2832
+ console.log(`\nRecent messages (last ${recent.length}):\n`);
2833
+ for (const msg of recent) {
2834
+ const content = typeof msg.content === 'string'
2835
+ ? msg.content.slice(0, 300)
2836
+ : '[complex content]';
2837
+ console.log(` [${msg.role}] ${content}`);
2838
+ }
2839
+ }
2840
+ break;
2841
+ }
2842
+ case 'compact': {
2843
+ const sessionId = args[1];
2844
+ if (!sessionId) {
2845
+ console.error('Usage: tako sessions compact <session-id>');
2846
+ process.exit(1);
2847
+ }
2848
+ console.log('Session compaction requires the daemon to be running.');
2849
+ console.log('Use the /compact command inside an active session instead.');
2850
+ console.log('Or start Tako and the auto-compaction will handle it based on your config.');
2851
+ break;
2852
+ }
2853
+ case 'clear': {
2854
+ const sessionId = args[1];
2855
+ if (!sessionId) {
2856
+ console.error('Usage: tako sessions clear <session-id>');
2857
+ process.exit(1);
2858
+ }
2859
+ let session = sessions.get(sessionId);
2860
+ if (!session) {
2861
+ const match = sessions.list().find((s) => s.id.startsWith(sessionId));
2862
+ if (match)
2863
+ session = match;
2864
+ }
2865
+ if (!session) {
2866
+ console.error(`Session not found: ${sessionId}`);
2867
+ sessions.stopPersistence();
2868
+ process.exit(1);
2869
+ }
2870
+ // Archive by renaming the session file
2871
+ const agentId = session.metadata.agentId ?? 'main';
2872
+ const agentDir = agentSessionDirs.get(agentId);
2873
+ if (agentDir) {
2874
+ const sessionFile = join(agentDir, `${session.id}.jsonl`);
2875
+ const archiveFile = join(agentDir, `${session.id}.jsonl.archived`);
2876
+ if (existsSync(sessionFile)) {
2877
+ await rename(sessionFile, archiveFile);
2878
+ console.log(`Session ${session.id.slice(0, 8)} archived.`);
2879
+ console.log(` File moved to: ${archiveFile}`);
2880
+ }
2881
+ else {
2882
+ console.log('Session file not found on disk (may be in-memory only).');
2883
+ }
2884
+ }
2885
+ break;
2886
+ }
2887
+ default:
2888
+ console.error(`Unknown sessions subcommand: ${subcommand}`);
2889
+ console.error('Available: list, history, inspect, compact, clear');
2890
+ process.exit(1);
2891
+ }
2892
+ sessions.stopPersistence();
2893
+ }
2894
+ // ─── tako status ─────────────────────────────────────────────────────
2895
+ // ─── Entry ───────────────────────────────────────────────────────────
2896
+ main().catch((err) => {
2897
+ console.error('Fatal:', err);
2898
+ process.exit(1);
2899
+ });
2900
+ //# sourceMappingURL=index.js.map