@posthog/agent 1.22.0 → 1.24.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/CLAUDE.md +3 -3
- package/README.md +3 -3
- package/dist/index.d.ts +11 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/src/adapters/claude/claude-adapter.d.ts +3 -3
- package/dist/src/adapters/claude/claude-adapter.d.ts.map +1 -1
- package/dist/src/adapters/claude/claude-adapter.js +156 -111
- package/dist/src/adapters/claude/claude-adapter.js.map +1 -1
- package/dist/src/adapters/claude/tool-mapper.d.ts +1 -1
- package/dist/src/adapters/claude/tool-mapper.d.ts.map +1 -1
- package/dist/src/adapters/claude/tool-mapper.js.map +1 -1
- package/dist/src/adapters/types.d.ts +1 -1
- package/dist/src/adapters/types.d.ts.map +1 -1
- package/dist/src/agent.d.ts +7 -7
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +143 -85
- package/dist/src/agent.js.map +1 -1
- package/dist/src/agents/execution.js.map +1 -1
- package/dist/src/agents/planning.js.map +1 -1
- package/dist/src/agents/research.js.map +1 -1
- package/dist/src/file-manager.d.ts +4 -4
- package/dist/src/file-manager.d.ts.map +1 -1
- package/dist/src/file-manager.js +59 -58
- package/dist/src/file-manager.js.map +1 -1
- package/dist/src/git-manager.d.ts +1 -1
- package/dist/src/git-manager.d.ts.map +1 -1
- package/dist/src/git-manager.js +93 -69
- package/dist/src/git-manager.js.map +1 -1
- package/dist/src/posthog-api.d.ts +2 -3
- package/dist/src/posthog-api.d.ts.map +1 -1
- package/dist/src/posthog-api.js +22 -22
- package/dist/src/posthog-api.js.map +1 -1
- package/dist/src/prompt-builder.d.ts +3 -3
- package/dist/src/prompt-builder.d.ts.map +1 -1
- package/dist/src/prompt-builder.js +123 -93
- package/dist/src/prompt-builder.js.map +1 -1
- package/dist/src/task-manager.d.ts +4 -4
- package/dist/src/task-manager.d.ts.map +1 -1
- package/dist/src/task-manager.js +19 -18
- package/dist/src/task-manager.js.map +1 -1
- package/dist/src/task-progress-reporter.d.ts +3 -4
- package/dist/src/task-progress-reporter.d.ts.map +1 -1
- package/dist/src/task-progress-reporter.js +59 -54
- package/dist/src/task-progress-reporter.js.map +1 -1
- package/dist/src/template-manager.d.ts +1 -1
- package/dist/src/template-manager.d.ts.map +1 -1
- package/dist/src/template-manager.js +30 -28
- package/dist/src/template-manager.js.map +1 -1
- package/dist/src/todo-manager.d.ts +3 -3
- package/dist/src/todo-manager.d.ts.map +1 -1
- package/dist/src/todo-manager.js +29 -24
- package/dist/src/todo-manager.js.map +1 -1
- package/dist/src/tools/registry.d.ts +1 -1
- package/dist/src/tools/registry.js +60 -60
- package/dist/src/tools/registry.js.map +1 -1
- package/dist/src/tools/types.d.ts +31 -31
- package/dist/src/types.d.ts +33 -33
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/logger.d.ts +4 -4
- package/dist/src/utils/logger.d.ts.map +1 -1
- package/dist/src/utils/logger.js +8 -8
- package/dist/src/utils/logger.js.map +1 -1
- package/dist/src/workflow/config.d.ts +1 -1
- package/dist/src/workflow/config.d.ts.map +1 -1
- package/dist/src/workflow/config.js +18 -18
- package/dist/src/workflow/config.js.map +1 -1
- package/dist/src/workflow/steps/build.d.ts +1 -1
- package/dist/src/workflow/steps/build.d.ts.map +1 -1
- package/dist/src/workflow/steps/build.js +46 -38
- package/dist/src/workflow/steps/build.js.map +1 -1
- package/dist/src/workflow/steps/finalize.d.ts +1 -1
- package/dist/src/workflow/steps/finalize.d.ts.map +1 -1
- package/dist/src/workflow/steps/finalize.js +54 -48
- package/dist/src/workflow/steps/finalize.js.map +1 -1
- package/dist/src/workflow/steps/plan.d.ts +1 -1
- package/dist/src/workflow/steps/plan.d.ts.map +1 -1
- package/dist/src/workflow/steps/plan.js +58 -46
- package/dist/src/workflow/steps/plan.js.map +1 -1
- package/dist/src/workflow/steps/research.d.ts +1 -1
- package/dist/src/workflow/steps/research.d.ts.map +1 -1
- package/dist/src/workflow/steps/research.js +68 -56
- package/dist/src/workflow/steps/research.js.map +1 -1
- package/dist/src/workflow/types.d.ts +12 -12
- package/dist/src/workflow/types.d.ts.map +1 -1
- package/dist/src/workflow/utils.d.ts +1 -1
- package/dist/src/workflow/utils.d.ts.map +1 -1
- package/dist/src/workflow/utils.js +7 -4
- package/dist/src/workflow/utils.js.map +1 -1
- package/package.json +8 -8
- package/src/adapters/claude/claude-adapter.ts +220 -168
- package/src/adapters/claude/tool-mapper.ts +2 -2
- package/src/adapters/types.ts +1 -1
- package/src/agent.ts +579 -444
- package/src/agents/execution.ts +1 -1
- package/src/agents/planning.ts +1 -1
- package/src/agents/research.ts +0 -1
- package/src/file-manager.ts +64 -63
- package/src/git-manager.ts +152 -87
- package/src/posthog-api.ts +122 -82
- package/src/prompt-builder.ts +180 -135
- package/src/task-manager.ts +38 -30
- package/src/task-progress-reporter.ts +70 -59
- package/src/template-manager.ts +98 -45
- package/src/todo-manager.ts +35 -30
- package/src/tools/registry.ts +62 -62
- package/src/tools/types.ts +36 -36
- package/src/types.ts +93 -71
- package/src/utils/logger.ts +62 -56
- package/src/workflow/config.ts +48 -48
- package/src/workflow/steps/build.ts +122 -113
- package/src/workflow/steps/finalize.ts +214 -182
- package/src/workflow/steps/plan.ts +151 -131
- package/src/workflow/steps/research.ts +205 -186
- package/src/workflow/types.ts +38 -36
- package/src/workflow/utils.ts +37 -34
- package/LICENSE +0 -33
package/src/posthog-api.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
Task,
|
|
3
|
-
TaskRun,
|
|
4
2
|
LogEntry,
|
|
5
|
-
SupportingFile,
|
|
6
3
|
PostHogAPIConfig,
|
|
7
4
|
PostHogResource,
|
|
8
|
-
|
|
9
|
-
UrlMention,
|
|
10
|
-
TaskRunArtifact,
|
|
5
|
+
Task,
|
|
11
6
|
TaskArtifactUploadPayload,
|
|
12
|
-
|
|
7
|
+
TaskRun,
|
|
8
|
+
TaskRunArtifact,
|
|
9
|
+
UrlMention,
|
|
10
|
+
} from "./types.js";
|
|
13
11
|
|
|
14
12
|
interface PostHogApiResponse<T> {
|
|
15
13
|
results?: T[];
|
|
@@ -29,32 +27,31 @@ export interface TaskRunUpdate {
|
|
|
29
27
|
|
|
30
28
|
export class PostHogAPIClient {
|
|
31
29
|
private config: PostHogAPIConfig;
|
|
32
|
-
private _teamId: number | null = null;
|
|
33
30
|
|
|
34
31
|
constructor(config: PostHogAPIConfig) {
|
|
35
32
|
this.config = config;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
private get baseUrl(): string {
|
|
39
|
-
const host = this.config.apiUrl.endsWith("/")
|
|
40
|
-
? this.config.apiUrl.slice(0, -1)
|
|
36
|
+
const host = this.config.apiUrl.endsWith("/")
|
|
37
|
+
? this.config.apiUrl.slice(0, -1)
|
|
41
38
|
: this.config.apiUrl;
|
|
42
39
|
return host;
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
private get headers(): Record<string, string> {
|
|
46
43
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
45
|
+
"Content-Type": "application/json",
|
|
49
46
|
};
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
private async apiRequest<T>(
|
|
53
|
-
endpoint: string,
|
|
54
|
-
options: RequestInit = {}
|
|
50
|
+
endpoint: string,
|
|
51
|
+
options: RequestInit = {},
|
|
55
52
|
): Promise<T> {
|
|
56
53
|
const url = `${this.baseUrl}${endpoint}`;
|
|
57
|
-
|
|
54
|
+
|
|
58
55
|
const response = await fetch(url, {
|
|
59
56
|
...options,
|
|
60
57
|
headers: {
|
|
@@ -106,7 +103,7 @@ export class PostHogAPIClient {
|
|
|
106
103
|
}): Promise<Task[]> {
|
|
107
104
|
const teamId = this.getTeamId();
|
|
108
105
|
const url = new URL(`${this.baseUrl}/api/projects/${teamId}/tasks/`);
|
|
109
|
-
|
|
106
|
+
|
|
110
107
|
if (filters) {
|
|
111
108
|
Object.entries(filters).forEach(([key, value]) => {
|
|
112
109
|
if (value) url.searchParams.append(key, value);
|
|
@@ -114,16 +111,16 @@ export class PostHogAPIClient {
|
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
const response = await this.apiRequest<PostHogApiResponse<Task>>(
|
|
117
|
-
url.pathname + url.search
|
|
114
|
+
url.pathname + url.search,
|
|
118
115
|
);
|
|
119
|
-
|
|
116
|
+
|
|
120
117
|
return response.results || [];
|
|
121
118
|
}
|
|
122
119
|
|
|
123
120
|
async updateTask(taskId: string, updates: Partial<Task>): Promise<Task> {
|
|
124
121
|
const teamId = this.getTeamId();
|
|
125
122
|
return this.apiRequest<Task>(`/api/projects/${teamId}/tasks/${taskId}/`, {
|
|
126
|
-
method:
|
|
123
|
+
method: "PATCH",
|
|
127
124
|
body: JSON.stringify(updates),
|
|
128
125
|
});
|
|
129
126
|
}
|
|
@@ -132,59 +129,86 @@ export class PostHogAPIClient {
|
|
|
132
129
|
async listTaskRuns(taskId: string): Promise<TaskRun[]> {
|
|
133
130
|
const teamId = this.getTeamId();
|
|
134
131
|
const response = await this.apiRequest<PostHogApiResponse<TaskRun>>(
|
|
135
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs
|
|
132
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/`,
|
|
136
133
|
);
|
|
137
134
|
return response.results || [];
|
|
138
135
|
}
|
|
139
136
|
|
|
140
137
|
async getTaskRun(taskId: string, runId: string): Promise<TaskRun> {
|
|
141
138
|
const teamId = this.getTeamId();
|
|
142
|
-
return this.apiRequest<TaskRun>(
|
|
139
|
+
return this.apiRequest<TaskRun>(
|
|
140
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`,
|
|
141
|
+
);
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
async createTaskRun(
|
|
146
145
|
taskId: string,
|
|
147
|
-
payload?: Partial<
|
|
146
|
+
payload?: Partial<
|
|
147
|
+
Omit<
|
|
148
|
+
TaskRun,
|
|
149
|
+
"id" | "task" | "team" | "created_at" | "updated_at" | "completed_at"
|
|
150
|
+
>
|
|
151
|
+
>,
|
|
148
152
|
): Promise<TaskRun> {
|
|
149
153
|
const teamId = this.getTeamId();
|
|
150
|
-
return this.apiRequest<TaskRun>(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
return this.apiRequest<TaskRun>(
|
|
155
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/`,
|
|
156
|
+
{
|
|
157
|
+
method: "POST",
|
|
158
|
+
body: JSON.stringify(payload || {}),
|
|
159
|
+
},
|
|
160
|
+
);
|
|
154
161
|
}
|
|
155
162
|
|
|
156
163
|
async updateTaskRun(
|
|
157
164
|
taskId: string,
|
|
158
165
|
runId: string,
|
|
159
|
-
payload: TaskRunUpdate
|
|
166
|
+
payload: TaskRunUpdate,
|
|
160
167
|
): Promise<TaskRun> {
|
|
161
168
|
const teamId = this.getTeamId();
|
|
162
|
-
return this.apiRequest<TaskRun>(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
return this.apiRequest<TaskRun>(
|
|
170
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`,
|
|
171
|
+
{
|
|
172
|
+
method: "PATCH",
|
|
173
|
+
body: JSON.stringify(payload),
|
|
174
|
+
},
|
|
175
|
+
);
|
|
166
176
|
}
|
|
167
177
|
|
|
168
|
-
async setTaskRunOutput(
|
|
178
|
+
async setTaskRunOutput(
|
|
179
|
+
taskId: string,
|
|
180
|
+
runId: string,
|
|
181
|
+
output: Record<string, unknown>,
|
|
182
|
+
): Promise<TaskRun> {
|
|
169
183
|
const teamId = this.getTeamId();
|
|
170
|
-
return this.apiRequest<TaskRun>(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
return this.apiRequest<TaskRun>(
|
|
185
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/set_output/`,
|
|
186
|
+
{
|
|
187
|
+
method: "PATCH",
|
|
188
|
+
body: JSON.stringify({ output }),
|
|
189
|
+
},
|
|
190
|
+
);
|
|
174
191
|
}
|
|
175
192
|
|
|
176
|
-
async appendTaskRunLog(
|
|
193
|
+
async appendTaskRunLog(
|
|
194
|
+
taskId: string,
|
|
195
|
+
runId: string,
|
|
196
|
+
entries: LogEntry[],
|
|
197
|
+
): Promise<TaskRun> {
|
|
177
198
|
const teamId = this.getTeamId();
|
|
178
|
-
return this.apiRequest<TaskRun>(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
199
|
+
return this.apiRequest<TaskRun>(
|
|
200
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/append_log/`,
|
|
201
|
+
{
|
|
202
|
+
method: "POST",
|
|
203
|
+
body: JSON.stringify({ entries }),
|
|
204
|
+
},
|
|
205
|
+
);
|
|
182
206
|
}
|
|
183
207
|
|
|
184
208
|
async uploadTaskArtifacts(
|
|
185
209
|
taskId: string,
|
|
186
210
|
runId: string,
|
|
187
|
-
artifacts: TaskArtifactUploadPayload[]
|
|
211
|
+
artifacts: TaskArtifactUploadPayload[],
|
|
188
212
|
): Promise<TaskRunArtifact[]> {
|
|
189
213
|
if (!artifacts.length) {
|
|
190
214
|
return [];
|
|
@@ -194,9 +218,9 @@ export class PostHogAPIClient {
|
|
|
194
218
|
const response = await this.apiRequest<{ artifacts: TaskRunArtifact[] }>(
|
|
195
219
|
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/`,
|
|
196
220
|
{
|
|
197
|
-
method:
|
|
221
|
+
method: "POST",
|
|
198
222
|
body: JSON.stringify({ artifacts }),
|
|
199
|
-
}
|
|
223
|
+
},
|
|
200
224
|
);
|
|
201
225
|
|
|
202
226
|
return response.artifacts ?? [];
|
|
@@ -214,13 +238,15 @@ export class PostHogAPIClient {
|
|
|
214
238
|
|
|
215
239
|
try {
|
|
216
240
|
const response = await fetch(taskRun.log_url);
|
|
217
|
-
|
|
241
|
+
|
|
218
242
|
if (!response.ok) {
|
|
219
|
-
throw new Error(
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Failed to fetch logs: ${response.status} ${response.statusText}`,
|
|
245
|
+
);
|
|
220
246
|
}
|
|
221
247
|
|
|
222
248
|
const content = await response.text();
|
|
223
|
-
|
|
249
|
+
|
|
224
250
|
if (!content.trim()) {
|
|
225
251
|
return [];
|
|
226
252
|
}
|
|
@@ -228,30 +254,37 @@ export class PostHogAPIClient {
|
|
|
228
254
|
// Parse newline-delimited JSON
|
|
229
255
|
return content
|
|
230
256
|
.trim()
|
|
231
|
-
.split(
|
|
232
|
-
.map(line => JSON.parse(line) as LogEntry);
|
|
257
|
+
.split("\n")
|
|
258
|
+
.map((line) => JSON.parse(line) as LogEntry);
|
|
233
259
|
} catch (error) {
|
|
234
|
-
throw new Error(
|
|
260
|
+
throw new Error(
|
|
261
|
+
`Failed to fetch task run logs: ${error instanceof Error ? error.message : String(error)}`,
|
|
262
|
+
);
|
|
235
263
|
}
|
|
236
264
|
}
|
|
237
265
|
|
|
238
266
|
/**
|
|
239
267
|
* Fetch error details from PostHog error tracking
|
|
240
268
|
*/
|
|
241
|
-
async fetchErrorDetails(
|
|
242
|
-
|
|
243
|
-
|
|
269
|
+
async fetchErrorDetails(
|
|
270
|
+
errorId: string,
|
|
271
|
+
projectId?: string,
|
|
272
|
+
): Promise<PostHogResource> {
|
|
273
|
+
const teamId = projectId ? parseInt(projectId, 10) : this.getTeamId();
|
|
274
|
+
|
|
244
275
|
try {
|
|
245
|
-
const errorData = await this.apiRequest<any>(
|
|
246
|
-
|
|
276
|
+
const errorData = await this.apiRequest<any>(
|
|
277
|
+
`/api/projects/${teamId}/error_tracking/${errorId}/`,
|
|
278
|
+
);
|
|
279
|
+
|
|
247
280
|
// Format error details for agent consumption
|
|
248
281
|
const content = this.formatErrorContent(errorData);
|
|
249
|
-
|
|
282
|
+
|
|
250
283
|
return {
|
|
251
|
-
type:
|
|
284
|
+
type: "error",
|
|
252
285
|
id: errorId,
|
|
253
286
|
url: `${this.baseUrl}/project/${teamId}/error_tracking/${errorId}`,
|
|
254
|
-
title: errorData.exception_type ||
|
|
287
|
+
title: errorData.exception_type || "Unknown Error",
|
|
255
288
|
content,
|
|
256
289
|
metadata: {
|
|
257
290
|
exception_type: errorData.exception_type,
|
|
@@ -271,9 +304,9 @@ export class PostHogAPIClient {
|
|
|
271
304
|
*/
|
|
272
305
|
async fetchResourceByUrl(urlMention: UrlMention): Promise<PostHogResource> {
|
|
273
306
|
switch (urlMention.type) {
|
|
274
|
-
case
|
|
307
|
+
case "error": {
|
|
275
308
|
if (!urlMention.id) {
|
|
276
|
-
throw new Error(
|
|
309
|
+
throw new Error("Error ID is required for error resources");
|
|
277
310
|
}
|
|
278
311
|
// Extract project ID from URL if available, otherwise use default team
|
|
279
312
|
let projectId: string | undefined;
|
|
@@ -282,23 +315,26 @@ export class PostHogAPIClient {
|
|
|
282
315
|
projectId = projectIdMatch ? projectIdMatch[1] : undefined;
|
|
283
316
|
}
|
|
284
317
|
return this.fetchErrorDetails(urlMention.id, projectId);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
case
|
|
288
|
-
case
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
case "experiment":
|
|
321
|
+
case "insight":
|
|
322
|
+
case "feature_flag":
|
|
323
|
+
throw new Error(
|
|
324
|
+
`Resource type '${urlMention.type}' not yet implemented`,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
case "generic":
|
|
292
328
|
// Return a minimal resource for generic URLs
|
|
293
329
|
return {
|
|
294
|
-
type:
|
|
295
|
-
id:
|
|
330
|
+
type: "generic",
|
|
331
|
+
id: "",
|
|
296
332
|
url: urlMention.url,
|
|
297
|
-
title:
|
|
333
|
+
title: "Generic Resource",
|
|
298
334
|
content: `Generic resource: ${urlMention.url}`,
|
|
299
335
|
metadata: {},
|
|
300
336
|
};
|
|
301
|
-
|
|
337
|
+
|
|
302
338
|
default:
|
|
303
339
|
throw new Error(`Unknown resource type: ${urlMention.type}`);
|
|
304
340
|
}
|
|
@@ -309,36 +345,40 @@ export class PostHogAPIClient {
|
|
|
309
345
|
*/
|
|
310
346
|
private formatErrorContent(errorData: any): string {
|
|
311
347
|
const sections = [];
|
|
312
|
-
|
|
348
|
+
|
|
313
349
|
if (errorData.exception_type) {
|
|
314
350
|
sections.push(`**Error Type**: ${errorData.exception_type}`);
|
|
315
351
|
}
|
|
316
|
-
|
|
352
|
+
|
|
317
353
|
if (errorData.exception_message) {
|
|
318
354
|
sections.push(`**Message**: ${errorData.exception_message}`);
|
|
319
355
|
}
|
|
320
|
-
|
|
356
|
+
|
|
321
357
|
if (errorData.stack_trace) {
|
|
322
|
-
sections.push(
|
|
358
|
+
sections.push(
|
|
359
|
+
`**Stack Trace**:\n\`\`\`\n${errorData.stack_trace}\n\`\`\``,
|
|
360
|
+
);
|
|
323
361
|
}
|
|
324
|
-
|
|
362
|
+
|
|
325
363
|
if (errorData.volume) {
|
|
326
364
|
sections.push(`**Volume**: ${errorData.volume} occurrences`);
|
|
327
365
|
}
|
|
328
|
-
|
|
366
|
+
|
|
329
367
|
if (errorData.users_affected) {
|
|
330
368
|
sections.push(`**Users Affected**: ${errorData.users_affected}`);
|
|
331
369
|
}
|
|
332
|
-
|
|
370
|
+
|
|
333
371
|
if (errorData.first_seen && errorData.last_seen) {
|
|
334
372
|
sections.push(`**First Seen**: ${errorData.first_seen}`);
|
|
335
373
|
sections.push(`**Last Seen**: ${errorData.last_seen}`);
|
|
336
374
|
}
|
|
337
|
-
|
|
375
|
+
|
|
338
376
|
if (errorData.properties && Object.keys(errorData.properties).length > 0) {
|
|
339
|
-
sections.push(
|
|
377
|
+
sections.push(
|
|
378
|
+
`**Properties**: ${JSON.stringify(errorData.properties, null, 2)}`,
|
|
379
|
+
);
|
|
340
380
|
}
|
|
341
|
-
|
|
342
|
-
return sections.join(
|
|
381
|
+
|
|
382
|
+
return sections.join("\n\n");
|
|
343
383
|
}
|
|
344
384
|
}
|