crewly 1.11.5 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/config/skills/agent/onboarding/synthesize-hierarchy/SKILL.md +65 -0
  2. package/config/skills/agent/onboarding/synthesize-hierarchy/execute.sh +61 -0
  3. package/config/skills/agent/web-search/SKILL.md +70 -0
  4. package/config/skills/agent/web-search/execute.sh +170 -0
  5. package/config/skills/agent/web-search/skill.json +23 -0
  6. package/dist/backend/backend/src/constants.d.ts +34 -1
  7. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  8. package/dist/backend/backend/src/constants.js +34 -1
  9. package/dist/backend/backend/src/constants.js.map +1 -1
  10. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +22 -0
  11. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
  12. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +58 -0
  13. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
  14. package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
  15. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +3 -1
  16. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts +27 -0
  18. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts.map +1 -1
  19. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js +108 -0
  20. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts +6 -2
  22. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts.map +1 -1
  23. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js +9 -3
  24. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js.map +1 -1
  25. package/dist/backend/backend/src/index.d.ts.map +1 -1
  26. package/dist/backend/backend/src/index.js +36 -2
  27. package/dist/backend/backend/src/index.js.map +1 -1
  28. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +18 -0
  29. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
  30. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +24 -2
  31. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
  32. package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts +90 -0
  33. package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
  34. package/dist/backend/backend/src/services/backup/backup-archive.service.js +309 -0
  35. package/dist/backend/backend/src/services/backup/backup-archive.service.js.map +1 -0
  36. package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
  37. package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
  38. package/dist/backend/backend/src/services/backup/backup-cloud.client.js +134 -0
  39. package/dist/backend/backend/src/services/backup/backup-cloud.client.js.map +1 -0
  40. package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts +78 -0
  41. package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
  42. package/dist/backend/backend/src/services/backup/backup-restore.service.js +358 -0
  43. package/dist/backend/backend/src/services/backup/backup-restore.service.js.map +1 -0
  44. package/dist/backend/backend/src/services/backup/backup.types.d.ts +163 -0
  45. package/dist/backend/backend/src/services/backup/backup.types.d.ts.map +1 -0
  46. package/dist/backend/backend/src/services/backup/backup.types.js +13 -0
  47. package/dist/backend/backend/src/services/backup/backup.types.js.map +1 -0
  48. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +29 -2
  49. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
  50. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +97 -13
  51. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
  52. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts +102 -0
  53. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -0
  54. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +164 -0
  55. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -0
  56. package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts +21 -0
  57. package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts.map +1 -1
  58. package/dist/backend/backend/src/services/fission/fission-guard.service.js +30 -0
  59. package/dist/backend/backend/src/services/fission/fission-guard.service.js.map +1 -1
  60. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts +4 -0
  61. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -1
  62. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js +8 -0
  63. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -1
  64. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts +79 -58
  65. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
  66. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +140 -65
  67. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
  68. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts +117 -0
  69. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts.map +1 -0
  70. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js +189 -0
  71. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js.map +1 -0
  72. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.d.ts.map +1 -1
  73. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js +1 -0
  74. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js.map +1 -1
  75. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.d.ts.map +1 -1
  76. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js +2 -0
  77. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js.map +1 -1
  78. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js +17 -1
  80. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js.map +1 -1
  81. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +50 -0
  82. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +71 -0
  84. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
  85. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +18 -0
  86. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +75 -1
  88. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  89. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +115 -0
  90. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
  91. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +189 -3
  92. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  93. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +28 -0
  94. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
  95. package/dist/backend/backend/src/services/session/pty/pty-session.js +61 -1
  96. package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
  97. package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
  98. package/dist/backend/backend/src/services/template/template.service.js +67 -2
  99. package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
  100. package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts +19 -1
  101. package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
  102. package/dist/backend/backend/src/services/v3/cascade-request-status.js +39 -2
  103. package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
  104. package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts +41 -0
  105. package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
  106. package/dist/backend/backend/src/services/v3/escalation-router.service.js +169 -0
  107. package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
  108. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts +4 -1
  109. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts.map +1 -1
  110. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js +21 -0
  111. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js.map +1 -1
  112. package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
  113. package/dist/backend/backend/src/types/intent-task.types.js +8 -0
  114. package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
  115. package/dist/backend/backend/src/types/v2/request.types.d.ts +1 -1
  116. package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
  117. package/dist/backend/backend/src/types/v2/request.types.js +1 -0
  118. package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
  119. package/dist/cli/backend/src/constants.d.ts +34 -1
  120. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  121. package/dist/cli/backend/src/constants.js +34 -1
  122. package/dist/cli/backend/src/constants.js.map +1 -1
  123. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts +70 -0
  124. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts.map +1 -0
  125. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js +427 -0
  126. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js.map +1 -0
  127. package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts +90 -0
  128. package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
  129. package/dist/cli/backend/src/services/backup/backup-archive.service.js +309 -0
  130. package/dist/cli/backend/src/services/backup/backup-archive.service.js.map +1 -0
  131. package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
  132. package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
  133. package/dist/cli/backend/src/services/backup/backup-cloud.client.js +134 -0
  134. package/dist/cli/backend/src/services/backup/backup-cloud.client.js.map +1 -0
  135. package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts +78 -0
  136. package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
  137. package/dist/cli/backend/src/services/backup/backup-restore.service.js +358 -0
  138. package/dist/cli/backend/src/services/backup/backup-restore.service.js.map +1 -0
  139. package/dist/cli/backend/src/services/backup/backup.types.d.ts +163 -0
  140. package/dist/cli/backend/src/services/backup/backup.types.d.ts.map +1 -0
  141. package/dist/cli/backend/src/services/backup/backup.types.js +13 -0
  142. package/dist/cli/backend/src/services/backup/backup.types.js.map +1 -0
  143. package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts +410 -0
  144. package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -0
  145. package/dist/cli/backend/src/services/cloud/cloud-client.service.js +863 -0
  146. package/dist/cli/backend/src/services/cloud/cloud-client.service.js.map +1 -0
  147. package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts +292 -0
  148. package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -0
  149. package/dist/cli/backend/src/services/cloud/cloud-sync.service.js +1093 -0
  150. package/dist/cli/backend/src/services/cloud/cloud-sync.service.js.map +1 -0
  151. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +328 -0
  152. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -0
  153. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js +171 -0
  154. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -0
  155. package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts +89 -0
  156. package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts.map +1 -0
  157. package/dist/cli/backend/src/services/cloud/device-identity.service.js +148 -0
  158. package/dist/cli/backend/src/services/cloud/device-identity.service.js.map +1 -0
  159. package/dist/cli/backend/src/services/user/user-identity.service.d.ts +86 -0
  160. package/dist/cli/backend/src/services/user/user-identity.service.d.ts.map +1 -0
  161. package/dist/cli/backend/src/services/user/user-identity.service.js +190 -0
  162. package/dist/cli/backend/src/services/user/user-identity.service.js.map +1 -0
  163. package/dist/cli/cli/src/commands/backup.d.ts +31 -0
  164. package/dist/cli/cli/src/commands/backup.d.ts.map +1 -0
  165. package/dist/cli/cli/src/commands/backup.js +280 -0
  166. package/dist/cli/cli/src/commands/backup.js.map +1 -0
  167. package/dist/cli/cli/src/index.js +10 -0
  168. package/dist/cli/cli/src/index.js.map +1 -1
  169. package/package.json +9 -3
  170. package/packages/crewly-agent/README.md +27 -0
  171. package/packages/crewly-agent/bin/crewly-agent +33 -0
  172. package/packages/crewly-agent/package.json +39 -0
  173. package/packages/crewly-agent/src/cli.ts +168 -0
  174. package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +2355 -0
  175. package/packages/crewly-agent/src/runtime/agent-runner.service.ts +1827 -0
  176. package/packages/crewly-agent/src/runtime/agent-stream.service.test.ts +153 -0
  177. package/packages/crewly-agent/src/runtime/agent-stream.service.ts +225 -0
  178. package/packages/crewly-agent/src/runtime/agent-worker.test.ts +171 -0
  179. package/packages/crewly-agent/src/runtime/agent-worker.ts +193 -0
  180. package/packages/crewly-agent/src/runtime/api-client.ts +143 -0
  181. package/packages/crewly-agent/src/runtime/approval-queue.service.ts +307 -0
  182. package/packages/crewly-agent/src/runtime/audit-log.service.test.ts +208 -0
  183. package/packages/crewly-agent/src/runtime/audit-log.service.ts +332 -0
  184. package/packages/crewly-agent/src/runtime/audit-trail.service.test.ts +178 -0
  185. package/packages/crewly-agent/src/runtime/audit-trail.service.ts +151 -0
  186. package/packages/crewly-agent/src/runtime/auditor-tools.test.ts +274 -0
  187. package/packages/crewly-agent/src/runtime/auditor-tools.ts +311 -0
  188. package/packages/crewly-agent/src/runtime/cloud-config.ts +67 -0
  189. package/packages/crewly-agent/src/runtime/deepseek-sse-transform.test.ts +165 -0
  190. package/packages/crewly-agent/src/runtime/deepseek-sse-transform.ts +168 -0
  191. package/packages/crewly-agent/src/runtime/env-isolation.service.ts +246 -0
  192. package/packages/crewly-agent/src/runtime/in-process-log-buffer.test.ts +280 -0
  193. package/packages/crewly-agent/src/runtime/in-process-log-buffer.ts +317 -0
  194. package/packages/crewly-agent/src/runtime/index.ts +38 -0
  195. package/packages/crewly-agent/src/runtime/mcp-tool-bridge.test.ts +352 -0
  196. package/packages/crewly-agent/src/runtime/mcp-tool-bridge.ts +244 -0
  197. package/packages/crewly-agent/src/runtime/model-manager.test.ts +326 -0
  198. package/packages/crewly-agent/src/runtime/model-manager.ts +363 -0
  199. package/packages/crewly-agent/src/runtime/output-filter.service.ts +175 -0
  200. package/packages/crewly-agent/src/runtime/prompt-guard.service.ts +303 -0
  201. package/packages/crewly-agent/src/runtime/rate-limiter.test.ts +228 -0
  202. package/packages/crewly-agent/src/runtime/rate-limiter.ts +353 -0
  203. package/packages/crewly-agent/src/runtime/tool-registry.test.ts +2510 -0
  204. package/packages/crewly-agent/src/runtime/tool-registry.ts +2104 -0
  205. package/packages/crewly-agent/src/runtime/types.test.ts +519 -0
  206. package/packages/crewly-agent/src/runtime/types.ts +637 -0
  207. package/packages/crewly-agent/src/runtime/web-search.tool.test.ts +131 -0
  208. package/packages/crewly-agent/src/runtime/web-search.tool.ts +140 -0
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Crewly Agent Model Manager
3
+ *
4
+ * Multi-provider model factory that creates AI SDK model instances
5
+ * from configuration. Supports Anthropic, OpenAI, Google, DeepSeek, and
6
+ * Ollama providers.
7
+ *
8
+ * API keys for cloud providers are resolved through the settings service
9
+ * (skill → runtime → global → env var) and injected into process.env so the
10
+ * provider SDKs can pick them up:
11
+ * - ANTHROPIC_API_KEY
12
+ * - OPENAI_API_KEY
13
+ * - GOOGLE_GENERATIVE_AI_API_KEY
14
+ * - DEEPSEEK_API_KEY (DeepSeek; served via OpenAI-compatible API)
15
+ *
16
+ * Ollama runs locally and does not require an API key.
17
+ * Configure the Ollama base URL via OLLAMA_BASE_URL (default: http://localhost:11434).
18
+ *
19
+ * @module services/agent/crewly-agent/model-manager
20
+ */
21
+
22
+ import type { LanguageModel } from 'ai';
23
+ import { type ModelConfig, type ModelProvider, CREWLY_AGENT_DEFAULTS } from './types.js';
24
+ /**
25
+ * Standalone runtime resolves API keys from environment variables only — it
26
+ * does not depend on OSS's settings service. Anyone running this binary on
27
+ * a user machine sets keys via env (.env, shell profile, or the harness that
28
+ * spawns the process), keeping the runtime credential-isolated.
29
+ *
30
+ * OSS-side bridging code can still pre-set env vars before spawning the
31
+ * subprocess, which gives users the same "configure key in UI" experience
32
+ * without coupling the runtime to OSS internals.
33
+ */
34
+ type ApiKeyProvider = 'gemini' | 'anthropic' | 'openai' | 'deepseek';
35
+
36
+ interface SettingsServiceLike {
37
+ getApiKey(provider: ApiKeyProvider, context: { runtime: string }): Promise<string | null>;
38
+ }
39
+
40
+ function getSettingsService(): SettingsServiceLike {
41
+ return {
42
+ getApiKey: async (provider) => {
43
+ switch (provider) {
44
+ case 'gemini':
45
+ return process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? process.env.GEMINI_API_KEY ?? null;
46
+ case 'anthropic':
47
+ return process.env.ANTHROPIC_API_KEY ?? null;
48
+ case 'openai':
49
+ return process.env.OPENAI_API_KEY ?? null;
50
+ case 'deepseek':
51
+ return process.env.DEEPSEEK_API_KEY ?? null;
52
+ default:
53
+ return null;
54
+ }
55
+ },
56
+ };
57
+ }
58
+ import { teeAndParse, type ParsedDeepseekSse } from './deepseek-sse-transform.js';
59
+
60
+ /**
61
+ * Base URL for DeepSeek's OpenAI-compatible chat completions endpoint.
62
+ * DeepSeek implements /chat/completions only — not /responses — so we
63
+ * must route via the `.chat(modelId)` factory of @ai-sdk/openai's
64
+ * createOpenAI() return value. Extracted per CLAUDE.md no-hardcoded-values rule.
65
+ */
66
+ const DEEPSEEK_BASE_URL = 'https://api.deepseek.com/v1';
67
+
68
+ /**
69
+ * Factory for creating AI SDK language model instances from configuration.
70
+ *
71
+ * Uses dynamic imports to avoid loading provider SDKs that aren't needed,
72
+ * keeping the startup cost minimal when only one provider is used.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const manager = new ModelManager();
77
+ * const model = await manager.getModel({ provider: 'anthropic', modelId: 'claude-sonnet-4-20250514' });
78
+ * ```
79
+ */
80
+ export class ModelManager {
81
+ /** Cached provider module references to avoid re-importing */
82
+ private providerCache = new Map<ModelProvider, (modelId: string) => LanguageModel>();
83
+
84
+ /**
85
+ * Per-instance buffer of parsed DeepSeek-R1 reasoning_content from in-flight
86
+ * and recently-completed HTTP calls. Each `streamText` call to a DeepSeek
87
+ * model triggers one or more fetch calls (one per agentic step); each fetch
88
+ * appends its parsed reasoning to this buffer. Consumer (agent-runner) reads
89
+ * via `consumeDeepseekReasoning()` after `await streamResult` and the buffer
90
+ * resets to `''` on read.
91
+ *
92
+ * **Concurrency:** AgentRunner uses one ModelManager per session and calls
93
+ * streamText serially per session (the rate limiter enforces this).
94
+ * Cross-session concurrency is not a concern because each AgentRunner gets
95
+ * its own ModelManager via `new ModelManager()` in the constructor.
96
+ */
97
+ private deepseekReasoningBuffer = '';
98
+
99
+ /** Tracks in-flight parsed-SSE handles so we can wait for drain on consume. */
100
+ private deepseekParsedHandles: ParsedDeepseekSse[] = [];
101
+
102
+ /**
103
+ * Get an AI SDK language model instance for the given configuration.
104
+ *
105
+ * Resolves API keys from settings (with override chain) and injects them
106
+ * into the environment before creating the model, so provider SDKs can find them.
107
+ *
108
+ * @param config - Model configuration specifying provider and model ID
109
+ * @returns AI SDK LanguageModel instance ready for use with generateText
110
+ * @throws Error if the provider is unknown or the SDK is not installed
111
+ */
112
+ async getModel(config: ModelConfig = CREWLY_AGENT_DEFAULTS.DEFAULT_MODEL): Promise<LanguageModel> {
113
+ // Ensure the provider's API key is in the environment from settings
114
+ await this.ensureApiKeyInEnv(config.provider);
115
+
116
+ const providerFn = await this.getProviderFunction(config.provider);
117
+ return providerFn(config.modelId);
118
+ }
119
+
120
+ /**
121
+ * Get provider function, using cache for repeated calls.
122
+ *
123
+ * @param provider - Provider name
124
+ * @returns Function that creates a model from a model ID string
125
+ */
126
+ private async getProviderFunction(provider: ModelProvider): Promise<(modelId: string) => LanguageModel> {
127
+ const cached = this.providerCache.get(provider);
128
+ if (cached) return cached;
129
+
130
+ let providerFn: (modelId: string) => LanguageModel;
131
+
132
+ try {
133
+ switch (provider) {
134
+ case 'anthropic': {
135
+ const { anthropic } = await import('@ai-sdk/anthropic');
136
+ providerFn = (modelId: string) => anthropic(modelId);
137
+ break;
138
+ }
139
+ case 'openai': {
140
+ const { openai } = await import('@ai-sdk/openai');
141
+ providerFn = (modelId: string) => openai(modelId);
142
+ break;
143
+ }
144
+ case 'google': {
145
+ const { google } = await import('@ai-sdk/google');
146
+ providerFn = (modelId: string) => google(modelId);
147
+ break;
148
+ }
149
+ case 'ollama': {
150
+ const { createOllama } = await import('ollama-ai-provider');
151
+ const baseURL = process.env.OLLAMA_BASE_URL || CREWLY_AGENT_DEFAULTS.OLLAMA_BASE_URL;
152
+ const ollamaProvider = createOllama({ baseURL });
153
+ // ollama-ai-provider exports LanguageModelV1 which is compatible but
154
+ // doesn't extend the newer LanguageModel union — safe to cast.
155
+ providerFn = (modelId: string) => ollamaProvider(modelId) as unknown as LanguageModel;
156
+ break;
157
+ }
158
+ case 'deepseek': {
159
+ // DeepSeek API is OpenAI-compatible — reuse the OpenAI SDK with a custom baseURL.
160
+ // IMPORTANT: must call deepseekProvider.chat(modelId), NOT deepseekProvider(modelId).
161
+ // The bare function-call form on @ai-sdk/openai >=3.x routes to /responses, which
162
+ // DeepSeek does not implement — it only exposes /chat/completions. The .chat factory
163
+ // forces the chat-completions path. See PR #400 review for full trace.
164
+ //
165
+ // **I2 — reasoning_content extraction:**
166
+ // Pass a custom `fetch` that tees the SSE response body. One branch flows
167
+ // through to AI SDK unmodified (zero impact on existing behavior); the other
168
+ // branch is parsed for `delta.reasoning_content` text and accumulated into
169
+ // `this.deepseekReasoningBuffer`, which agent-runner consumes after
170
+ // streamResult drains. See deepseek-sse-transform.ts for the parser.
171
+ const { createOpenAI } = await import('@ai-sdk/openai');
172
+ const customFetch = this.makeDeepseekFetch();
173
+ const deepseekProvider = createOpenAI({
174
+ baseURL: DEEPSEEK_BASE_URL,
175
+ apiKey: process.env.DEEPSEEK_API_KEY,
176
+ fetch: customFetch as unknown as typeof globalThis.fetch,
177
+ });
178
+ providerFn = (modelId: string) => deepseekProvider.chat(modelId);
179
+ break;
180
+ }
181
+ default:
182
+ throw new Error(`Unknown model provider: ${provider}`);
183
+ }
184
+ } catch (error) {
185
+ if (error instanceof Error && error.message.startsWith('Unknown model provider:')) {
186
+ throw error;
187
+ }
188
+ throw new Error(
189
+ `Failed to load provider SDK for '${provider}'. Is the package installed? ` +
190
+ `Original error: ${error instanceof Error ? error.message : String(error)}`
191
+ );
192
+ }
193
+
194
+ this.providerCache.set(provider, providerFn);
195
+ return providerFn;
196
+ }
197
+
198
+ /**
199
+ * Check which providers have API keys configured (settings + env vars).
200
+ *
201
+ * @returns Object indicating which providers are available
202
+ */
203
+ async getAvailableProviders(): Promise<Record<ModelProvider, boolean>> {
204
+ const settingsService = getSettingsService();
205
+ const context = { runtime: 'crewly-agent' };
206
+
207
+ const [geminiKey, anthropicKey, openaiKey, deepseekKey] = await Promise.all([
208
+ settingsService.getApiKey('gemini', context),
209
+ settingsService.getApiKey('anthropic', context),
210
+ settingsService.getApiKey('openai', context),
211
+ settingsService.getApiKey('deepseek', context),
212
+ ]);
213
+
214
+ return {
215
+ anthropic: !!anthropicKey,
216
+ openai: !!openaiKey,
217
+ google: !!geminiKey,
218
+ ollama: true, // Ollama runs locally, always "available" if installed
219
+ deepseek: !!deepseekKey,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Map model provider name to API key provider name.
225
+ * Only applicable for cloud providers wired through the settings service
226
+ * (anthropic, openai, google, deepseek). Ollama runs locally and is
227
+ * excluded.
228
+ *
229
+ * @param provider - Cloud model provider
230
+ * @returns Corresponding ApiKeyProvider name
231
+ */
232
+ private static providerToApiKeyProvider(provider: Exclude<ModelProvider, 'ollama'>): ApiKeyProvider {
233
+ return provider === 'google' ? 'gemini' : provider;
234
+ }
235
+
236
+ /**
237
+ * Ensure the API key for a provider is available in process.env
238
+ * by resolving it from settings if not already present.
239
+ *
240
+ * No-op for 'ollama' (runs locally, no key required). All other providers —
241
+ * including 'deepseek' — flow through the settings service so users can
242
+ * configure them from the UI without touching env vars.
243
+ *
244
+ * @param provider - The model provider
245
+ */
246
+ private async ensureApiKeyInEnv(provider: ModelProvider): Promise<void> {
247
+ if (provider === 'ollama') return; // Ollama is local, no API key needed
248
+ const apiKeyProvider = ModelManager.providerToApiKeyProvider(provider);
249
+ const settingsService = getSettingsService();
250
+ const key = await settingsService.getApiKey(apiKeyProvider, { runtime: 'crewly-agent' });
251
+
252
+ if (!key) return;
253
+
254
+ // Set env vars so the provider SDKs can find them.
255
+ // Always override — settings keys take priority over stale env vars.
256
+ switch (provider) {
257
+ case 'anthropic':
258
+ process.env.ANTHROPIC_API_KEY = key;
259
+ break;
260
+ case 'openai':
261
+ process.env.OPENAI_API_KEY = key;
262
+ break;
263
+ case 'google':
264
+ process.env.GOOGLE_GENERATIVE_AI_API_KEY = key;
265
+ break;
266
+ case 'deepseek':
267
+ process.env.DEEPSEEK_API_KEY = key;
268
+ break;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Clear the provider cache (useful for testing or reconfiguration).
274
+ * Also clears any buffered DeepSeek reasoning so a fresh test/run starts clean.
275
+ */
276
+ clearCache(): void {
277
+ this.providerCache.clear();
278
+ this.deepseekReasoningBuffer = '';
279
+ this.deepseekParsedHandles = [];
280
+ }
281
+
282
+ /**
283
+ * Build the custom fetch wrapper for the DeepSeek provider.
284
+ *
285
+ * Returns a function with the same signature as `globalThis.fetch` that:
286
+ * 1. Calls native fetch with the supplied input/init.
287
+ * 2. If the response is a streaming SSE body, tees it and parses one branch
288
+ * for `delta.reasoning_content`, accumulating into `this.deepseekReasoningBuffer`.
289
+ * 3. Returns a new Response wrapping the un-tampered passthrough branch as
290
+ * its body, so AI SDK consumes exactly the bytes DeepSeek sent.
291
+ *
292
+ * If the response is not an SSE stream (e.g. error JSON, no body), it is
293
+ * returned unchanged.
294
+ *
295
+ * **Why a method, not a free function:** the wrapper closes over `this` to
296
+ * append to the per-instance reasoning buffer. Each ModelManager instance
297
+ * has its own buffer (one per AgentRunner per session).
298
+ */
299
+ private makeDeepseekFetch(): (input: unknown, init?: unknown) => Promise<Response> {
300
+ return async (input: unknown, init?: unknown): Promise<Response> => {
301
+ // Cast through `any` because @ai-sdk/openai's fetch type is the standard
302
+ // global fetch shape; we re-export native fetch behavior identically.
303
+ const response: Response = await (globalThis.fetch as any)(input, init);
304
+
305
+ // Only intercept successful streaming responses. Non-stream errors
306
+ // (4xx/5xx with JSON body) and empty bodies pass through untouched.
307
+ if (!response.ok || !response.body) return response;
308
+ const contentType = response.headers.get('content-type') ?? '';
309
+ if (!contentType.includes('text/event-stream')) return response;
310
+
311
+ const parsed = teeAndParse(response.body);
312
+ this.deepseekParsedHandles.push(parsed);
313
+
314
+ // Wrap into a new Response with the passthrough branch as body.
315
+ // Headers, status, and statusText are copied so AI SDK sees an identical
316
+ // response shape.
317
+ return new Response(parsed.passthroughBody, {
318
+ status: response.status,
319
+ statusText: response.statusText,
320
+ headers: response.headers,
321
+ });
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Drain any in-flight DeepSeek SSE parser branches and return the accumulated
327
+ * `reasoning_content` string. Resets the buffer to `''` on each call (consume
328
+ * semantics).
329
+ *
330
+ * **Caller contract:** call AFTER `await streamResult` resolves in agent-runner.
331
+ * The AI SDK consumer branch must have been fully drained for the parser branch
332
+ * (which lags slightly due to tee buffering) to have caught up.
333
+ *
334
+ * **Multi-step accumulation:** if a single `streamText` call made multiple HTTP
335
+ * calls (one per agentic step), reasoning from all steps is concatenated in
336
+ * call order — not separated by step boundary. This matches the user-facing
337
+ * mental model of "what was the model's full chain of thought for this turn."
338
+ *
339
+ * **Returns `null` if no reasoning was captured.** Empty string means a fetch
340
+ * happened but produced no reasoning content (e.g. non-R1 DeepSeek model).
341
+ */
342
+ async consumeDeepseekReasoning(): Promise<string | null> {
343
+ const handles = this.deepseekParsedHandles;
344
+ this.deepseekParsedHandles = [];
345
+ if (handles.length === 0) {
346
+ const buffered = this.deepseekReasoningBuffer;
347
+ this.deepseekReasoningBuffer = '';
348
+ return buffered || null;
349
+ }
350
+ // Wait for all parser branches to finish draining (they should already be
351
+ // done if AI SDK consumer drained, but allow up to one event-loop tick).
352
+ for (const h of handles) {
353
+ if (!h.isDrained()) {
354
+ // Yield once so the parser background reader can flush.
355
+ await new Promise((r) => setImmediate(r));
356
+ }
357
+ this.deepseekReasoningBuffer += h.getReasoning();
358
+ }
359
+ const out = this.deepseekReasoningBuffer;
360
+ this.deepseekReasoningBuffer = '';
361
+ return out || null;
362
+ }
363
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Output Filter Service — API Key Redaction
3
+ *
4
+ * Scans all agent text output for API key patterns and replaces them
5
+ * with [REDACTED] before the output reaches users or logs.
6
+ *
7
+ * Detects patterns from major providers (OpenAI, Anthropic, Google, AWS)
8
+ * as well as generic key/token/secret assignments.
9
+ *
10
+ * @module services/agent/crewly-agent/output-filter.service
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const filter = new OutputFilterService();
15
+ * const safe = filter.redact('My key is sk-abc123xyz');
16
+ * // => 'My key is [REDACTED]'
17
+ * ```
18
+ */
19
+
20
+ /**
21
+ * A single API key detection pattern with a label for audit logging.
22
+ */
23
+ export interface KeyPattern {
24
+ /** Regex to match the key in text */
25
+ pattern: RegExp;
26
+ /** Human-readable label for audit/logging */
27
+ label: string;
28
+ }
29
+
30
+ /**
31
+ * Result of scanning text for API keys.
32
+ */
33
+ export interface ScanResult {
34
+ /** Whether any keys were detected */
35
+ detected: boolean;
36
+ /** Number of keys found */
37
+ count: number;
38
+ /** Labels of the matched patterns */
39
+ matchedPatterns: string[];
40
+ /** Redacted text with keys replaced */
41
+ redactedText: string;
42
+ }
43
+
44
+ /**
45
+ * API key patterns for major providers and generic secrets.
46
+ *
47
+ * Each pattern uses word boundaries or lookbehind/lookahead to minimize
48
+ * false positives while catching real key formats.
49
+ */
50
+ export const API_KEY_PATTERNS: readonly KeyPattern[] = [
51
+ // OpenAI: sk-<org>-<rest> or sk-<48+ chars>
52
+ { pattern: /\bsk-[A-Za-z0-9_-]{20,}/g, label: 'OpenAI API Key' },
53
+ // Anthropic: sk-ant-api03-<rest>
54
+ { pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}/g, label: 'Anthropic API Key' },
55
+ // Google AI: AIza<rest>
56
+ { pattern: /\bAIza[A-Za-z0-9_-]{30,}/g, label: 'Google API Key' },
57
+ // AWS Access Key: AKIA<16 alphanumeric>
58
+ { pattern: /\bAKIA[A-Z0-9]{16}\b/g, label: 'AWS Access Key' },
59
+ // AWS Secret Key (often follows access key)
60
+ { pattern: /(?<=aws_secret_access_key\s*[=:]\s*)[A-Za-z0-9/+=]{30,}/g, label: 'AWS Secret Key' },
61
+ // GitHub tokens: ghp_, gho_, ghu_, ghs_, ghr_
62
+ { pattern: /\bgh[pousr]_[A-Za-z0-9_]{30,}/g, label: 'GitHub Token' },
63
+ // Stripe: sk_live_ or sk_test_
64
+ { pattern: /\bsk_(live|test)_[A-Za-z0-9]{20,}/g, label: 'Stripe API Key' },
65
+ // Supabase anon/service keys (JWT-like, start with eyJ)
66
+ { pattern: /\beyJ[A-Za-z0-9_-]{50,}\.[A-Za-z0-9_-]{50,}\.[A-Za-z0-9_-]{20,}/g, label: 'JWT Token' },
67
+ // Generic key=value patterns (key, token, secret, password, api_key, apikey)
68
+ { pattern: /(?<=(?:api[_-]?key|api[_-]?secret|api[_-]?token|secret[_-]?key|access[_-]?token|auth[_-]?token|password|secret)\s*[=:]\s*["']?)[A-Za-z0-9_/+=.-]{16,}/gi, label: 'Generic Secret' },
69
+ // Environment variable assignments with sensitive names
70
+ { pattern: /(?<=(?:ANTHROPIC_API_KEY|OPENAI_API_KEY|GOOGLE_API_KEY|AWS_SECRET_ACCESS_KEY|STRIPE_SECRET_KEY|SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL|REDIS_URL)\s*=\s*["']?)[^\s"']{8,}/g, label: 'Environment Variable Secret' },
71
+ ] as const;
72
+
73
+ /** Replacement text for redacted keys */
74
+ export const REDACTION_PLACEHOLDER = '[REDACTED]';
75
+
76
+ /**
77
+ * Service that scans and redacts API keys from agent output text.
78
+ */
79
+ export class OutputFilterService {
80
+ private readonly patterns: readonly KeyPattern[];
81
+
82
+ /**
83
+ * Creates a new OutputFilterService.
84
+ * @param customPatterns - Optional additional patterns to detect (appended to defaults)
85
+ */
86
+ constructor(customPatterns?: KeyPattern[]) {
87
+ this.patterns = customPatterns
88
+ ? [...API_KEY_PATTERNS, ...customPatterns]
89
+ : API_KEY_PATTERNS;
90
+ }
91
+
92
+ /**
93
+ * Scans text for API keys and returns detailed results.
94
+ *
95
+ * @param text - Text to scan for API keys
96
+ * @returns Scan result with detection info and redacted text
97
+ */
98
+ scan(text: string): ScanResult {
99
+ if (!text) {
100
+ return { detected: false, count: 0, matchedPatterns: [], redactedText: text };
101
+ }
102
+
103
+ let redacted = text;
104
+ let totalCount = 0;
105
+ const matchedLabels: Set<string> = new Set();
106
+
107
+ for (const { pattern, label } of this.patterns) {
108
+ // Reset regex lastIndex for global patterns
109
+ const regex = new RegExp(pattern.source, pattern.flags);
110
+ const matches = redacted.match(regex);
111
+ if (matches) {
112
+ totalCount += matches.length;
113
+ matchedLabels.add(label);
114
+ redacted = redacted.replace(regex, REDACTION_PLACEHOLDER);
115
+ }
116
+ }
117
+
118
+ return {
119
+ detected: totalCount > 0,
120
+ count: totalCount,
121
+ matchedPatterns: Array.from(matchedLabels),
122
+ redactedText: redacted,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Redacts all API keys in the given text.
128
+ * Convenience method that returns only the redacted string.
129
+ *
130
+ * @param text - Text to redact
131
+ * @returns Text with all detected API keys replaced with [REDACTED]
132
+ */
133
+ redact(text: string): string {
134
+ return this.scan(text).redactedText;
135
+ }
136
+
137
+ /**
138
+ * Checks if text contains any API keys without modifying it.
139
+ *
140
+ * @param text - Text to check
141
+ * @returns True if any API key patterns are detected
142
+ */
143
+ containsKeys(text: string): boolean {
144
+ if (!text) return false;
145
+ for (const { pattern } of this.patterns) {
146
+ const regex = new RegExp(pattern.source, pattern.flags);
147
+ if (regex.test(text)) return true;
148
+ }
149
+ return false;
150
+ }
151
+
152
+ /**
153
+ * Redacts API keys from a structured object (recursively scans string values).
154
+ * Useful for sanitizing tool call arguments and results before logging.
155
+ *
156
+ * @param obj - Object to scan and redact
157
+ * @returns Deep copy with all string values redacted
158
+ */
159
+ redactObject(obj: unknown): unknown {
160
+ if (typeof obj === 'string') {
161
+ return this.redact(obj);
162
+ }
163
+ if (Array.isArray(obj)) {
164
+ return obj.map((item) => this.redactObject(item));
165
+ }
166
+ if (typeof obj === 'object' && obj !== null) {
167
+ const result: Record<string, unknown> = {};
168
+ for (const [key, value] of Object.entries(obj)) {
169
+ result[key] = this.redactObject(value);
170
+ }
171
+ return result;
172
+ }
173
+ return obj;
174
+ }
175
+ }