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,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crewly Auditor Tool Registry
|
|
3
|
+
*
|
|
4
|
+
* Read-only tools for the Auditor agent. These tools allow the auditor
|
|
5
|
+
* to observe agent activity, read logs, check task alignment, and write
|
|
6
|
+
* structured bug reports — without any ability to modify system state.
|
|
7
|
+
*
|
|
8
|
+
* @module services/agent/crewly-agent/auditor-tools
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { promises as fsPromises } from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import type { CrewlyApiClient } from './api-client.js';
|
|
15
|
+
import type { ToolDefinition } from './types.js';
|
|
16
|
+
import { convertMarkdownToSlackMrkdwn } from './tool-registry.js';
|
|
17
|
+
|
|
18
|
+
/** Severity levels for audit findings */
|
|
19
|
+
const AUDIT_SEVERITIES = ['critical', 'high', 'medium', 'low'] as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create the auditor tool set for the Crewly Auditor agent.
|
|
23
|
+
*
|
|
24
|
+
* All tools are read-only except `write_audit_report` which appends
|
|
25
|
+
* to the audit log file.
|
|
26
|
+
*
|
|
27
|
+
* @param client - API client for Crewly REST API calls
|
|
28
|
+
* @param projectPath - Project root path for file operations
|
|
29
|
+
* @returns Object of named tools for the auditor agent
|
|
30
|
+
*/
|
|
31
|
+
export function createAuditorTools(
|
|
32
|
+
client: CrewlyApiClient,
|
|
33
|
+
projectPath: string,
|
|
34
|
+
): Record<string, ToolDefinition> {
|
|
35
|
+
const auditDir = path.join(projectPath, '.crewly', 'audit');
|
|
36
|
+
const bugsFile = path.join(auditDir, 'bugs.md');
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
get_team_status: {
|
|
40
|
+
description: 'Get status of all teams and their agents. Returns team names, member statuses, and runtime types.',
|
|
41
|
+
inputSchema: z.object({}),
|
|
42
|
+
execute: async () => {
|
|
43
|
+
const result = await client.get('/teams');
|
|
44
|
+
return result.success ? result.data : { error: result.error };
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
get_agent_logs: {
|
|
49
|
+
description: 'Get recent terminal output from an agent session. Use to inspect agent activity and detect errors.',
|
|
50
|
+
inputSchema: z.object({
|
|
51
|
+
sessionName: z.string().describe('Agent session name'),
|
|
52
|
+
lines: z.number().default(50).describe('Number of lines to retrieve'),
|
|
53
|
+
}),
|
|
54
|
+
execute: async (args) => {
|
|
55
|
+
const { sessionName, lines } = args as { sessionName: string; lines: number };
|
|
56
|
+
const result = await client.get(`/terminal/${sessionName}/output?lines=${lines}`);
|
|
57
|
+
return result.success ? result.data : { error: result.error };
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
get_tasks: {
|
|
62
|
+
description: 'Get all tracked tasks for a project. Use to detect stale, failed, or orphaned tasks.',
|
|
63
|
+
inputSchema: z.object({
|
|
64
|
+
projectPath: z.string().describe('Project path'),
|
|
65
|
+
status: z.string().optional().describe('Filter by status (open, in_progress, done, failed)'),
|
|
66
|
+
}),
|
|
67
|
+
execute: async (args) => {
|
|
68
|
+
// V3-only as of spec 2026-05-06-task-management-v1-deprecation.md.
|
|
69
|
+
// The V3 task-pool is global; `projectPath` is no longer a filter.
|
|
70
|
+
const { status } = args as { projectPath: string; status?: string };
|
|
71
|
+
const query = status ? `?status=${encodeURIComponent(status)}` : '';
|
|
72
|
+
const result = await client.get(`/task-pool/items${query}`);
|
|
73
|
+
return result.success ? result.data : { error: result.error };
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
recall_goals: {
|
|
78
|
+
description: 'Retrieve current project goals, OKRs, and objectives from memory. Use to check goal alignment.',
|
|
79
|
+
inputSchema: z.object({
|
|
80
|
+
context: z.string().default('current project goals OKR objectives priorities').describe('Search context'),
|
|
81
|
+
}),
|
|
82
|
+
execute: async (args) => {
|
|
83
|
+
const { context } = args as { context: string };
|
|
84
|
+
const result = await client.post('/memory/recall', {
|
|
85
|
+
context,
|
|
86
|
+
scope: 'project',
|
|
87
|
+
});
|
|
88
|
+
return result.success ? result.data : { error: result.error };
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
heartbeat: {
|
|
93
|
+
description: 'System health check. Returns teams, projects, and queue status in one call.',
|
|
94
|
+
inputSchema: z.object({}),
|
|
95
|
+
execute: async () => {
|
|
96
|
+
const [teams, projects] = await Promise.all([
|
|
97
|
+
client.get('/teams'),
|
|
98
|
+
client.get('/projects'),
|
|
99
|
+
]);
|
|
100
|
+
return {
|
|
101
|
+
teams: teams.success ? teams.data : { error: teams.error },
|
|
102
|
+
projects: projects.success ? projects.data : { error: projects.error },
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
get_agent_status: {
|
|
109
|
+
description: 'Get the current status of a specific agent by session name.',
|
|
110
|
+
inputSchema: z.object({
|
|
111
|
+
sessionName: z.string().describe('Agent session name to check'),
|
|
112
|
+
}),
|
|
113
|
+
execute: async (args) => {
|
|
114
|
+
const { sessionName } = args as { sessionName: string };
|
|
115
|
+
const result = await client.get('/teams');
|
|
116
|
+
if (!result.success) return { error: result.error };
|
|
117
|
+
const teams = result.data as Array<{ members?: Array<{ sessionName: string }> }>;
|
|
118
|
+
for (const team of teams) {
|
|
119
|
+
const member = team.members?.find(m => m.sessionName === sessionName);
|
|
120
|
+
if (member) return member;
|
|
121
|
+
}
|
|
122
|
+
return { error: `Agent '${sessionName}' not found` };
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
subscribe_event: {
|
|
127
|
+
description: 'Subscribe to agent lifecycle events for monitoring. Read-only observation.',
|
|
128
|
+
inputSchema: z.object({
|
|
129
|
+
eventType: z.string().describe('Event type (e.g., "agent:idle", "agent:busy", "agent:status_changed")'),
|
|
130
|
+
filter: z.record(z.string()).optional().describe('Event filter criteria'),
|
|
131
|
+
oneShot: z.boolean().default(false).describe('Auto-unsubscribe after first event'),
|
|
132
|
+
}),
|
|
133
|
+
execute: async (args) => {
|
|
134
|
+
const { eventType, filter, oneShot } = args as { eventType: string; filter?: Record<string, string>; oneShot: boolean };
|
|
135
|
+
const result = await client.post('/events/subscribe', {
|
|
136
|
+
eventType,
|
|
137
|
+
filter,
|
|
138
|
+
oneShot,
|
|
139
|
+
target: 'crewly-auditor',
|
|
140
|
+
});
|
|
141
|
+
return result.success ? result.data : { error: result.error };
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
write_audit_report: {
|
|
146
|
+
description: 'Append a structured audit finding to the bugs.md file. Use when you detect a problem.',
|
|
147
|
+
inputSchema: z.object({
|
|
148
|
+
severity: z.enum(AUDIT_SEVERITIES).describe('Problem severity level'),
|
|
149
|
+
agents: z.array(z.string()).describe('Session names of involved agents'),
|
|
150
|
+
title: z.string().describe('Short problem title'),
|
|
151
|
+
description: z.string().describe('Detailed problem description'),
|
|
152
|
+
evidence: z.string().describe('Log excerpts or data supporting the finding'),
|
|
153
|
+
suggestion: z.string().describe('Recommended fix or action'),
|
|
154
|
+
}),
|
|
155
|
+
execute: async (args) => {
|
|
156
|
+
const { severity, agents, title, description, evidence, suggestion } =
|
|
157
|
+
args as { severity: string; agents: string[]; title: string; description: string; evidence: string; suggestion: string };
|
|
158
|
+
|
|
159
|
+
const timestamp = new Date().toISOString();
|
|
160
|
+
const severityIcon = severity === 'critical' ? '🔴' : severity === 'high' ? '🟠' : severity === 'medium' ? '🟡' : '🔵';
|
|
161
|
+
|
|
162
|
+
const entry = [
|
|
163
|
+
'',
|
|
164
|
+
`### ${severityIcon} [${severity.toUpperCase()}] ${title}`,
|
|
165
|
+
`**Time:** ${timestamp}`,
|
|
166
|
+
`**Agents:** ${agents.join(', ')}`,
|
|
167
|
+
'',
|
|
168
|
+
description,
|
|
169
|
+
'',
|
|
170
|
+
'**Evidence:**',
|
|
171
|
+
'```',
|
|
172
|
+
evidence,
|
|
173
|
+
'```',
|
|
174
|
+
'',
|
|
175
|
+
`**Suggestion:** ${suggestion}`,
|
|
176
|
+
'',
|
|
177
|
+
'---',
|
|
178
|
+
'',
|
|
179
|
+
].join('\n');
|
|
180
|
+
|
|
181
|
+
// Ensure audit directory exists
|
|
182
|
+
await fsPromises.mkdir(auditDir, { recursive: true });
|
|
183
|
+
|
|
184
|
+
// Append to bugs.md (create with header if new)
|
|
185
|
+
let existing = '';
|
|
186
|
+
try {
|
|
187
|
+
existing = await fsPromises.readFile(bugsFile, 'utf8');
|
|
188
|
+
} catch {
|
|
189
|
+
existing = '# Crewly Audit — Bug Reports\n\nAutomated findings from the Crewly Auditor agent.\n\n---\n';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await fsPromises.writeFile(bugsFile, existing + entry, 'utf8');
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
success: true,
|
|
196
|
+
file: bugsFile,
|
|
197
|
+
severity,
|
|
198
|
+
title,
|
|
199
|
+
timestamp,
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
reply_slack: {
|
|
205
|
+
description: 'Send a message directly to a Slack thread. Use this to respond to user questions when SLACK_CONTEXT is present. Supports Slack mrkdwn formatting.',
|
|
206
|
+
inputSchema: z.object({
|
|
207
|
+
text: z.string().describe('Message text in Slack mrkdwn format'),
|
|
208
|
+
channelId: z.string().describe('Slack channel ID from SLACK_CONTEXT'),
|
|
209
|
+
threadTs: z.string().describe('Slack thread timestamp from SLACK_CONTEXT'),
|
|
210
|
+
}),
|
|
211
|
+
execute: async (args) => {
|
|
212
|
+
const { text, channelId, threadTs } = args as { text: string; channelId: string; threadTs: string };
|
|
213
|
+
// #181: Convert markdown to Slack mrkdwn format
|
|
214
|
+
const slackText = convertMarkdownToSlackMrkdwn(text);
|
|
215
|
+
const result = await client.post('/slack/send', { channelId, text: slackText, threadTs });
|
|
216
|
+
return result.success ? { sent: true } : { error: result.error };
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
create_github_issue: {
|
|
221
|
+
description: 'Create a GitHub issue for a bug or enhancement found during audit (#178). Deduplicates: checks existing open issues before creating. Requires gh CLI to be available.',
|
|
222
|
+
inputSchema: z.object({
|
|
223
|
+
title: z.string().describe('Issue title (include severity prefix like [Critical], [High], [Medium])'),
|
|
224
|
+
body: z.string().describe('Issue body in markdown format'),
|
|
225
|
+
labels: z.array(z.string()).default(['bug']).describe('GitHub labels (e.g., bug, enhancement)'),
|
|
226
|
+
}),
|
|
227
|
+
execute: async (args) => {
|
|
228
|
+
const { title, body, labels } = args as { title: string; body: string; labels: string[] };
|
|
229
|
+
const { execSync } = await import('child_process');
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
// Dedup: search for existing open issues with similar title
|
|
233
|
+
const searchQuery = title.replace(/\[.*?\]\s*/, '').slice(0, 50);
|
|
234
|
+
let existingIssues = '';
|
|
235
|
+
try {
|
|
236
|
+
existingIssues = execSync(
|
|
237
|
+
`gh issue list --state open --search "${searchQuery.replace(/"/g, '\\"')}" --json number,title --limit 5`,
|
|
238
|
+
{ encoding: 'utf-8', timeout: 15000 }
|
|
239
|
+
);
|
|
240
|
+
} catch { /* gh not available or no repo */ }
|
|
241
|
+
|
|
242
|
+
if (existingIssues) {
|
|
243
|
+
const issues = JSON.parse(existingIssues);
|
|
244
|
+
const duplicate = issues.find((i: { title: string }) =>
|
|
245
|
+
i.title.toLowerCase().includes(searchQuery.toLowerCase().slice(0, 30))
|
|
246
|
+
);
|
|
247
|
+
if (duplicate) {
|
|
248
|
+
return {
|
|
249
|
+
success: true,
|
|
250
|
+
created: false,
|
|
251
|
+
duplicate: true,
|
|
252
|
+
existingIssue: `#${duplicate.number}: ${duplicate.title}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Create the issue
|
|
258
|
+
const labelFlag = labels.map(l => `--label "${l}"`).join(' ');
|
|
259
|
+
const result = execSync(
|
|
260
|
+
`gh issue create --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}" ${labelFlag}`,
|
|
261
|
+
{ encoding: 'utf-8', timeout: 30000 }
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const issueUrl = result.trim();
|
|
265
|
+
return { success: true, created: true, url: issueUrl, title };
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
error: error instanceof Error ? error.message : String(error),
|
|
270
|
+
note: 'GitHub CLI (gh) may not be installed or authenticated',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
read_audit_history: {
|
|
277
|
+
description: 'Read previous audit findings from bugs.md. Use to avoid duplicate reports.',
|
|
278
|
+
inputSchema: z.object({
|
|
279
|
+
lastN: z.number().default(10).describe('Number of recent findings to return'),
|
|
280
|
+
}),
|
|
281
|
+
execute: async (args) => {
|
|
282
|
+
const { lastN } = args as { lastN: number };
|
|
283
|
+
try {
|
|
284
|
+
const content = await fsPromises.readFile(bugsFile, 'utf8');
|
|
285
|
+
// Split on --- separator, take last N entries
|
|
286
|
+
const entries = content.split('---').filter(e => e.trim());
|
|
287
|
+
const recent = entries.slice(-lastN);
|
|
288
|
+
return {
|
|
289
|
+
totalFindings: entries.length - 1, // subtract header
|
|
290
|
+
recentFindings: recent.join('---'),
|
|
291
|
+
};
|
|
292
|
+
} catch {
|
|
293
|
+
return { totalFindings: 0, recentFindings: 'No audit history found.' };
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get the list of auditor tool names.
|
|
302
|
+
*
|
|
303
|
+
* @returns Array of auditor tool name strings
|
|
304
|
+
*/
|
|
305
|
+
export function getAuditorToolNames(): string[] {
|
|
306
|
+
return [
|
|
307
|
+
'get_team_status', 'get_agent_logs', 'get_tasks', 'recall_goals',
|
|
308
|
+
'heartbeat', 'get_agent_status', 'subscribe_event',
|
|
309
|
+
'write_audit_report', 'reply_slack', 'read_audit_history', 'create_github_issue',
|
|
310
|
+
];
|
|
311
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud config helper for tools that need to talk to api.crewlyai.com.
|
|
3
|
+
*
|
|
4
|
+
* Reads `~/.crewly/cloud/config.json` (the same file the OSS server writes
|
|
5
|
+
* during `crewly login`). Each call re-reads from disk so token refreshes are
|
|
6
|
+
* picked up without process restart.
|
|
7
|
+
*
|
|
8
|
+
* @module services/agent/crewly-agent/cloud-config
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { promises as fs } from 'fs';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
|
|
15
|
+
export interface CloudConfig {
|
|
16
|
+
cloudUrl: string;
|
|
17
|
+
token: string;
|
|
18
|
+
tier?: string;
|
|
19
|
+
refreshToken?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const CLOUD_CONFIG_PATH = path.join(homedir(), '.crewly', 'cloud', 'config.json');
|
|
23
|
+
|
|
24
|
+
export class CloudNotLoggedInError extends Error {
|
|
25
|
+
constructor() {
|
|
26
|
+
super(
|
|
27
|
+
`Crewly Cloud is not connected. Run \`crewly login\` (or open the Crewly desktop app and sign in) to enable cloud-backed tools.`,
|
|
28
|
+
);
|
|
29
|
+
this.name = 'CloudNotLoggedInError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function loadCloudConfig(filePath: string = CLOUD_CONFIG_PATH): Promise<CloudConfig> {
|
|
34
|
+
let raw: string;
|
|
35
|
+
try {
|
|
36
|
+
raw = await fs.readFile(filePath, 'utf-8');
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
39
|
+
throw new CloudNotLoggedInError();
|
|
40
|
+
}
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let parsed: unknown;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(raw);
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error(`Cloud config at ${filePath} is not valid JSON.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
!parsed ||
|
|
53
|
+
typeof parsed !== 'object' ||
|
|
54
|
+
typeof (parsed as Record<string, unknown>)['cloudUrl'] !== 'string' ||
|
|
55
|
+
typeof (parsed as Record<string, unknown>)['token'] !== 'string'
|
|
56
|
+
) {
|
|
57
|
+
throw new CloudNotLoggedInError();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const cfg = parsed as Record<string, unknown>;
|
|
61
|
+
return {
|
|
62
|
+
cloudUrl: (cfg['cloudUrl'] as string).replace(/\/$/, ''),
|
|
63
|
+
token: cfg['token'] as string,
|
|
64
|
+
tier: typeof cfg['tier'] === 'string' ? (cfg['tier'] as string) : undefined,
|
|
65
|
+
refreshToken: typeof cfg['refreshToken'] === 'string' ? (cfg['refreshToken'] as string) : undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for DeepSeek SSE Transform.
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* - parseSseBlock: well-formed reasoning chunks, plain content (no reasoning),
|
|
6
|
+
* `[DONE]` sentinel, malformed JSON resilience, multi-line blocks.
|
|
7
|
+
* - teeAndParse: passthrough body identical to source, reasoning accumulation
|
|
8
|
+
* across chunks, parser error isolation from consumer.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { parseSseBlock, teeAndParse } from './deepseek-sse-transform.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper to build a ReadableStream<Uint8Array> from a list of string chunks.
|
|
16
|
+
* Each chunk becomes one stream queue entry — useful to simulate SSE arriving
|
|
17
|
+
* in arbitrary network packet boundaries.
|
|
18
|
+
*/
|
|
19
|
+
function streamOf(chunks: string[]): ReadableStream<Uint8Array> {
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
let i = 0;
|
|
22
|
+
return new ReadableStream<Uint8Array>({
|
|
23
|
+
pull(controller) {
|
|
24
|
+
if (i >= chunks.length) {
|
|
25
|
+
controller.close();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
controller.enqueue(encoder.encode(chunks[i]!));
|
|
29
|
+
i++;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Drain a ReadableStream<Uint8Array> into a single decoded string.
|
|
36
|
+
*/
|
|
37
|
+
async function drain(stream: ReadableStream<Uint8Array>): Promise<string> {
|
|
38
|
+
const reader = stream.getReader();
|
|
39
|
+
const decoder = new TextDecoder();
|
|
40
|
+
let out = '';
|
|
41
|
+
while (true) {
|
|
42
|
+
const { done, value } = await reader.read();
|
|
43
|
+
if (done) break;
|
|
44
|
+
out += decoder.decode(value, { stream: true });
|
|
45
|
+
}
|
|
46
|
+
out += decoder.decode();
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('parseSseBlock', () => {
|
|
51
|
+
it('extracts reasoning_content from a single delta chunk', () => {
|
|
52
|
+
const block = 'data: {"choices":[{"delta":{"reasoning_content":"Let me think..."}}]}';
|
|
53
|
+
expect(parseSseBlock(block)).toBe('Let me think...');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns empty string when delta has only content (no reasoning)', () => {
|
|
57
|
+
const block = 'data: {"choices":[{"delta":{"content":"answer"}}]}';
|
|
58
|
+
expect(parseSseBlock(block)).toBe('');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns null for [DONE] sentinel', () => {
|
|
62
|
+
expect(parseSseBlock('data: [DONE]')).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('handles multi-line block with multiple data: lines', () => {
|
|
66
|
+
const block = [
|
|
67
|
+
'data: {"choices":[{"delta":{"reasoning_content":"step 1 "}}]}',
|
|
68
|
+
'data: {"choices":[{"delta":{"reasoning_content":"step 2"}}]}',
|
|
69
|
+
].join('\n');
|
|
70
|
+
expect(parseSseBlock(block)).toBe('step 1 step 2');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('skips malformed JSON without throwing', () => {
|
|
74
|
+
const block = 'data: {not valid json';
|
|
75
|
+
expect(parseSseBlock(block)).toBe('');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('skips non-data: lines (e.g. event:, id:)', () => {
|
|
79
|
+
const block = [
|
|
80
|
+
'id: abc',
|
|
81
|
+
'event: chunk',
|
|
82
|
+
'data: {"choices":[{"delta":{"reasoning_content":"x"}}]}',
|
|
83
|
+
].join('\n');
|
|
84
|
+
expect(parseSseBlock(block)).toBe('x');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns empty string for empty data payload', () => {
|
|
88
|
+
expect(parseSseBlock('data:')).toBe('');
|
|
89
|
+
expect(parseSseBlock('data: ')).toBe('');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('handles delta with both content and reasoning_content (returns only reasoning)', () => {
|
|
93
|
+
const block =
|
|
94
|
+
'data: {"choices":[{"delta":{"content":"answer","reasoning_content":"thought"}}]}';
|
|
95
|
+
expect(parseSseBlock(block)).toBe('thought');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('teeAndParse', () => {
|
|
100
|
+
it('passthroughBody yields exactly the source bytes', async () => {
|
|
101
|
+
const source = [
|
|
102
|
+
'data: {"choices":[{"delta":{"reasoning_content":"r1"}}]}\n\n',
|
|
103
|
+
'data: {"choices":[{"delta":{"content":"hello"}}]}\n\n',
|
|
104
|
+
'data: [DONE]\n\n',
|
|
105
|
+
];
|
|
106
|
+
const expected = source.join('');
|
|
107
|
+
const stream = streamOf(source);
|
|
108
|
+
const parsed = teeAndParse(stream);
|
|
109
|
+
const got = await drain(parsed.passthroughBody);
|
|
110
|
+
expect(got).toBe(expected);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('accumulates reasoning_content across multiple chunks', async () => {
|
|
114
|
+
const source = [
|
|
115
|
+
'data: {"choices":[{"delta":{"reasoning_content":"alpha "}}]}\n\n',
|
|
116
|
+
'data: {"choices":[{"delta":{"reasoning_content":"beta "}}]}\n\n',
|
|
117
|
+
'data: {"choices":[{"delta":{"reasoning_content":"gamma"}}]}\n\n',
|
|
118
|
+
'data: [DONE]\n\n',
|
|
119
|
+
];
|
|
120
|
+
const stream = streamOf(source);
|
|
121
|
+
const parsed = teeAndParse(stream);
|
|
122
|
+
await drain(parsed.passthroughBody);
|
|
123
|
+
// Wait one microtask cycle for parser branch to finish draining.
|
|
124
|
+
// Parser branch runs in the same event loop but completes async; the
|
|
125
|
+
// consumer drain above ensures backpressure has cleared.
|
|
126
|
+
await new Promise((r) => setImmediate(r));
|
|
127
|
+
expect(parsed.getReasoning()).toBe('alpha beta gamma');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('handles SSE chunks split across packet boundaries', async () => {
|
|
131
|
+
// Simulate a single SSE event arriving in two TCP packets.
|
|
132
|
+
const source = [
|
|
133
|
+
'data: {"choices":[{"delta":{"reaso',
|
|
134
|
+
'ning_content":"split"}}]}\n\n',
|
|
135
|
+
'data: [DONE]\n\n',
|
|
136
|
+
];
|
|
137
|
+
const stream = streamOf(source);
|
|
138
|
+
const parsed = teeAndParse(stream);
|
|
139
|
+
await drain(parsed.passthroughBody);
|
|
140
|
+
await new Promise((r) => setImmediate(r));
|
|
141
|
+
expect(parsed.getReasoning()).toBe('split');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('isDrained becomes true after [DONE] sentinel is parsed', async () => {
|
|
145
|
+
const source = ['data: {"choices":[{"delta":{"content":"x"}}]}\n\n', 'data: [DONE]\n\n'];
|
|
146
|
+
const stream = streamOf(source);
|
|
147
|
+
const parsed = teeAndParse(stream);
|
|
148
|
+
expect(parsed.isDrained()).toBe(false);
|
|
149
|
+
await drain(parsed.passthroughBody);
|
|
150
|
+
await new Promise((r) => setImmediate(r));
|
|
151
|
+
expect(parsed.isDrained()).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('returns empty reasoning when stream contains no reasoning_content', async () => {
|
|
155
|
+
const source = [
|
|
156
|
+
'data: {"choices":[{"delta":{"content":"plain answer"}}]}\n\n',
|
|
157
|
+
'data: [DONE]\n\n',
|
|
158
|
+
];
|
|
159
|
+
const stream = streamOf(source);
|
|
160
|
+
const parsed = teeAndParse(stream);
|
|
161
|
+
await drain(parsed.passthroughBody);
|
|
162
|
+
await new Promise((r) => setImmediate(r));
|
|
163
|
+
expect(parsed.getReasoning()).toBe('');
|
|
164
|
+
});
|
|
165
|
+
});
|