@softize/opus 8.6.6

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 (286) hide show
  1. package/CHANGELOG.md +1616 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -0
  4. package/bin/cli.mjs +528 -0
  5. package/bin/lib/check.mjs +307 -0
  6. package/bin/lib/components.mjs +151 -0
  7. package/bin/lib/create.mjs +208 -0
  8. package/bin/lib/db-check-runner.mjs +86 -0
  9. package/bin/lib/db-migrate-runner.mjs +89 -0
  10. package/bin/lib/db-scaffold-runner.mjs +84 -0
  11. package/bin/lib/db.mjs +261 -0
  12. package/bin/lib/docs-include.mjs +48 -0
  13. package/bin/lib/gen-dicts.mjs +134 -0
  14. package/bin/lib/gen-docs.mjs +288 -0
  15. package/bin/lib/gen-manifest.mjs +102 -0
  16. package/bin/lib/gen-openapi.mjs +195 -0
  17. package/bin/lib/gen-runner.mjs +472 -0
  18. package/bin/lib/gen-stubs.mjs +463 -0
  19. package/bin/lib/gen.mjs +311 -0
  20. package/bin/lib/init.mjs +514 -0
  21. package/bin/lib/introspect.mjs +107 -0
  22. package/bin/lib/mcp.mjs +85 -0
  23. package/bin/lib/postinstall.mjs +56 -0
  24. package/docs/chat-event-protocol.md +85 -0
  25. package/docs/code-style.md +16 -0
  26. package/docs/data-layer.md +246 -0
  27. package/docs/ownership-vs-shadcn-lock.md +102 -0
  28. package/docs/protocol.md +2053 -0
  29. package/docs/releasing.md +110 -0
  30. package/docs/shellnav.md +131 -0
  31. package/package.json +338 -0
  32. package/registry/hooks/hooks.json +26 -0
  33. package/registry/hooks/link-memory-on-start.mjs +46 -0
  34. package/registry/hooks/opus-check-on-stop.mjs +114 -0
  35. package/registry/skills/create-action/SKILL.md +49 -0
  36. package/registry/skills/create-action/scaffold.mjs +122 -0
  37. package/registry/templates/app/_gitignore +3 -0
  38. package/registry/templates/app/_npmrc +1 -0
  39. package/registry/templates/app/_opus/_gitignore +5 -0
  40. package/registry/templates/app/_prettierrc.json +6 -0
  41. package/registry/templates/app/index.html +13 -0
  42. package/registry/templates/app/opus.config.ts +16 -0
  43. package/registry/templates/app/package.json +43 -0
  44. package/registry/templates/app/pnpm-workspace.yaml +11 -0
  45. package/registry/templates/app/public/favicon.svg +4 -0
  46. package/registry/templates/app/src/App.tsx +37 -0
  47. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
  48. package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
  49. package/registry/templates/app/src/domains/tasks/index.ts +13 -0
  50. package/registry/templates/app/src/index.css +18 -0
  51. package/registry/templates/app/src/main.tsx +25 -0
  52. package/registry/templates/app/tsconfig.json +20 -0
  53. package/registry/templates/app/vite.config.ts +46 -0
  54. package/registry/templates/monorepo/_gitignore +3 -0
  55. package/registry/templates/monorepo/_npmrc +1 -0
  56. package/registry/templates/monorepo/package.json +9 -0
  57. package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
  58. package/src/ai/ask.ts +64 -0
  59. package/src/ai/drivers/anthropic.ts +309 -0
  60. package/src/ai/index.ts +17 -0
  61. package/src/audit/drivers/console.ts +117 -0
  62. package/src/audit/drivers/pg.ts +172 -0
  63. package/src/audit/index.ts +51 -0
  64. package/src/auth/drivers/better-auth.ts +103 -0
  65. package/src/auth/drivers/jwt.ts +188 -0
  66. package/src/auth/index.ts +9 -0
  67. package/src/client/drivers/fetch.ts +202 -0
  68. package/src/client/index.ts +22 -0
  69. package/src/core/actions.ts +110 -0
  70. package/src/core/audit.ts +239 -0
  71. package/src/core/contracts.ts +137 -0
  72. package/src/core/domain.ts +310 -0
  73. package/src/core/errors.ts +181 -0
  74. package/src/core/index.ts +174 -0
  75. package/src/core/logical-type.ts +31 -0
  76. package/src/core/reactions.ts +81 -0
  77. package/src/core/runtime.ts +1167 -0
  78. package/src/core/schedules.ts +41 -0
  79. package/src/core/types.ts +1356 -0
  80. package/src/data/drivers/kysely.ts +389 -0
  81. package/src/data/index.ts +10 -0
  82. package/src/data/readonly-pool.ts +160 -0
  83. package/src/dsl/eval.ts +136 -0
  84. package/src/dsl/index.ts +29 -0
  85. package/src/dsl/kysely.ts +230 -0
  86. package/src/dsl/loads.ts +123 -0
  87. package/src/dsl/parser.ts +423 -0
  88. package/src/dsl/types.ts +113 -0
  89. package/src/events/drivers/mitt.ts +70 -0
  90. package/src/events/index.ts +9 -0
  91. package/src/log/drivers/pino.ts +57 -0
  92. package/src/log/index.ts +9 -0
  93. package/src/mcp/index.ts +62 -0
  94. package/src/queue/drivers/bullmq.ts +190 -0
  95. package/src/queue/index.ts +9 -0
  96. package/src/scheduler/drivers/node-cron.ts +93 -0
  97. package/src/scheduler/every.ts +45 -0
  98. package/src/scheduler/index.ts +9 -0
  99. package/src/schema/drivers/zod.ts +765 -0
  100. package/src/schema/entity.ts +439 -0
  101. package/src/schema/format/locale.ts +144 -0
  102. package/src/schema/index.ts +65 -0
  103. package/src/schema/openapi.ts +302 -0
  104. package/src/schema/scaffold.ts +160 -0
  105. package/src/server/drivers/fastify.ts +224 -0
  106. package/src/server/drivers/node.ts +386 -0
  107. package/src/server/index.ts +142 -0
  108. package/src/storage/drivers/fs.ts +90 -0
  109. package/src/storage/drivers/s3.ts +117 -0
  110. package/src/storage/index.ts +27 -0
  111. package/src/testing/fake.ts +298 -0
  112. package/src/testing/index.ts +324 -0
  113. package/src/ui/components/patterns/action-form-card.tsx +48 -0
  114. package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
  115. package/src/ui/components/patterns/app-shell.tsx +227 -0
  116. package/src/ui/components/patterns/confirm.tsx +226 -0
  117. package/src/ui/components/patterns/data-state.tsx +75 -0
  118. package/src/ui/components/patterns/form-dialog.tsx +64 -0
  119. package/src/ui/components/patterns/form.tsx +584 -0
  120. package/src/ui/components/patterns/list.tsx +1488 -0
  121. package/src/ui/components/patterns/page.tsx +46 -0
  122. package/src/ui/components/patterns/section-shell.tsx +246 -0
  123. package/src/ui/components/patterns/shell-nav.tsx +150 -0
  124. package/src/ui/components/patterns/sidebar.tsx +89 -0
  125. package/src/ui/components/patterns/split.tsx +93 -0
  126. package/src/ui/components/patterns/trigger.tsx +196 -0
  127. package/src/ui/components/patterns/view.tsx +84 -0
  128. package/src/ui/components/primitives/accordion.tsx +64 -0
  129. package/src/ui/components/primitives/alert-dialog.tsx +190 -0
  130. package/src/ui/components/primitives/alert.tsx +116 -0
  131. package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
  132. package/src/ui/components/primitives/avatar.tsx +107 -0
  133. package/src/ui/components/primitives/badge.tsx +37 -0
  134. package/src/ui/components/primitives/breadcrumb.tsx +109 -0
  135. package/src/ui/components/primitives/button-group.tsx +83 -0
  136. package/src/ui/components/primitives/button.tsx +102 -0
  137. package/src/ui/components/primitives/calendar.tsx +218 -0
  138. package/src/ui/components/primitives/card.tsx +56 -0
  139. package/src/ui/components/primitives/carousel.tsx +239 -0
  140. package/src/ui/components/primitives/chat.tsx +407 -0
  141. package/src/ui/components/primitives/checkbox.tsx +30 -0
  142. package/src/ui/components/primitives/collapsible.tsx +31 -0
  143. package/src/ui/components/primitives/command.tsx +182 -0
  144. package/src/ui/components/primitives/composer.tsx +121 -0
  145. package/src/ui/components/primitives/copyable.tsx +50 -0
  146. package/src/ui/components/primitives/dialog.tsx +147 -0
  147. package/src/ui/components/primitives/drawer.tsx +141 -0
  148. package/src/ui/components/primitives/empty.tsx +104 -0
  149. package/src/ui/components/primitives/field.tsx +246 -0
  150. package/src/ui/components/primitives/icon-picker.tsx +180 -0
  151. package/src/ui/components/primitives/input-group.tsx +168 -0
  152. package/src/ui/components/primitives/input-otp.tsx +75 -0
  153. package/src/ui/components/primitives/input.tsx +72 -0
  154. package/src/ui/components/primitives/item.tsx +193 -0
  155. package/src/ui/components/primitives/kbd.tsx +28 -0
  156. package/src/ui/components/primitives/label.tsx +22 -0
  157. package/src/ui/components/primitives/markdown.tsx +35 -0
  158. package/src/ui/components/primitives/menu.tsx +255 -0
  159. package/src/ui/components/primitives/pagination.tsx +127 -0
  160. package/src/ui/components/primitives/popover.tsx +87 -0
  161. package/src/ui/components/primitives/progress.tsx +29 -0
  162. package/src/ui/components/primitives/radio-group.tsx +43 -0
  163. package/src/ui/components/primitives/resizable.tsx +51 -0
  164. package/src/ui/components/primitives/scroll-area.tsx +56 -0
  165. package/src/ui/components/primitives/select.tsx +479 -0
  166. package/src/ui/components/primitives/separator.tsx +26 -0
  167. package/src/ui/components/primitives/skeleton.tsx +13 -0
  168. package/src/ui/components/primitives/slider.tsx +61 -0
  169. package/src/ui/components/primitives/sonner.tsx +46 -0
  170. package/src/ui/components/primitives/spinner.tsx +29 -0
  171. package/src/ui/components/primitives/switch.tsx +33 -0
  172. package/src/ui/components/primitives/table.tsx +114 -0
  173. package/src/ui/components/primitives/tabs.tsx +104 -0
  174. package/src/ui/components/primitives/textarea.tsx +18 -0
  175. package/src/ui/components/primitives/toggle-group.tsx +81 -0
  176. package/src/ui/components/primitives/toggle.tsx +45 -0
  177. package/src/ui/components/primitives/tooltip.tsx +55 -0
  178. package/src/ui/components/primitives/truncate.tsx +49 -0
  179. package/src/ui/docs/DocBrowser.tsx +90 -0
  180. package/src/ui/docs/changelog.tsx +80 -0
  181. package/src/ui/docs/content/accordion.md +86 -0
  182. package/src/ui/docs/content/action-form-card.md +24 -0
  183. package/src/ui/docs/content/action-form-dialog.md +30 -0
  184. package/src/ui/docs/content/action-form.md +125 -0
  185. package/src/ui/docs/content/action-list-dialog.md +68 -0
  186. package/src/ui/docs/content/action-list.md +194 -0
  187. package/src/ui/docs/content/action-trigger.md +72 -0
  188. package/src/ui/docs/content/action-view.md +47 -0
  189. package/src/ui/docs/content/actions.md +138 -0
  190. package/src/ui/docs/content/ai.md +112 -0
  191. package/src/ui/docs/content/alert-dialog.md +73 -0
  192. package/src/ui/docs/content/alert.md +69 -0
  193. package/src/ui/docs/content/app-shell.md +155 -0
  194. package/src/ui/docs/content/aspect-ratio.md +66 -0
  195. package/src/ui/docs/content/audit.md +84 -0
  196. package/src/ui/docs/content/auth.md +70 -0
  197. package/src/ui/docs/content/avatar.md +94 -0
  198. package/src/ui/docs/content/badge.md +48 -0
  199. package/src/ui/docs/content/breadcrumb.md +87 -0
  200. package/src/ui/docs/content/button-group.md +71 -0
  201. package/src/ui/docs/content/button.md +60 -0
  202. package/src/ui/docs/content/calendar.md +62 -0
  203. package/src/ui/docs/content/card.md +49 -0
  204. package/src/ui/docs/content/carousel.md +85 -0
  205. package/src/ui/docs/content/chat.md +69 -0
  206. package/src/ui/docs/content/checkbox.md +75 -0
  207. package/src/ui/docs/content/cli.md +58 -0
  208. package/src/ui/docs/content/collapsible.md +64 -0
  209. package/src/ui/docs/content/command.md +56 -0
  210. package/src/ui/docs/content/composer.md +50 -0
  211. package/src/ui/docs/content/confirm.md +120 -0
  212. package/src/ui/docs/content/copyable.md +30 -0
  213. package/src/ui/docs/content/customization.md +110 -0
  214. package/src/ui/docs/content/cycle.md +34 -0
  215. package/src/ui/docs/content/data-state.md +47 -0
  216. package/src/ui/docs/content/data.md +99 -0
  217. package/src/ui/docs/content/dialog.md +60 -0
  218. package/src/ui/docs/content/drawer.md +55 -0
  219. package/src/ui/docs/content/empty.md +66 -0
  220. package/src/ui/docs/content/events.md +61 -0
  221. package/src/ui/docs/content/field.md +58 -0
  222. package/src/ui/docs/content/getting-started.md +109 -0
  223. package/src/ui/docs/content/icon-picker.md +51 -0
  224. package/src/ui/docs/content/input-group.md +78 -0
  225. package/src/ui/docs/content/input-otp.md +72 -0
  226. package/src/ui/docs/content/input.md +78 -0
  227. package/src/ui/docs/content/item.md +84 -0
  228. package/src/ui/docs/content/kbd.md +62 -0
  229. package/src/ui/docs/content/label.md +32 -0
  230. package/src/ui/docs/content/log.md +55 -0
  231. package/src/ui/docs/content/markdown.md +41 -0
  232. package/src/ui/docs/content/mcp.md +44 -0
  233. package/src/ui/docs/content/menu.md +114 -0
  234. package/src/ui/docs/content/microcopy.md +83 -0
  235. package/src/ui/docs/content/page.md +34 -0
  236. package/src/ui/docs/content/pagination.md +99 -0
  237. package/src/ui/docs/content/popover.md +49 -0
  238. package/src/ui/docs/content/progress.md +69 -0
  239. package/src/ui/docs/content/queue.md +62 -0
  240. package/src/ui/docs/content/radio-group.md +77 -0
  241. package/src/ui/docs/content/resizable.md +86 -0
  242. package/src/ui/docs/content/router.md +56 -0
  243. package/src/ui/docs/content/runtime.md +77 -0
  244. package/src/ui/docs/content/scheduler.md +66 -0
  245. package/src/ui/docs/content/scroll-area.md +89 -0
  246. package/src/ui/docs/content/section-shell.md +121 -0
  247. package/src/ui/docs/content/select.md +342 -0
  248. package/src/ui/docs/content/separator.md +33 -0
  249. package/src/ui/docs/content/sidebar.md +38 -0
  250. package/src/ui/docs/content/skeleton.md +34 -0
  251. package/src/ui/docs/content/slider.md +64 -0
  252. package/src/ui/docs/content/spinner.md +37 -0
  253. package/src/ui/docs/content/split.md +33 -0
  254. package/src/ui/docs/content/storage.md +69 -0
  255. package/src/ui/docs/content/switch.md +69 -0
  256. package/src/ui/docs/content/table.md +102 -0
  257. package/src/ui/docs/content/tabs.md +94 -0
  258. package/src/ui/docs/content/testing.md +89 -0
  259. package/src/ui/docs/content/textarea.md +30 -0
  260. package/src/ui/docs/content/toast.md +67 -0
  261. package/src/ui/docs/content/toggle-group.md +81 -0
  262. package/src/ui/docs/content/toggle.md +72 -0
  263. package/src/ui/docs/content/tokens.md +171 -0
  264. package/src/ui/docs/content/tooltip.md +50 -0
  265. package/src/ui/docs/content/truncate.md +37 -0
  266. package/src/ui/docs/content/ui.md +40 -0
  267. package/src/ui/docs/content/upgrading.md +48 -0
  268. package/src/ui/docs/doc-client.tsx +214 -0
  269. package/src/ui/docs/doc.tsx +301 -0
  270. package/src/ui/docs/folder.tsx +149 -0
  271. package/src/ui/docs/index.ts +21 -0
  272. package/src/ui/docs/markdown.tsx +130 -0
  273. package/src/ui/docs/md-raw.d.ts +4 -0
  274. package/src/ui/docs/plugin.ts +104 -0
  275. package/src/ui/docs/registry.tsx +424 -0
  276. package/src/ui/docs/standalone.tsx +107 -0
  277. package/src/ui/drivers/react.tsx +627 -0
  278. package/src/ui/index.ts +92 -0
  279. package/src/ui/lib/cn.ts +10 -0
  280. package/src/ui/lib/zod-pt-br.ts +38 -0
  281. package/src/ui/meta.ts +412 -0
  282. package/src/ui/react.tsx +235 -0
  283. package/src/ui/router.ts +96 -0
  284. package/src/ui/theme.css +234 -0
  285. package/src/vite/design.ts +652 -0
  286. package/src/vite/index.ts +8 -0
@@ -0,0 +1,14 @@
1
+ # Membros do monorepo: apps (produtos) em apps/*, libs compartilhadas em packages/*.
2
+ packages:
3
+ - 'apps/*'
4
+ - 'packages/*'
5
+
6
+ # Permissão de build scripts (o pnpm bloqueia por default): o postinstall do
7
+ # @softize/opus roda o `opus setup` a cada install; o esbuild (vite) compila o binário.
8
+ allowBuilds:
9
+ '@softize/opus': true
10
+ esbuild: true
11
+
12
+ # A base é do nosso registry (confiada): fora da quarentena de release do pnpm.
13
+ minimumReleaseAgeExclude:
14
+ - '@softize/opus'
package/src/ai/ask.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Contrato de "perguntar à pessoa no meio do turno" (elicitação) — o ponto de CONSISTÊNCIA
3
+ * pra qualquer consumidor de IA do Opus. O driver reserva o nome `ask_user` e, com um
4
+ * `onAsk` (ver `AiRunOptions.onAsk`), faz o round-trip real; sem ele, recusa com erro
5
+ * acionável em vez de deixar a chamada morrer no vazio. Aqui vivem o nome reservado, o
6
+ * schema/descrição da tool e o formatador da resposta.
7
+ */
8
+ import type { AskQuestion, AskAnswer } from '../core/index.ts'
9
+
10
+ /** Nome reservado da tool de perguntar à pessoa — o driver a intercepta (ver `onAsk`). */
11
+ export const ASK_USER_TOOL = 'ask_user'
12
+
13
+ /** Descrição da tool — instrui o modelo a preferir isto a chutar. */
14
+ export const ASK_TOOL_DESCRIPTION =
15
+ 'Faça UMA rodada de perguntas de esclarecimento à pessoa e BLOQUEIE até ela responder. ' +
16
+ 'Use quando faltar informação pra decidir (escopo, formato, obrigatoriedade) em vez de assumir. ' +
17
+ 'Envie de 1 a 4 perguntas objetivas, cada uma com opções curtas; a ferramenta devolve as escolhas da pessoa.'
18
+
19
+ /** JSON Schema do input do `ask_user` — fonte única (o driver injeta a tool com isto). */
20
+ export const ASK_INPUT_SCHEMA = {
21
+ type: 'object',
22
+ properties: {
23
+ questions: {
24
+ type: 'array',
25
+ minItems: 1,
26
+ maxItems: 4,
27
+ description: 'De 1 a 4 perguntas objetivas de esclarecimento.',
28
+ items: {
29
+ type: 'object',
30
+ properties: {
31
+ question: { type: 'string', description: 'O enunciado da pergunta.' },
32
+ header: { type: 'string', description: 'Rótulo curto (1-2 palavras) que resume a pergunta.' },
33
+ multiSelect: { type: 'boolean', description: 'Permite marcar mais de uma opção.' },
34
+ options: {
35
+ type: 'array',
36
+ minItems: 2,
37
+ items: {
38
+ type: 'object',
39
+ properties: { label: { type: 'string' }, description: { type: 'string' } },
40
+ required: ['label'],
41
+ },
42
+ },
43
+ },
44
+ required: ['question', 'options'],
45
+ },
46
+ },
47
+ },
48
+ required: ['questions'],
49
+ } as const
50
+
51
+ /** Texto legível das respostas pro modelo consumir como resultado do tool. */
52
+ export function formatAnswers(questions: AskQuestion[], answers: AskAnswer[]): string {
53
+ return questions
54
+ .map((q, i) => {
55
+ const a = answers[i]
56
+ const label = q.header !== undefined && q.header.length > 0 ? q.header : q.question
57
+ const picked = [
58
+ ...(a?.selected ?? []),
59
+ ...(a?.text !== undefined && a.text.trim().length > 0 ? [a.text.trim()] : []),
60
+ ]
61
+ return `${label}: ${picked.length > 0 ? picked.join(', ') : '(sem resposta)'}`
62
+ })
63
+ .join('\n')
64
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * @softize/opus/ai/anthropic — driver Anthropic (EXPERIMENTAL).
3
+ *
4
+ * O consumidor pode construir o client (`new Anthropic(...)`) e injetá-lo — em
5
+ * teste, um fake sem rede. Sem client injetado, o driver constrói um sob demanda
6
+ * (peer OPCIONAL `@anthropic-ai/sdk`; `ANTHROPIC_API_KEY` do ambiente vale).
7
+ *
8
+ * `extract` usa tool forçado (`tool_choice: {type: 'tool'}` garante exatamente um
9
+ * bloco `tool_use`) com o JSON Schema derivado do schema zod, e VALIDA a resposta
10
+ * pelo próprio `Schema` do protocolo antes de devolver — o contrato da action é o
11
+ * contrato do modelo. Upgrade futuro: `output_config.format` (SDK ≥0.35, sampling
12
+ * restrito por gramática) quando valer alargar o subset injetável do client.
13
+ */
14
+
15
+ import { zodToJsonSchema } from 'zod-to-json-schema'
16
+ import type {
17
+ AiAdapter,
18
+ AiCompleteOptions,
19
+ AiMessage,
20
+ AiRunOptions,
21
+ AiRunResult,
22
+ AskQuestion,
23
+ ChatEvent,
24
+ Schema,
25
+ } from '../../core/index.ts'
26
+ import { ASK_INPUT_SCHEMA, ASK_TOOL_DESCRIPTION, ASK_USER_TOOL, formatAnswers } from '../ask.ts'
27
+
28
+ /** Injeta a tool reservada `ask_user` no catálogo quando há `onAsk` (round-trip de perguntar
29
+ * à pessoa) — assim o consumidor só precisa passar o handler; a tool aparece pro modelo. */
30
+ function withAskTool(
31
+ tools: Array<{ name: string; description: string; input_schema: unknown }>,
32
+ opts: AiRunOptions,
33
+ ): Array<{ name: string; description: string; input_schema: unknown }> {
34
+ if (opts.onAsk === undefined || tools.some((t) => t.name === ASK_USER_TOOL)) return tools
35
+ return [...tools, { name: ASK_USER_TOOL, description: ASK_TOOL_DESCRIPTION, input_schema: ASK_INPUT_SCHEMA }]
36
+ }
37
+
38
+ /** Executa uma tool escolhida pelo modelo. A `ask_user` é INTERCEPTADA: com `onAsk`, faz o
39
+ * round-trip real; sem ele, recusa com erro acionável (nunca dead-end silencioso). O resto
40
+ * cai no `execute` do consumidor. */
41
+ async function callTool(name: string, input: unknown, opts: AiRunOptions): Promise<unknown> {
42
+ if (name === ASK_USER_TOOL) {
43
+ const questions = ((input as { questions?: AskQuestion[] })?.questions ?? []) as AskQuestion[]
44
+ if (opts.onAsk !== undefined) {
45
+ const answers = await opts.onAsk(questions)
46
+ return { text: formatAnswers(questions, answers) }
47
+ }
48
+ return {
49
+ error:
50
+ 'Não há canal interativo para perguntar à pessoa aqui. Faça a pergunta no seu texto de ' +
51
+ 'resposta e siga com o melhor palpite — não trave nem invente que perguntou.',
52
+ }
53
+ }
54
+ return opts.execute(name, input)
55
+ }
56
+
57
+ /** O subset do client Anthropic que o driver usa (injetável em teste). O `stream` é
58
+ * opcional: com ele, `runStream` emite texto token a token; sem ele (ex.: fake de
59
+ * teste só com `create`), o texto de cada passo sai como um delta único. */
60
+ export interface AnthropicClientLike {
61
+ messages: {
62
+ create(params: Record<string, unknown>): Promise<unknown>
63
+ stream?(params: Record<string, unknown>): AsyncIterable<unknown> & { finalMessage(): Promise<unknown> }
64
+ }
65
+ }
66
+
67
+ export interface AnthropicAiOptions {
68
+ /** Client pronto (injetável em teste). Default: `new Anthropic()` do peer, sob demanda. */
69
+ client?: AnthropicClientLike
70
+ /** API key pro client default. Sem ela, o SDK lê `ANTHROPIC_API_KEY` do ambiente. */
71
+ apiKey?: string
72
+ /** Modelo default das chamadas (por chamada via opts). Default: haiku (rápido/barato). */
73
+ model?: string
74
+ /** Teto default de tokens de saída (default 1024). */
75
+ maxTokens?: number
76
+ /** Nome do adapter (default 'anthropic'). */
77
+ name?: string
78
+ /**
79
+ * Conversor schema → JSON Schema do tool (injetável). Default: `zod-to-json-schema`
80
+ * inline (sem $ref) — cobre os schemas zod que as actions da casa usam.
81
+ */
82
+ toJsonSchema?: (schema: unknown) => unknown
83
+ }
84
+
85
+ export const DEFAULT_ANTHROPIC_MODEL = 'claude-haiku-4-5-20251001'
86
+
87
+ // Import sob demanda do SDK (peer opcional): cacheado após o primeiro uso.
88
+ let sdkPromise: Promise<typeof import('@anthropic-ai/sdk')> | null = null
89
+ function sdk(): Promise<typeof import('@anthropic-ai/sdk')> {
90
+ sdkPromise ??= import('@anthropic-ai/sdk').catch(() => {
91
+ sdkPromise = null
92
+ throw new Error('ai/anthropic precisa do peer @anthropic-ai/sdk — `pnpm add @anthropic-ai/sdk`.')
93
+ })
94
+ return sdkPromise
95
+ }
96
+
97
+ function defaultToJsonSchema(schema: unknown): unknown {
98
+ return zodToJsonSchema(schema as Parameters<typeof zodToJsonSchema>[0], { $refStrategy: 'none' })
99
+ }
100
+
101
+ interface ContentBlock {
102
+ type: string
103
+ text?: string
104
+ input?: unknown
105
+ id?: string
106
+ name?: string
107
+ }
108
+
109
+ export function anthropicAi(options: AnthropicAiOptions = {}): AiAdapter {
110
+ const model = options.model ?? DEFAULT_ANTHROPIC_MODEL
111
+ const maxTokens = options.maxTokens ?? 1024
112
+ const toJsonSchema = options.toJsonSchema ?? defaultToJsonSchema
113
+
114
+ let clientPromise: Promise<AnthropicClientLike> | null = null
115
+ function client(): Promise<AnthropicClientLike> {
116
+ if (options.client !== undefined) return Promise.resolve(options.client)
117
+ // O create real é overloaded (streaming/non-streaming); o subset injetável cobre o driver.
118
+ clientPromise ??= sdk().then(
119
+ ({ default: Anthropic }) =>
120
+ new Anthropic(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}) as never,
121
+ )
122
+ return clientPromise
123
+ }
124
+
125
+ function baseParams(opts?: AiCompleteOptions): Record<string, unknown> {
126
+ return {
127
+ model: opts?.model ?? model,
128
+ max_tokens: opts?.maxTokens ?? maxTokens,
129
+ ...(opts?.system !== undefined ? { system: opts.system } : {}),
130
+ ...(opts?.temperature !== undefined ? { temperature: opts.temperature } : {}),
131
+ }
132
+ }
133
+
134
+ return {
135
+ name: options.name ?? 'anthropic',
136
+ kind: 'ai',
137
+
138
+ async complete(prompt: string, opts?: AiCompleteOptions): Promise<string> {
139
+ const c = await client()
140
+ const response = (await c.messages.create({
141
+ ...baseParams(opts),
142
+ messages: [{ role: 'user', content: prompt }],
143
+ })) as { content: ContentBlock[] }
144
+ return response.content
145
+ .filter((b) => b.type === 'text')
146
+ .map((b) => b.text ?? '')
147
+ .join('')
148
+ },
149
+
150
+ async extract<T>(prompt: string, schema: Schema<T>, opts?: AiCompleteOptions): Promise<T> {
151
+ const inputSchema = toJsonSchema(schema)
152
+ // A API de tools exige objeto na raiz — schema de action já é (z.object).
153
+ if ((inputSchema as { type?: string }).type !== 'object') {
154
+ throw new Error('ai/anthropic extract: o schema precisa ser um objeto na raiz (ex.: z.object).')
155
+ }
156
+ const c = await client()
157
+ const response = (await c.messages.create({
158
+ ...baseParams(opts),
159
+ tools: [
160
+ {
161
+ name: 'extract',
162
+ description: 'Devolve o dado extraído, exatamente no formato pedido.',
163
+ input_schema: inputSchema,
164
+ },
165
+ ],
166
+ tool_choice: { type: 'tool', name: 'extract' },
167
+ messages: [{ role: 'user', content: prompt }],
168
+ })) as { content: ContentBlock[] }
169
+
170
+ const toolUse = response.content.find((b) => b.type === 'tool_use')
171
+ if (toolUse === undefined) {
172
+ throw new Error('ai/anthropic extract: resposta sem bloco tool_use (inesperado com tool forçado).')
173
+ }
174
+ const result = await Promise.resolve(schema['~standard'].validate(toolUse.input))
175
+ if ('issues' in result && result.issues !== undefined) {
176
+ const detail = result.issues.map((i) => `${(i.path ?? []).join('.')}: ${i.message}`).join('; ')
177
+ throw new Error(`ai/anthropic extract: resposta do modelo reprovou no schema — ${detail}`)
178
+ }
179
+ return (result as { value: T }).value
180
+ },
181
+
182
+ async run(input: string | AiMessage[], opts: AiRunOptions): Promise<AiRunResult> {
183
+ const c = await client()
184
+ const tools = withAskTool(
185
+ opts.tools.map((t) => ({
186
+ name: t.name,
187
+ description: t.description,
188
+ input_schema: toJsonSchema(t.inputSchema),
189
+ })),
190
+ opts,
191
+ )
192
+ // Mensagens no formato Anthropic — crescem com tool_use/tool_result a cada passo.
193
+ const messages: Array<{ role: string; content: unknown }> =
194
+ typeof input === 'string'
195
+ ? [{ role: 'user', content: input }]
196
+ : input.map((m) => ({ role: m.role, content: m.content }))
197
+
198
+ const maxSteps = opts.maxSteps ?? 8
199
+ const calls: Array<{ name: string; input: unknown }> = []
200
+
201
+ for (let steps = 1; steps <= maxSteps; steps++) {
202
+ const response = (await c.messages.create({
203
+ ...baseParams(opts),
204
+ tools,
205
+ messages,
206
+ })) as { content: ContentBlock[] }
207
+ messages.push({ role: 'assistant', content: response.content })
208
+
209
+ const toolUses = response.content.filter((b) => b.type === 'tool_use')
210
+ if (toolUses.length === 0) {
211
+ const text = response.content
212
+ .filter((b) => b.type === 'text')
213
+ .map((b) => b.text ?? '')
214
+ .join('')
215
+ return { text, steps, calls }
216
+ }
217
+
218
+ // Executa cada tool escolhida e devolve o resultado (ou o erro) pro modelo.
219
+ const results = []
220
+ for (const tu of toolUses) {
221
+ calls.push({ name: tu.name ?? '', input: tu.input })
222
+ let out: unknown
223
+ try {
224
+ out = await callTool(tu.name ?? '', tu.input, opts)
225
+ } catch (err) {
226
+ out = { error: err instanceof Error ? err.message : String(err) }
227
+ }
228
+ results.push({
229
+ type: 'tool_result',
230
+ tool_use_id: tu.id ?? '',
231
+ content: JSON.stringify(out ?? null),
232
+ })
233
+ }
234
+ messages.push({ role: 'user', content: results })
235
+ }
236
+
237
+ // Bateu o teto de passos sem uma resposta final em texto.
238
+ return { text: 'Não consegui concluir dentro do limite de passos.', steps: maxSteps, calls }
239
+ },
240
+
241
+ async *runStream(input: string | AiMessage[], opts: AiRunOptions): AsyncIterable<ChatEvent> {
242
+ const c = await client()
243
+ const tools = withAskTool(
244
+ opts.tools.map((t) => ({
245
+ name: t.name,
246
+ description: t.description,
247
+ input_schema: toJsonSchema(t.inputSchema),
248
+ })),
249
+ opts,
250
+ )
251
+ const messages: Array<{ role: string; content: unknown }> =
252
+ typeof input === 'string'
253
+ ? [{ role: 'user', content: input }]
254
+ : input.map((m) => ({ role: m.role, content: m.content }))
255
+
256
+ const maxSteps = opts.maxSteps ?? 8
257
+
258
+ for (let steps = 1; steps <= maxSteps; steps++) {
259
+ const params = { ...baseParams(opts), tools, messages }
260
+ let content: ContentBlock[]
261
+ if (typeof c.messages.stream === 'function') {
262
+ // Streaming de verdade: os deltas de texto saem conforme o modelo gera.
263
+ const stream = c.messages.stream(params)
264
+ for await (const event of stream) {
265
+ const e = event as { type?: string; delta?: { type?: string; text?: string } }
266
+ if (e.type === 'content_block_delta' && e.delta?.type === 'text_delta' && e.delta.text) {
267
+ yield { type: 'text', delta: e.delta.text }
268
+ }
269
+ }
270
+ content = ((await stream.finalMessage()) as { content: ContentBlock[] }).content
271
+ } else {
272
+ // Fallback sem stream (ex.: fake de teste): o texto do passo vira um delta único.
273
+ content = ((await c.messages.create(params)) as { content: ContentBlock[] }).content
274
+ const text = content
275
+ .filter((b) => b.type === 'text')
276
+ .map((b) => b.text ?? '')
277
+ .join('')
278
+ if (text !== '') yield { type: 'text', delta: text }
279
+ }
280
+ messages.push({ role: 'assistant', content })
281
+
282
+ const toolUses = content.filter((b) => b.type === 'tool_use')
283
+ if (toolUses.length === 0) {
284
+ yield { type: 'done', ok: true }
285
+ return
286
+ }
287
+
288
+ const results = []
289
+ for (const tu of toolUses) {
290
+ yield { type: 'tool', name: tu.name ?? '' }
291
+ let out: unknown
292
+ try {
293
+ out = await callTool(tu.name ?? '', tu.input, opts)
294
+ } catch (err) {
295
+ out = { error: err instanceof Error ? err.message : String(err) }
296
+ }
297
+ results.push({
298
+ type: 'tool_result',
299
+ tool_use_id: tu.id ?? '',
300
+ content: JSON.stringify(out ?? null),
301
+ })
302
+ }
303
+ messages.push({ role: 'user', content: results })
304
+ }
305
+
306
+ yield { type: 'done', ok: false, error: 'Não consegui concluir dentro do limite de passos.' }
307
+ },
308
+ }
309
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @softize/opus/ai — a capability de IA generativa (EXPERIMENTAL).
3
+ *
4
+ * O contrato (`AiAdapter`) vive no core: `complete` (texto → texto) e `extract`
5
+ * (texto → objeto VALIDADO por um `Schema` do protocolo — o mesmo tipo de schema
6
+ * das actions vira o contrato da resposta do modelo). Driver em `ai/anthropic`.
7
+ * A superfície é a mínima de backoffice e cresce por reincidência — não por
8
+ * especulação (streaming, chat multi-turn e tools livres entram quando um caso
9
+ * real cobrar).
10
+ */
11
+
12
+ export type { AiAdapter, AiCompleteOptions } from '../core/index.ts'
13
+ // Elicitação — perguntar à pessoa no meio do turno (round-trip via `AiRunOptions.onAsk`).
14
+ // O contrato (tipos) mora no core; aqui saem o nome reservado, o schema/descrição da tool e
15
+ // o formatador. É o ponto de consistência pra qualquer consumidor (chat, copilot, MCP externo).
16
+ export type { AskOption, AskQuestion, AskAnswer } from '../core/index.ts'
17
+ export { ASK_USER_TOOL, ASK_TOOL_DESCRIPTION, ASK_INPUT_SCHEMA, formatAnswers } from './ask.ts'
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @softize/opus/audit/console — Console sink driver
3
+ *
4
+ * Imprime `AuditRecord` no stdout/stderr. Útil em dev pra inspeção
5
+ * imediata e em testes; em produção, prefira sinks persistentes (/pg,
6
+ * sentry, datadog).
7
+ *
8
+ * Formatos:
9
+ * - 'pretty' — multi-linha com cores ANSI (dev default)
10
+ * - 'json' — single-line JSON (prod default; parseável)
11
+ *
12
+ * Default: 'pretty' se TTY, 'json' caso contrário.
13
+ */
14
+
15
+ import type { AuditRecord, AuditSink } from '../../core/index.ts'
16
+ import { redactRecord } from '../index.ts'
17
+
18
+ export interface ConsoleAuditOptions {
19
+ /**
20
+ * Formato de output. Default: 'pretty' se process.stdout.isTTY, senão 'json'.
21
+ */
22
+ format?: 'pretty' | 'json'
23
+
24
+ /**
25
+ * Override do logger usado pra escrever. Default: console.log / console.error.
26
+ * Útil pra capturar em testes ou redirecionar.
27
+ */
28
+ write?: (level: 'info' | 'error', line: string) => void
29
+
30
+ /**
31
+ * Nome do sink (default 'console'). Útil quando registrando múltiplos
32
+ * sinks console pra contextos diferentes.
33
+ */
34
+ name?: string
35
+
36
+ /**
37
+ * Redator aplicado a input/output antes de imprimir (ex.: `redactDeep` de
38
+ * `@softize/opus/audit`) — terminal/log agregado também vaza senha.
39
+ */
40
+ redact?: (value: unknown) => unknown
41
+ }
42
+
43
+ export function consoleAudit(options: ConsoleAuditOptions = {}): AuditSink {
44
+ const {
45
+ format = defaultFormat(),
46
+ write = defaultWrite,
47
+ name = 'console',
48
+ redact,
49
+ } = options
50
+
51
+ return {
52
+ name,
53
+ kind: 'audit',
54
+ emit(record: AuditRecord) {
55
+ const r = redact === undefined ? record : redactRecord(record, redact)
56
+ const line = format === 'pretty' ? renderPretty(r) : renderJson(r)
57
+ const level = r.outcome === 'error' ? 'error' : 'info'
58
+ write(level, line)
59
+ },
60
+ healthCheck() {
61
+ return Promise.resolve({ ok: true })
62
+ },
63
+ }
64
+ }
65
+
66
+ // =============================================================================
67
+ // Format helpers
68
+ // =============================================================================
69
+
70
+ function defaultFormat(): 'pretty' | 'json' {
71
+ /* v8 ignore next — ambiente Node sem stdout.isTTY (raro) */
72
+ if (typeof process === 'undefined' || process.stdout === undefined) return 'json'
73
+ return process.stdout.isTTY === true ? 'pretty' : 'json'
74
+ }
75
+
76
+ function defaultWrite(level: 'info' | 'error', line: string): void {
77
+ const target = level === 'error' ? console.error : console.log
78
+ target(line)
79
+ }
80
+
81
+ function renderJson(record: AuditRecord): string {
82
+ return JSON.stringify(record)
83
+ }
84
+
85
+ function renderPretty(record: AuditRecord): string {
86
+ const tag = record.outcome === 'success' ? ANSI.green('✓') : ANSI.red('✗')
87
+ const sev = paintSeverity(record.severity)
88
+ const head = `${tag} ${ANSI.bold(record.action)} ${ANSI.dim(record.id)}`
89
+ const meta = [
90
+ `${ANSI.dim('outcome:')} ${record.outcome}`,
91
+ `${ANSI.dim('severity:')} ${sev}`,
92
+ `${ANSI.dim('duration:')} ${record.durationMs.toFixed(2)}ms`,
93
+ `${ANSI.dim('actor:')} ${record.actor.id ?? 'system'}`,
94
+ ...(record.tenant !== null && record.tenant !== undefined
95
+ ? [`${ANSI.dim('tenant:')} ${record.tenant}`]
96
+ : []),
97
+ ].join(' ')
98
+ return `${head}\n ${meta}`
99
+ }
100
+
101
+ function paintSeverity(s: AuditRecord['severity']): string {
102
+ if (s === 'error') return ANSI.red(s)
103
+ if (s === 'warning') return ANSI.yellow(s)
104
+ return ANSI.green(s)
105
+ }
106
+
107
+ // =============================================================================
108
+ // ANSI helpers (zero-dep)
109
+ // =============================================================================
110
+
111
+ const ANSI = {
112
+ bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
113
+ dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
114
+ red: (s: string) => `\x1b[31m${s}\x1b[0m`,
115
+ green: (s: string) => `\x1b[32m${s}\x1b[0m`,
116
+ yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
117
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @softize/opus/audit/pg — PostgreSQL sink driver
3
+ *
4
+ * Persiste `AuditRecord` numa tabela Postgres via `pg.Pool`. Schema esperado:
5
+ *
6
+ * CREATE TABLE audit_log (
7
+ * id uuid PRIMARY KEY,
8
+ * timestamp timestamptz NOT NULL,
9
+ * action text NOT NULL,
10
+ * outcome text NOT NULL,
11
+ * duration_ms numeric NOT NULL,
12
+ * provenance_kind text NOT NULL, -- discriminator (http, schedule, reaction, ...)
13
+ * provenance jsonb NOT NULL, -- objeto completo da Provenance (incl. chain)
14
+ * actor_id text,
15
+ * actor_type text,
16
+ * tenant text,
17
+ * input jsonb,
18
+ * output jsonb,
19
+ * error jsonb,
20
+ * severity text NOT NULL,
21
+ * trace_request_id text,
22
+ * trace_parent_action_id text,
23
+ * meta jsonb
24
+ * );
25
+ * CREATE INDEX audit_provenance_kind_idx ON audit_log(provenance_kind);
26
+ *
27
+ * Tabela é criada pelo consumer (migration). Adapter apenas insere.
28
+ *
29
+ * Uso:
30
+ * import { Pool } from 'pg'
31
+ * import { pgAudit } from '@softize/opus/audit/pg'
32
+ *
33
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL })
34
+ * createRuntime({ audit: pgAudit({ pool, table: 'audit_log' }) })
35
+ *
36
+ * Com dado sensível, plugue o redator global (recursivo, por nome de chave):
37
+ *
38
+ * import { redactDeep } from '@softize/opus/audit'
39
+ * pgAudit({ pool, redact: redactDeep })
40
+ */
41
+
42
+ import type { AuditRecord, AuditSink, HealthStatus } from '../../core/index.ts'
43
+ import { redactRecord } from '../index.ts'
44
+
45
+ /**
46
+ * Subset mínimo de `pg.Pool` que precisamos. Aceitar qualquer objeto com
47
+ * `query()` evita dep direta de `pg` no tipo público; aceita pools-like
48
+ * (e.g., wrappers de Neon, Supabase pooler).
49
+ */
50
+ export interface PgQueryable {
51
+ query(text: string, values?: unknown[]): Promise<unknown>
52
+ }
53
+
54
+ export interface PgAuditOptions {
55
+ /** Pool ou conexão pg-compatível. */
56
+ pool: PgQueryable
57
+
58
+ /** Nome da tabela. Default 'audit_log'. */
59
+ table?: string
60
+
61
+ /** Nome do sink (default 'pg'). */
62
+ name?: string
63
+
64
+ /**
65
+ * Redator aplicado a input/output ANTES de persistir — sem ele, os dois vão
66
+ * CRUS (user.create/setPassword gravaria senha em claro no log). Pra dado
67
+ * sensível, plugue o `redactDeep` de `@softize/opus/audit` (recursivo, por
68
+ * nome de chave) ou um redator próprio. O `AuditConfig.redact` por action
69
+ * (dot-paths) continua valendo — este é o cinto de segurança global do sink.
70
+ */
71
+ redact?: (value: unknown) => unknown
72
+ }
73
+
74
+ export function pgAudit(options: PgAuditOptions): AuditSink {
75
+ const { pool, table = 'audit_log', name = 'pg', redact } = options
76
+ validateTableName(table)
77
+ const sql = buildInsertSql(table)
78
+
79
+ return {
80
+ name,
81
+ kind: 'audit',
82
+
83
+ async emit(record: AuditRecord) {
84
+ const values = recordToValues(redact === undefined ? record : redactRecord(record, redact))
85
+ await pool.query(sql, values)
86
+ },
87
+
88
+ async healthCheck(): Promise<HealthStatus> {
89
+ try {
90
+ await pool.query('SELECT 1')
91
+ return { ok: true }
92
+ } catch (err) {
93
+ return { ok: false, details: { error: String(err) } }
94
+ }
95
+ },
96
+ }
97
+ }
98
+
99
+ // =============================================================================
100
+ // SQL builder
101
+ // =============================================================================
102
+
103
+ const COLUMNS = [
104
+ 'id',
105
+ 'timestamp',
106
+ 'action',
107
+ 'outcome',
108
+ 'duration_ms',
109
+ 'provenance_kind',
110
+ 'provenance',
111
+ 'actor_id',
112
+ 'actor_type',
113
+ 'tenant',
114
+ 'input',
115
+ 'output',
116
+ 'error',
117
+ 'severity',
118
+ 'trace_request_id',
119
+ 'trace_parent_action_id',
120
+ 'meta',
121
+ ] as const
122
+
123
+ function buildInsertSql(table: string): string {
124
+ const cols = COLUMNS.join(', ')
125
+ const params = COLUMNS.map((_, i) => `$${i + 1}`).join(', ')
126
+ return `INSERT INTO ${table} (${cols}) VALUES (${params})`
127
+ }
128
+
129
+ function recordToValues(r: AuditRecord): unknown[] {
130
+ return [
131
+ r.id,
132
+ r.timestamp,
133
+ r.action,
134
+ r.outcome,
135
+ r.durationMs,
136
+ r.provenance.kind,
137
+ jsonOrNull(r.provenance),
138
+ r.actor.id,
139
+ r.actor.type ?? null,
140
+ r.tenant ?? null,
141
+ jsonOrNull(r.input),
142
+ jsonOrNull(r.output),
143
+ jsonOrNull(r.error),
144
+ r.severity,
145
+ r.trace?.requestId ?? null,
146
+ r.trace?.parentActionId ?? null,
147
+ jsonOrNull(r.meta),
148
+ ]
149
+ }
150
+
151
+ function jsonOrNull(value: unknown): string | null {
152
+ if (value === undefined || value === null) return null
153
+ return JSON.stringify(value)
154
+ }
155
+
156
+ // =============================================================================
157
+ // Safety
158
+ // =============================================================================
159
+
160
+ /**
161
+ * Valida que `table` é um identificador SQL seguro. Aceita
162
+ * `schema.table` opcional. Lança se conter caracteres suspeitos —
163
+ * preveni injection já que table name não pode ser parametrizado.
164
+ */
165
+ function validateTableName(table: string): void {
166
+ if (!/^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)?$/i.test(table)) {
167
+ throw new Error(
168
+ `pgAudit: invalid table name "${table}". ` +
169
+ `Must match /^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*)?$/i.`,
170
+ )
171
+ }
172
+ }