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.
- package/config/skills/agent/onboarding/synthesize-hierarchy/SKILL.md +65 -0
- package/config/skills/agent/onboarding/synthesize-hierarchy/execute.sh +61 -0
- package/config/skills/agent/web-search/SKILL.md +70 -0
- package/config/skills/agent/web-search/execute.sh +170 -0
- package/config/skills/agent/web-search/skill.json +23 -0
- package/dist/backend/backend/src/constants.d.ts +34 -1
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +34 -1
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +22 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +58 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +3 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts +27 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js +108 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts +6 -2
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js +9 -3
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +36 -2
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +18 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +24 -2
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts +90 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.js +309 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.js +134 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts +78 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.js +358 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup.types.d.ts +163 -0
- package/dist/backend/backend/src/services/backup/backup.types.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup.types.js +13 -0
- package/dist/backend/backend/src/services/backup/backup.types.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +29 -2
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +97 -13
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts +102 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +164 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts +21 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/fission/fission-guard.service.js +30 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.js.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts +4 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js +8 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts +79 -58
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +140 -65
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts +117 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js +189 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js +2 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js +17 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +50 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +71 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +18 -0
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js +75 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +115 -0
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +189 -3
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +28 -0
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js +61 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.js +67 -2
- package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts +19 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.js +39 -2
- package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts +41 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.js +169 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts +4 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js +21 -0
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.js +8 -0
- package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.js +1 -0
- package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +34 -1
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +34 -1
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts +70 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts.map +1 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js +427 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts +90 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.js +309 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.js +134 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts +78 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.js +358 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup.types.d.ts +163 -0
- package/dist/cli/backend/src/services/backup/backup.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup.types.js +13 -0
- package/dist/cli/backend/src/services/backup/backup.types.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts +410 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js +863 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts +292 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js +1093 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +328 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.js +171 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts +89 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.js +148 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.js.map +1 -0
- package/dist/cli/backend/src/services/user/user-identity.service.d.ts +86 -0
- package/dist/cli/backend/src/services/user/user-identity.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/user/user-identity.service.js +190 -0
- package/dist/cli/backend/src/services/user/user-identity.service.js.map +1 -0
- package/dist/cli/cli/src/commands/backup.d.ts +31 -0
- package/dist/cli/cli/src/commands/backup.d.ts.map +1 -0
- package/dist/cli/cli/src/commands/backup.js +280 -0
- package/dist/cli/cli/src/commands/backup.js.map +1 -0
- package/dist/cli/cli/src/index.js +10 -0
- package/dist/cli/cli/src/index.js.map +1 -1
- package/package.json +9 -3
- package/packages/crewly-agent/README.md +27 -0
- package/packages/crewly-agent/bin/crewly-agent +33 -0
- package/packages/crewly-agent/package.json +39 -0
- package/packages/crewly-agent/src/cli.ts +168 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +2355 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.ts +1827 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.test.ts +153 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.ts +225 -0
- package/packages/crewly-agent/src/runtime/agent-worker.test.ts +171 -0
- package/packages/crewly-agent/src/runtime/agent-worker.ts +193 -0
- package/packages/crewly-agent/src/runtime/api-client.ts +143 -0
- package/packages/crewly-agent/src/runtime/approval-queue.service.ts +307 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.test.ts +208 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.ts +332 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.test.ts +178 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.ts +151 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.test.ts +274 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.ts +311 -0
- package/packages/crewly-agent/src/runtime/cloud-config.ts +67 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.test.ts +165 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.ts +168 -0
- package/packages/crewly-agent/src/runtime/env-isolation.service.ts +246 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.test.ts +280 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.ts +317 -0
- package/packages/crewly-agent/src/runtime/index.ts +38 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.test.ts +352 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.ts +244 -0
- package/packages/crewly-agent/src/runtime/model-manager.test.ts +326 -0
- package/packages/crewly-agent/src/runtime/model-manager.ts +363 -0
- package/packages/crewly-agent/src/runtime/output-filter.service.ts +175 -0
- package/packages/crewly-agent/src/runtime/prompt-guard.service.ts +303 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.test.ts +228 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.ts +353 -0
- package/packages/crewly-agent/src/runtime/tool-registry.test.ts +2510 -0
- package/packages/crewly-agent/src/runtime/tool-registry.ts +2104 -0
- package/packages/crewly-agent/src/runtime/types.test.ts +519 -0
- package/packages/crewly-agent/src/runtime/types.ts +637 -0
- package/packages/crewly-agent/src/runtime/web-search.tool.test.ts +131 -0
- package/packages/crewly-agent/src/runtime/web-search.tool.ts +140 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek SSE Transform — extracts `reasoning_content` from DeepSeek-R1 streaming responses.
|
|
3
|
+
*
|
|
4
|
+
* **Why this exists:**
|
|
5
|
+
* DeepSeek-R1 (`deepseek-reasoner`) returns chain-of-thought as `delta.reasoning_content`
|
|
6
|
+
* inside each SSE chunk. The Vercel AI SDK's @ai-sdk/openai chat-completions parser
|
|
7
|
+
* (3.0.41) accumulates `reasoning_tokens` into `usage.reasoningTokens` (correct billing
|
|
8
|
+
* surface) but **does not** map `reasoning_content` text to any content-part or
|
|
9
|
+
* `reasoningText` field. Result: users pay for reasoning tokens but the reasoning
|
|
10
|
+
* text is silently dropped.
|
|
11
|
+
*
|
|
12
|
+
* **What this module does:**
|
|
13
|
+
* - Pure stream transformer: takes a DeepSeek SSE response body, tees it,
|
|
14
|
+
* passes one branch through unchanged (for AI SDK to consume normally),
|
|
15
|
+
* and parses the other branch to extract reasoning_content into an
|
|
16
|
+
* accumulator string.
|
|
17
|
+
* - Zero dependency on AI SDK internals — operates only on the raw SSE byte
|
|
18
|
+
* stream that AI SDK is about to consume.
|
|
19
|
+
*
|
|
20
|
+
* **Architectural note (Sam memo reconciliation):**
|
|
21
|
+
* Earlier scope memo referenced wrapping a "PR #425 translator output" layer.
|
|
22
|
+
* Verified: PR #425 is the frontend Settings UI fix; no Crewly-owned translator
|
|
23
|
+
* exists. The only seam Crewly owns between DeepSeek and the AI SDK is the
|
|
24
|
+
* `createOpenAI({ fetch })` upstream hook in model-manager.ts. This module is
|
|
25
|
+
* the body of that hook.
|
|
26
|
+
*
|
|
27
|
+
* **DeepClaude pattern reference (read-only):**
|
|
28
|
+
* Reasoning-content extraction approach borrowed conceptually from public
|
|
29
|
+
* DeepClaude pattern (HTTP proxy that splits R1 reasoning from final answer).
|
|
30
|
+
* **No code, no fork, no dependency** — pattern reference only, per Anthropic
|
|
31
|
+
* ToS guardrail flagged by Arch.
|
|
32
|
+
*
|
|
33
|
+
* @module services/agent/crewly-agent/deepseek-sse-transform
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Shape of a single SSE `data:` payload from DeepSeek's chat-completions endpoint.
|
|
38
|
+
* Only the fields we read are typed; the rest is left open.
|
|
39
|
+
*/
|
|
40
|
+
interface DeepseekSseChunk {
|
|
41
|
+
choices?: Array<{
|
|
42
|
+
delta?: {
|
|
43
|
+
content?: string;
|
|
44
|
+
reasoning_content?: string;
|
|
45
|
+
};
|
|
46
|
+
finish_reason?: string | null;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Result of teeing and parsing a DeepSeek SSE body.
|
|
52
|
+
*
|
|
53
|
+
* - `passthroughBody` is the un-tampered byte stream that should be handed
|
|
54
|
+
* to the consumer (AI SDK) as the response body.
|
|
55
|
+
* - `getReasoning()` returns the accumulated reasoning_content text once
|
|
56
|
+
* the underlying stream has fully drained. Calling it before drain
|
|
57
|
+
* returns whatever has been parsed so far.
|
|
58
|
+
*/
|
|
59
|
+
export interface ParsedDeepseekSse {
|
|
60
|
+
passthroughBody: ReadableStream<Uint8Array>;
|
|
61
|
+
getReasoning(): string;
|
|
62
|
+
/** True once the parser has seen the SSE `[DONE]` sentinel or stream end. */
|
|
63
|
+
isDrained(): boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse a single SSE event-block (one or more `data:` lines + a blank line).
|
|
68
|
+
*
|
|
69
|
+
* Returns the accumulated reasoning_content string from this block, or
|
|
70
|
+
* empty string if the block had no reasoning content. Returns `null` if
|
|
71
|
+
* the block is the `[DONE]` sentinel.
|
|
72
|
+
*
|
|
73
|
+
* @param block - One SSE event block (between blank-line delimiters)
|
|
74
|
+
* @returns reasoning_content string, or `null` if `[DONE]`
|
|
75
|
+
*/
|
|
76
|
+
export function parseSseBlock(block: string): string | null {
|
|
77
|
+
const lines = block.split('\n');
|
|
78
|
+
let reasoning = '';
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (!line.startsWith('data:')) continue;
|
|
81
|
+
const payload = line.slice(5).trim();
|
|
82
|
+
if (!payload) continue;
|
|
83
|
+
if (payload === '[DONE]') return null;
|
|
84
|
+
try {
|
|
85
|
+
const chunk = JSON.parse(payload) as DeepseekSseChunk;
|
|
86
|
+
const r = chunk.choices?.[0]?.delta?.reasoning_content;
|
|
87
|
+
if (typeof r === 'string') {
|
|
88
|
+
reasoning += r;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Malformed JSON — skip silently; AI SDK consumer will surface
|
|
92
|
+
// any real error itself when it parses the same chunk.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return reasoning;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Tee a DeepSeek SSE response body and parse one branch for reasoning_content
|
|
100
|
+
* while passing the other branch through to the AI SDK consumer unchanged.
|
|
101
|
+
*
|
|
102
|
+
* The parser runs as a fire-and-forget background reader on the cloned stream.
|
|
103
|
+
* It accumulates reasoning text into an internal buffer that the caller can
|
|
104
|
+
* read via `getReasoning()` after the consumer has drained the passthrough.
|
|
105
|
+
*
|
|
106
|
+
* **Stream safety:**
|
|
107
|
+
* - The tee is symmetric: backpressure on the consumer branch does not
|
|
108
|
+
* stall the parser branch (and vice versa) thanks to ReadableStream.tee()
|
|
109
|
+
* internal buffering.
|
|
110
|
+
* - Parser errors are caught and logged but never propagated to the consumer.
|
|
111
|
+
*
|
|
112
|
+
* @param body - Raw SSE response body from DeepSeek
|
|
113
|
+
* @returns Object with passthrough body and reasoning accumulator
|
|
114
|
+
*/
|
|
115
|
+
export function teeAndParse(body: ReadableStream<Uint8Array>): ParsedDeepseekSse {
|
|
116
|
+
const [consumerBranch, parserBranch] = body.tee();
|
|
117
|
+
let reasoning = '';
|
|
118
|
+
let drained = false;
|
|
119
|
+
|
|
120
|
+
// Background reader: drain parserBranch, parse SSE, accumulate reasoning.
|
|
121
|
+
// Errors are swallowed — consumer branch is independent and unaffected.
|
|
122
|
+
void (async () => {
|
|
123
|
+
const reader = parserBranch.getReader();
|
|
124
|
+
const decoder = new TextDecoder();
|
|
125
|
+
let buffer = '';
|
|
126
|
+
try {
|
|
127
|
+
while (true) {
|
|
128
|
+
const { done, value } = await reader.read();
|
|
129
|
+
if (done) break;
|
|
130
|
+
buffer += decoder.decode(value, { stream: true });
|
|
131
|
+
// SSE event blocks are delimited by blank lines (\n\n).
|
|
132
|
+
let blankIdx: number;
|
|
133
|
+
while ((blankIdx = buffer.indexOf('\n\n')) >= 0) {
|
|
134
|
+
const block = buffer.slice(0, blankIdx);
|
|
135
|
+
buffer = buffer.slice(blankIdx + 2);
|
|
136
|
+
const result = parseSseBlock(block);
|
|
137
|
+
if (result === null) {
|
|
138
|
+
drained = true;
|
|
139
|
+
} else if (result) {
|
|
140
|
+
reasoning += result;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Flush trailing partial block (if SSE source ended mid-block — defensive)
|
|
145
|
+
if (buffer.trim()) {
|
|
146
|
+
const result = parseSseBlock(buffer);
|
|
147
|
+
if (result && result !== null) reasoning += result;
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// Parser failures must not break consumer flow — log and exit.
|
|
151
|
+
// eslint-disable-next-line no-console
|
|
152
|
+
console.warn('[DeepSeek SSE Transform] parser branch error (consumer unaffected):', err);
|
|
153
|
+
} finally {
|
|
154
|
+
drained = true;
|
|
155
|
+
try {
|
|
156
|
+
reader.releaseLock();
|
|
157
|
+
} catch {
|
|
158
|
+
/* already released */
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
})();
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
passthroughBody: consumerBranch,
|
|
165
|
+
getReasoning: () => reasoning,
|
|
166
|
+
isDrained: () => drained,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Isolation Service
|
|
3
|
+
*
|
|
4
|
+
* Strips sensitive environment variables (API keys, tokens, secrets) from
|
|
5
|
+
* child process environments when agents execute bash commands. Maintains
|
|
6
|
+
* an allowlist of safe variables and provides a mechanism to explicitly
|
|
7
|
+
* pass specific vars when needed.
|
|
8
|
+
*
|
|
9
|
+
* @module services/agent/crewly-agent/env-isolation.service
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const isolation = new EnvIsolationService();
|
|
14
|
+
* const safeEnv = isolation.createSafeEnv(process.env);
|
|
15
|
+
* // safeEnv has PATH, HOME, etc. but no ANTHROPIC_API_KEY
|
|
16
|
+
*
|
|
17
|
+
* const withOverrides = isolation.createSafeEnv(process.env, ['MY_CUSTOM_VAR']);
|
|
18
|
+
* // safeEnv + MY_CUSTOM_VAR explicitly included
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Environment variables that are always safe to pass to child processes.
|
|
24
|
+
* These are essential for basic shell operation and don't contain secrets.
|
|
25
|
+
*/
|
|
26
|
+
export const SAFE_ENV_ALLOWLIST: readonly string[] = [
|
|
27
|
+
// System essentials
|
|
28
|
+
'PATH',
|
|
29
|
+
'HOME',
|
|
30
|
+
'USER',
|
|
31
|
+
'LOGNAME',
|
|
32
|
+
'SHELL',
|
|
33
|
+
'TERM',
|
|
34
|
+
'TERM_PROGRAM',
|
|
35
|
+
'LANG',
|
|
36
|
+
'LC_ALL',
|
|
37
|
+
'LC_CTYPE',
|
|
38
|
+
'TZ',
|
|
39
|
+
'TMPDIR',
|
|
40
|
+
'TEMP',
|
|
41
|
+
'TMP',
|
|
42
|
+
|
|
43
|
+
// Node.js / runtime
|
|
44
|
+
'NODE_ENV',
|
|
45
|
+
'NODE_PATH',
|
|
46
|
+
'NODE_OPTIONS',
|
|
47
|
+
'NPM_CONFIG_PREFIX',
|
|
48
|
+
'NVM_DIR',
|
|
49
|
+
'NVM_BIN',
|
|
50
|
+
|
|
51
|
+
// Build tools
|
|
52
|
+
'FORCE_COLOR',
|
|
53
|
+
'NO_COLOR',
|
|
54
|
+
'CI',
|
|
55
|
+
'EDITOR',
|
|
56
|
+
'VISUAL',
|
|
57
|
+
'PAGER',
|
|
58
|
+
|
|
59
|
+
// Git (non-secret)
|
|
60
|
+
'GIT_AUTHOR_NAME',
|
|
61
|
+
'GIT_AUTHOR_EMAIL',
|
|
62
|
+
'GIT_COMMITTER_NAME',
|
|
63
|
+
'GIT_COMMITTER_EMAIL',
|
|
64
|
+
|
|
65
|
+
// Platform
|
|
66
|
+
'HOSTNAME',
|
|
67
|
+
'PWD',
|
|
68
|
+
'OLDPWD',
|
|
69
|
+
'SHLVL',
|
|
70
|
+
'XDG_CONFIG_HOME',
|
|
71
|
+
'XDG_DATA_HOME',
|
|
72
|
+
'XDG_CACHE_HOME',
|
|
73
|
+
'XDG_RUNTIME_DIR',
|
|
74
|
+
|
|
75
|
+
// macOS specifics
|
|
76
|
+
'COMMAND_MODE',
|
|
77
|
+
'__CF_USER_TEXT_ENCODING',
|
|
78
|
+
|
|
79
|
+
// Python
|
|
80
|
+
'PYTHONPATH',
|
|
81
|
+
'VIRTUAL_ENV',
|
|
82
|
+
|
|
83
|
+
// Rust
|
|
84
|
+
'CARGO_HOME',
|
|
85
|
+
'RUSTUP_HOME',
|
|
86
|
+
|
|
87
|
+
// Go
|
|
88
|
+
'GOPATH',
|
|
89
|
+
'GOROOT',
|
|
90
|
+
|
|
91
|
+
// Java
|
|
92
|
+
'JAVA_HOME',
|
|
93
|
+
|
|
94
|
+
// Homebrew
|
|
95
|
+
'HOMEBREW_PREFIX',
|
|
96
|
+
'HOMEBREW_CELLAR',
|
|
97
|
+
'HOMEBREW_REPOSITORY',
|
|
98
|
+
] as const;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Patterns that identify sensitive environment variable names.
|
|
102
|
+
* Variables matching these patterns are stripped from child processes.
|
|
103
|
+
*/
|
|
104
|
+
export const SENSITIVE_ENV_PATTERNS: readonly RegExp[] = [
|
|
105
|
+
/api[_-]?key/i,
|
|
106
|
+
/api[_-]?secret/i,
|
|
107
|
+
/api[_-]?token/i,
|
|
108
|
+
/secret[_-]?key/i,
|
|
109
|
+
/access[_-]?key/i,
|
|
110
|
+
/access[_-]?token/i,
|
|
111
|
+
/auth[_-]?token/i,
|
|
112
|
+
/bearer[_-]?token/i,
|
|
113
|
+
/private[_-]?key/i,
|
|
114
|
+
/password/i,
|
|
115
|
+
/passwd/i,
|
|
116
|
+
/credential/i,
|
|
117
|
+
/^AWS_SECRET/i,
|
|
118
|
+
/^AWS_SESSION/i,
|
|
119
|
+
/^ANTHROPIC_API/i,
|
|
120
|
+
/^OPENAI_API/i,
|
|
121
|
+
/^GOOGLE_API/i,
|
|
122
|
+
/^GOOGLE_APPLICATION_CREDENTIALS/i,
|
|
123
|
+
/^STRIPE_SECRET/i,
|
|
124
|
+
/^SUPABASE_SERVICE_ROLE/i,
|
|
125
|
+
/^DATABASE_URL$/i,
|
|
126
|
+
/^REDIS_URL$/i,
|
|
127
|
+
/^MONGODB_URI$/i,
|
|
128
|
+
/^GITHUB_TOKEN$/i,
|
|
129
|
+
/^GH_TOKEN$/i,
|
|
130
|
+
/^NPM_TOKEN$/i,
|
|
131
|
+
/^DOCKER_PASSWORD$/i,
|
|
132
|
+
/^DOCKER_AUTH$/i,
|
|
133
|
+
/connection[_-]?string/i,
|
|
134
|
+
/^SENTRY_DSN$/i,
|
|
135
|
+
/^DATADOG_API_KEY$/i,
|
|
136
|
+
/^SENDGRID_API_KEY$/i,
|
|
137
|
+
/^TWILIO_AUTH_TOKEN$/i,
|
|
138
|
+
/^SLACK_BOT_TOKEN$/i,
|
|
139
|
+
/^SLACK_TOKEN$/i,
|
|
140
|
+
/^DISCORD_TOKEN$/i,
|
|
141
|
+
/^WEBHOOK_SECRET$/i,
|
|
142
|
+
/^SIGNING_SECRET$/i,
|
|
143
|
+
/^ENCRYPTION_KEY$/i,
|
|
144
|
+
/^JWT_SECRET$/i,
|
|
145
|
+
/^SESSION_SECRET$/i,
|
|
146
|
+
] as const;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Service that creates sanitized environment objects for child processes.
|
|
150
|
+
*/
|
|
151
|
+
export class EnvIsolationService {
|
|
152
|
+
private readonly allowlist: Set<string>;
|
|
153
|
+
private readonly sensitivePatterns: readonly RegExp[];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Creates a new EnvIsolationService.
|
|
157
|
+
*
|
|
158
|
+
* @param additionalSafeVars - Extra variable names to always allow
|
|
159
|
+
* @param additionalSensitivePatterns - Extra patterns to block
|
|
160
|
+
*/
|
|
161
|
+
constructor(
|
|
162
|
+
additionalSafeVars?: string[],
|
|
163
|
+
additionalSensitivePatterns?: RegExp[],
|
|
164
|
+
) {
|
|
165
|
+
this.allowlist = new Set([...SAFE_ENV_ALLOWLIST, ...(additionalSafeVars || [])]);
|
|
166
|
+
this.sensitivePatterns = additionalSensitivePatterns
|
|
167
|
+
? [...SENSITIVE_ENV_PATTERNS, ...additionalSensitivePatterns]
|
|
168
|
+
: SENSITIVE_ENV_PATTERNS;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Creates a safe environment object by filtering out sensitive variables.
|
|
173
|
+
*
|
|
174
|
+
* Strategy:
|
|
175
|
+
* 1. Start with all env vars
|
|
176
|
+
* 2. Keep vars on the allowlist
|
|
177
|
+
* 3. Remove vars matching sensitive patterns
|
|
178
|
+
* 4. Add explicitly requested vars (overrides)
|
|
179
|
+
*
|
|
180
|
+
* @param sourceEnv - Source environment (typically process.env)
|
|
181
|
+
* @param explicitVars - Variable names to explicitly include even if they'd be filtered
|
|
182
|
+
* @returns Sanitized environment object safe for child processes
|
|
183
|
+
*/
|
|
184
|
+
createSafeEnv(
|
|
185
|
+
sourceEnv: Record<string, string | undefined>,
|
|
186
|
+
explicitVars?: string[],
|
|
187
|
+
): Record<string, string> {
|
|
188
|
+
const safeEnv: Record<string, string> = {};
|
|
189
|
+
const explicitSet = new Set(explicitVars || []);
|
|
190
|
+
|
|
191
|
+
for (const [key, value] of Object.entries(sourceEnv)) {
|
|
192
|
+
if (value === undefined) continue;
|
|
193
|
+
|
|
194
|
+
// Always include explicitly requested vars
|
|
195
|
+
if (explicitSet.has(key)) {
|
|
196
|
+
safeEnv[key] = value;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Include if on allowlist
|
|
201
|
+
if (this.allowlist.has(key)) {
|
|
202
|
+
safeEnv[key] = value;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Skip if matches a sensitive pattern
|
|
207
|
+
if (this.isSensitive(key)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Default: include non-sensitive vars not on either list
|
|
212
|
+
safeEnv[key] = value;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return safeEnv;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Checks if an environment variable name matches any sensitive pattern.
|
|
220
|
+
*
|
|
221
|
+
* @param varName - Environment variable name to check
|
|
222
|
+
* @returns True if the variable is considered sensitive
|
|
223
|
+
*/
|
|
224
|
+
isSensitive(varName: string): boolean {
|
|
225
|
+
return this.sensitivePatterns.some((pattern) => pattern.test(varName));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Returns the list of sensitive variable names found in the given environment.
|
|
230
|
+
* Useful for audit logging.
|
|
231
|
+
*
|
|
232
|
+
* @param sourceEnv - Environment to scan
|
|
233
|
+
* @returns Array of sensitive variable names found
|
|
234
|
+
*/
|
|
235
|
+
findSensitiveVars(sourceEnv: Record<string, string | undefined>): string[] {
|
|
236
|
+
return Object.keys(sourceEnv).filter((key) => this.isSensitive(key));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Returns the current allowlist as an array.
|
|
241
|
+
* @returns Copy of the safe variable allowlist
|
|
242
|
+
*/
|
|
243
|
+
getAllowlist(): string[] {
|
|
244
|
+
return Array.from(this.allowlist);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { InProcessLogBuffer } from './in-process-log-buffer.js';
|
|
6
|
+
|
|
7
|
+
// Constants inlined here too — the standalone runtime owns its own values.
|
|
8
|
+
const CREWLY_CONSTANTS = {
|
|
9
|
+
PATHS: {
|
|
10
|
+
CREWLY_HOME: '.crewly',
|
|
11
|
+
LOGS_DIR: 'logs',
|
|
12
|
+
},
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
const LOG_ROTATION_CONSTANTS = {
|
|
16
|
+
SESSIONS_LOG_DIR: 'sessions',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
describe('InProcessLogBuffer', () => {
|
|
20
|
+
let buffer: InProcessLogBuffer;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
InProcessLogBuffer.resetInstance();
|
|
24
|
+
buffer = InProcessLogBuffer.getInstance();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('singleton', () => {
|
|
28
|
+
it('should return the same instance', () => {
|
|
29
|
+
const a = InProcessLogBuffer.getInstance();
|
|
30
|
+
const b = InProcessLogBuffer.getInstance();
|
|
31
|
+
expect(a).toBe(b);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return new instance after reset', () => {
|
|
35
|
+
const a = InProcessLogBuffer.getInstance();
|
|
36
|
+
InProcessLogBuffer.resetInstance();
|
|
37
|
+
const b = InProcessLogBuffer.getInstance();
|
|
38
|
+
expect(a).not.toBe(b);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('append', () => {
|
|
43
|
+
it('should create session on first append', () => {
|
|
44
|
+
expect(buffer.hasSession('test')).toBe(false);
|
|
45
|
+
buffer.append('test', 'info', 'hello');
|
|
46
|
+
expect(buffer.hasSession('test')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should enforce ring buffer limit', () => {
|
|
50
|
+
for (let i = 0; i < 600; i++) {
|
|
51
|
+
buffer.append('test', 'info', `line ${i}`);
|
|
52
|
+
}
|
|
53
|
+
const output = buffer.capture('test', 600);
|
|
54
|
+
const lines = output.split('\n');
|
|
55
|
+
expect(lines.length).toBe(500);
|
|
56
|
+
expect(lines[lines.length - 1]).toContain('line 599');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('capture', () => {
|
|
61
|
+
it('should return placeholder for empty session', () => {
|
|
62
|
+
buffer.registerSession('empty');
|
|
63
|
+
const output = buffer.capture('empty');
|
|
64
|
+
expect(output).toContain('No output yet');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return placeholder for unknown session', () => {
|
|
68
|
+
const output = buffer.capture('nonexistent');
|
|
69
|
+
expect(output).toContain('No output yet');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should format entries with timestamps', () => {
|
|
73
|
+
buffer.append('test', 'info', 'hello world');
|
|
74
|
+
const output = buffer.capture('test');
|
|
75
|
+
expect(output).toMatch(/\[\d{2}:\d{2}:\d{2}\.\d{3}\] hello world/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should prefix error and warn levels', () => {
|
|
79
|
+
buffer.append('test', 'error', 'something broke');
|
|
80
|
+
buffer.append('test', 'warn', 'be careful');
|
|
81
|
+
buffer.append('test', 'debug', 'details');
|
|
82
|
+
const output = buffer.capture('test');
|
|
83
|
+
expect(output).toContain('ERROR: something broke');
|
|
84
|
+
expect(output).toContain('WARN: be careful');
|
|
85
|
+
expect(output).toContain('DEBUG: details');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should respect lines parameter', () => {
|
|
89
|
+
buffer.append('test', 'info', 'line 1');
|
|
90
|
+
buffer.append('test', 'info', 'line 2');
|
|
91
|
+
buffer.append('test', 'info', 'line 3');
|
|
92
|
+
const output = buffer.capture('test', 2);
|
|
93
|
+
const lines = output.split('\n');
|
|
94
|
+
expect(lines.length).toBe(2);
|
|
95
|
+
expect(lines[1]).toContain('line 3');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('session management', () => {
|
|
100
|
+
it('should register empty session', () => {
|
|
101
|
+
buffer.registerSession('new');
|
|
102
|
+
expect(buffer.hasSession('new')).toBe(true);
|
|
103
|
+
expect(buffer.capture('new')).toContain('No output yet');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should remove session', () => {
|
|
107
|
+
buffer.append('test', 'info', 'data');
|
|
108
|
+
buffer.removeSession('test');
|
|
109
|
+
expect(buffer.hasSession('test')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should list session names', () => {
|
|
113
|
+
buffer.registerSession('a');
|
|
114
|
+
buffer.registerSession('b');
|
|
115
|
+
buffer.append('c', 'info', 'test');
|
|
116
|
+
expect(buffer.getSessionNames().sort()).toEqual(['a', 'b', 'c']);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should clear all sessions', () => {
|
|
120
|
+
buffer.registerSession('a');
|
|
121
|
+
buffer.registerSession('b');
|
|
122
|
+
buffer.clear();
|
|
123
|
+
expect(buffer.getSessionNames()).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('EventEmitter data events', () => {
|
|
128
|
+
it('should emit data event on append', () => new Promise<void>((resolve, reject) => {
|
|
129
|
+
buffer.on('data', (sessionName: string, formattedLine: string) => {
|
|
130
|
+
try {
|
|
131
|
+
expect(sessionName).toBe('test-session');
|
|
132
|
+
expect(formattedLine).toContain('hello world');
|
|
133
|
+
expect(formattedLine).toMatch(/\[\d{2}:\d{2}:\d{2}\.\d{3}\] hello world/);
|
|
134
|
+
resolve();
|
|
135
|
+
} catch (err) { reject(err); }
|
|
136
|
+
});
|
|
137
|
+
buffer.append('test-session', 'info', 'hello world');
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
it('should emit data event with level prefix for errors', () => new Promise<void>((resolve, reject) => {
|
|
141
|
+
buffer.on('data', (sessionName: string, formattedLine: string) => {
|
|
142
|
+
try {
|
|
143
|
+
expect(sessionName).toBe('test-session');
|
|
144
|
+
expect(formattedLine).toContain('ERROR: something broke');
|
|
145
|
+
resolve();
|
|
146
|
+
} catch (err) { reject(err); }
|
|
147
|
+
});
|
|
148
|
+
buffer.append('test-session', 'error', 'something broke');
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
it('should emit data events for each append', () => {
|
|
152
|
+
const events: string[] = [];
|
|
153
|
+
buffer.on('data', (_session: string, line: string) => {
|
|
154
|
+
events.push(line);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
buffer.append('s1', 'info', 'first');
|
|
158
|
+
buffer.append('s2', 'warn', 'second');
|
|
159
|
+
|
|
160
|
+
expect(events.length).toBe(2);
|
|
161
|
+
expect(events[0]).toContain('first');
|
|
162
|
+
expect(events[1]).toContain('WARN: second');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should include session name in data event', () => {
|
|
166
|
+
const sessions: string[] = [];
|
|
167
|
+
buffer.on('data', (session: string) => {
|
|
168
|
+
sessions.push(session);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
buffer.append('agent-a', 'info', 'msg1');
|
|
172
|
+
buffer.append('agent-b', 'info', 'msg2');
|
|
173
|
+
|
|
174
|
+
expect(sessions).toEqual(['agent-a', 'agent-b']);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should clean up listeners on reset', () => {
|
|
178
|
+
const handler = vi.fn();
|
|
179
|
+
buffer.on('data', handler);
|
|
180
|
+
|
|
181
|
+
InProcessLogBuffer.resetInstance();
|
|
182
|
+
const newBuffer = InProcessLogBuffer.getInstance();
|
|
183
|
+
newBuffer.append('test', 'info', 'after reset');
|
|
184
|
+
|
|
185
|
+
// Old handler should NOT be called since instance was reset
|
|
186
|
+
expect(handler).not.toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('file persistence', () => {
|
|
191
|
+
const sessionLogsDir = path.join(
|
|
192
|
+
os.homedir(),
|
|
193
|
+
CREWLY_CONSTANTS.PATHS.CREWLY_HOME,
|
|
194
|
+
CREWLY_CONSTANTS.PATHS.LOGS_DIR,
|
|
195
|
+
LOG_ROTATION_CONSTANTS.SESSIONS_LOG_DIR,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const testSessionName = `__test-inprocess-${Date.now()}`;
|
|
199
|
+
|
|
200
|
+
afterEach(() => {
|
|
201
|
+
// Clean up test log file
|
|
202
|
+
const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
|
|
203
|
+
try { fs.unlinkSync(logPath); } catch { /* may not exist */ }
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should create a log file on registerSession', (done) => {
|
|
207
|
+
buffer.registerSession(testSessionName);
|
|
208
|
+
const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
|
|
209
|
+
// WriteStream opens the file lazily — give it a tick to flush the header
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
expect(fs.existsSync(logPath)).toBe(true);
|
|
212
|
+
done();
|
|
213
|
+
}, 100);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should write log entries to disk on append', (done) => {
|
|
217
|
+
buffer.registerSession(testSessionName);
|
|
218
|
+
buffer.append(testSessionName, 'info', 'disk test message');
|
|
219
|
+
buffer.append(testSessionName, 'error', 'disk error message');
|
|
220
|
+
|
|
221
|
+
// WriteStream is async — give it a tick to flush
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
|
|
224
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
225
|
+
expect(content).toContain('SESSION STARTED');
|
|
226
|
+
expect(content).toContain('disk test message');
|
|
227
|
+
expect(content).toContain('ERROR: disk error message');
|
|
228
|
+
done();
|
|
229
|
+
}, 100);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should write SESSION ENDED marker on removeSession', (done) => {
|
|
233
|
+
buffer.registerSession(testSessionName);
|
|
234
|
+
buffer.append(testSessionName, 'info', 'before shutdown');
|
|
235
|
+
buffer.removeSession(testSessionName);
|
|
236
|
+
|
|
237
|
+
setTimeout(() => {
|
|
238
|
+
const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
|
|
239
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
240
|
+
expect(content).toContain('SESSION ENDED');
|
|
241
|
+
done();
|
|
242
|
+
}, 100);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should write RESTARTED marker when session is re-registered', (done) => {
|
|
246
|
+
buffer.registerSession(testSessionName);
|
|
247
|
+
buffer.append(testSessionName, 'info', 'first run');
|
|
248
|
+
buffer.removeSession(testSessionName);
|
|
249
|
+
|
|
250
|
+
// Re-register same session (simulates restart)
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
buffer.registerSession(testSessionName);
|
|
253
|
+
buffer.append(testSessionName, 'info', 'second run');
|
|
254
|
+
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
|
|
257
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
258
|
+
expect(content).toContain('SESSION STARTED');
|
|
259
|
+
expect(content).toContain('first run');
|
|
260
|
+
expect(content).toContain('SESSION RESTARTED');
|
|
261
|
+
expect(content).toContain('second run');
|
|
262
|
+
done();
|
|
263
|
+
}, 100);
|
|
264
|
+
}, 100);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should include full ISO timestamp in file entries', (done) => {
|
|
268
|
+
buffer.registerSession(testSessionName);
|
|
269
|
+
buffer.append(testSessionName, 'info', 'timestamp check');
|
|
270
|
+
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
|
|
273
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
274
|
+
// File entries should have full ISO timestamp like "2026-03-18T16:01:00.000Z"
|
|
275
|
+
expect(content).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z.*timestamp check/);
|
|
276
|
+
done();
|
|
277
|
+
}, 100);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|