@getpaseo/server 0.1.94 → 0.1.95
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.
|
@@ -34,6 +34,9 @@ export interface SessionRuntimeMetrics {
|
|
|
34
34
|
inflightRequests: number;
|
|
35
35
|
peakInflightRequests: number;
|
|
36
36
|
}
|
|
37
|
+
export interface SessionFileSystem {
|
|
38
|
+
isDirectory(path: string): Promise<boolean>;
|
|
39
|
+
}
|
|
37
40
|
type AgentMcpTransportFactory = () => Promise<unknown>;
|
|
38
41
|
export interface SessionOptions {
|
|
39
42
|
clientId: string;
|
|
@@ -51,6 +54,7 @@ export interface SessionOptions {
|
|
|
51
54
|
agentStorage: AgentStorage;
|
|
52
55
|
projectRegistry: ProjectRegistry;
|
|
53
56
|
workspaceRegistry: WorkspaceRegistry;
|
|
57
|
+
filesystem?: SessionFileSystem;
|
|
54
58
|
chatService: FileBackedChatService;
|
|
55
59
|
scheduleService: ScheduleService;
|
|
56
60
|
loopService: LoopService;
|
|
@@ -149,6 +153,7 @@ export declare class Session {
|
|
|
149
153
|
private readonly agentStorage;
|
|
150
154
|
private readonly projectRegistry;
|
|
151
155
|
private readonly workspaceRegistry;
|
|
156
|
+
private readonly filesystem;
|
|
152
157
|
private readonly chatService;
|
|
153
158
|
private readonly scheduleService;
|
|
154
159
|
private readonly loopService;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import equal from "fast-deep-equal";
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
|
+
import { stat } from "node:fs/promises";
|
|
4
5
|
import { basename, resolve, sep } from "path";
|
|
5
6
|
import { homedir } from "node:os";
|
|
6
7
|
import { z } from "zod";
|
|
@@ -215,6 +216,12 @@ const PCM_BYTES_PER_MS = (PCM_SAMPLE_RATE * PCM_CHANNELS * (PCM_BITS_PER_SAMPLE
|
|
|
215
216
|
const MIN_STREAMING_SEGMENT_DURATION_MS = 1000;
|
|
216
217
|
const MIN_STREAMING_SEGMENT_BYTES = Math.round(PCM_BYTES_PER_MS * MIN_STREAMING_SEGMENT_DURATION_MS);
|
|
217
218
|
const AgentIdSchema = z.string().uuid();
|
|
219
|
+
const nodeSessionFileSystem = {
|
|
220
|
+
async isDirectory(path) {
|
|
221
|
+
const stats = await stat(path).catch(() => null);
|
|
222
|
+
return stats?.isDirectory() ?? false;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
218
225
|
class VoiceFeatureUnavailableError extends Error {
|
|
219
226
|
constructor(context) {
|
|
220
227
|
super(context.message);
|
|
@@ -258,6 +265,12 @@ function parseClientCapabilities(capabilities) {
|
|
|
258
265
|
}
|
|
259
266
|
return new Set(result);
|
|
260
267
|
}
|
|
268
|
+
function describeRegistryTransition(record) {
|
|
269
|
+
if (!record) {
|
|
270
|
+
return "created";
|
|
271
|
+
}
|
|
272
|
+
return record.archivedAt ? "unarchived" : "existing";
|
|
273
|
+
}
|
|
261
274
|
/**
|
|
262
275
|
* Session represents a single connected client session.
|
|
263
276
|
* It owns all state management, orchestration logic, and message processing.
|
|
@@ -318,7 +331,7 @@ export class Session {
|
|
|
318
331
|
}
|
|
319
332
|
},
|
|
320
333
|
});
|
|
321
|
-
const { clientId, appVersion, clientCapabilities, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, worktreesRoot, agentManager, agentStorage, projectRegistry, workspaceRegistry, chatService, scheduleService, loopService, checkoutDiffManager, github, workspaceGitService, daemonConfigStore, mcpBaseUrl, stt, sttLanguage, tts, terminalManager, providerSnapshotManager, serviceProxy, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, serviceProxyPublicBaseUrl, resolveScriptHealth, voice, voiceBridge, dictation, serverId, daemonVersion, daemonRuntimeConfig, } = options;
|
|
334
|
+
const { clientId, appVersion, clientCapabilities, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, worktreesRoot, agentManager, agentStorage, projectRegistry, workspaceRegistry, filesystem, chatService, scheduleService, loopService, checkoutDiffManager, github, workspaceGitService, daemonConfigStore, mcpBaseUrl, stt, sttLanguage, tts, terminalManager, providerSnapshotManager, serviceProxy, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, serviceProxyPublicBaseUrl, resolveScriptHealth, voice, voiceBridge, dictation, serverId, daemonVersion, daemonRuntimeConfig, } = options;
|
|
322
335
|
this.clientId = clientId;
|
|
323
336
|
this.appVersion = appVersion ?? null;
|
|
324
337
|
this.clientCapabilities = parseClientCapabilities(clientCapabilities);
|
|
@@ -340,6 +353,7 @@ export class Session {
|
|
|
340
353
|
this.agentStorage = agentStorage;
|
|
341
354
|
this.projectRegistry = projectRegistry;
|
|
342
355
|
this.workspaceRegistry = workspaceRegistry;
|
|
356
|
+
this.filesystem = filesystem ?? nodeSessionFileSystem;
|
|
343
357
|
this.chatService = chatService;
|
|
344
358
|
this.scheduleService = scheduleService;
|
|
345
359
|
this.loopService = loopService;
|
|
@@ -5109,9 +5123,10 @@ export class Session {
|
|
|
5109
5123
|
return result;
|
|
5110
5124
|
}
|
|
5111
5125
|
async archiveWorkspaceRecord(workspaceId, archivedAt) {
|
|
5126
|
+
const archiveTimestamp = archivedAt ?? new Date().toISOString();
|
|
5112
5127
|
const existingWorkspace = await archivePersistedWorkspaceRecord({
|
|
5113
5128
|
workspaceId,
|
|
5114
|
-
archivedAt,
|
|
5129
|
+
archivedAt: archiveTimestamp,
|
|
5115
5130
|
workspaceRegistry: this.workspaceRegistry,
|
|
5116
5131
|
projectRegistry: this.projectRegistry,
|
|
5117
5132
|
});
|
|
@@ -5119,6 +5134,16 @@ export class Session {
|
|
|
5119
5134
|
this.removeWorkspaceGitSubscription(workspaceId);
|
|
5120
5135
|
return;
|
|
5121
5136
|
}
|
|
5137
|
+
if (!existingWorkspace.archivedAt) {
|
|
5138
|
+
const activeSiblings = (await this.workspaceRegistry.list()).filter((workspace) => workspace.projectId === existingWorkspace.projectId && !workspace.archivedAt);
|
|
5139
|
+
this.sessionLogger.info({
|
|
5140
|
+
workspaceId,
|
|
5141
|
+
workspaceCwd: existingWorkspace.cwd,
|
|
5142
|
+
projectId: existingWorkspace.projectId,
|
|
5143
|
+
projectArchived: activeSiblings.length === 0,
|
|
5144
|
+
archivedAt: archiveTimestamp,
|
|
5145
|
+
}, "Workspace archived");
|
|
5146
|
+
}
|
|
5122
5147
|
await this.removeWorkspaceGitWatchTarget(existingWorkspace.cwd);
|
|
5123
5148
|
this.scriptRuntimeStore?.removeForWorkspace(existingWorkspace.cwd);
|
|
5124
5149
|
this.removeWorkspaceGitSubscription(workspaceId);
|
|
@@ -5437,11 +5462,47 @@ export class Session {
|
|
|
5437
5462
|
}
|
|
5438
5463
|
}
|
|
5439
5464
|
async handleOpenProjectRequest(request) {
|
|
5465
|
+
const requestedCwd = request.cwd;
|
|
5466
|
+
const cwd = expandTilde(requestedCwd);
|
|
5467
|
+
const directoryExists = await this.filesystem.isDirectory(cwd).catch(() => false);
|
|
5468
|
+
if (!directoryExists) {
|
|
5469
|
+
this.sessionLogger.info({ requestedCwd, resolvedCwd: cwd, reason: "directory_not_found" }, "Open project rejected");
|
|
5470
|
+
this.emit({
|
|
5471
|
+
type: "open_project_response",
|
|
5472
|
+
payload: {
|
|
5473
|
+
requestId: request.requestId,
|
|
5474
|
+
workspace: null,
|
|
5475
|
+
error: `Directory not found: ${cwd}`,
|
|
5476
|
+
errorCode: "directory_not_found",
|
|
5477
|
+
},
|
|
5478
|
+
});
|
|
5479
|
+
return;
|
|
5480
|
+
}
|
|
5440
5481
|
try {
|
|
5441
|
-
const
|
|
5482
|
+
const projectsBefore = new Map();
|
|
5483
|
+
for (const project of await this.projectRegistry.list()) {
|
|
5484
|
+
projectsBefore.set(project.projectId, project);
|
|
5485
|
+
}
|
|
5486
|
+
const workspacesBefore = new Map();
|
|
5487
|
+
for (const workspaceRecord of await this.workspaceRegistry.list()) {
|
|
5488
|
+
workspacesBefore.set(workspaceRecord.workspaceId, workspaceRecord);
|
|
5489
|
+
}
|
|
5490
|
+
const workspace = await this.findOrCreateWorkspaceForDirectory(cwd);
|
|
5491
|
+
const project = await this.projectRegistry.get(workspace.projectId);
|
|
5442
5492
|
await this.syncWorkspaceGitObserverForWorkspace(workspace);
|
|
5443
5493
|
const descriptor = await this.describeWorkspaceRecord(workspace);
|
|
5444
5494
|
await this.emitWorkspaceUpdateForCwd(workspace.cwd);
|
|
5495
|
+
this.sessionLogger.info({
|
|
5496
|
+
requestedCwd,
|
|
5497
|
+
resolvedCwd: cwd,
|
|
5498
|
+
workspaceCwd: workspace.cwd,
|
|
5499
|
+
workspaceId: workspace.workspaceId,
|
|
5500
|
+
workspaceKind: workspace.kind,
|
|
5501
|
+
workspaceTransition: describeRegistryTransition(workspacesBefore.get(workspace.workspaceId) ?? null),
|
|
5502
|
+
projectId: workspace.projectId,
|
|
5503
|
+
projectKind: project?.kind ?? null,
|
|
5504
|
+
projectTransition: describeRegistryTransition(projectsBefore.get(workspace.projectId) ?? null),
|
|
5505
|
+
}, "Project opened");
|
|
5445
5506
|
this.emit({
|
|
5446
5507
|
type: "open_project_response",
|
|
5447
5508
|
payload: {
|
|
@@ -5462,7 +5523,7 @@ export class Session {
|
|
|
5462
5523
|
}
|
|
5463
5524
|
catch (error) {
|
|
5464
5525
|
const message = error instanceof Error ? error.message : "Failed to open project";
|
|
5465
|
-
this.sessionLogger.error({ err: error, cwd
|
|
5526
|
+
this.sessionLogger.error({ err: error, cwd }, "Failed to open project");
|
|
5466
5527
|
this.emit({
|
|
5467
5528
|
type: "open_project_response",
|
|
5468
5529
|
payload: {
|
|
@@ -55,10 +55,7 @@ export class WorkspaceReconciliationService {
|
|
|
55
55
|
return;
|
|
56
56
|
this.running = true;
|
|
57
57
|
try {
|
|
58
|
-
|
|
59
|
-
if (result.changesApplied.length > 0) {
|
|
60
|
-
this.logger.info({ changeCount: result.changesApplied.length, durationMs: result.durationMs }, "Reconciliation pass completed with changes");
|
|
61
|
-
}
|
|
58
|
+
await this.reconcile();
|
|
62
59
|
}
|
|
63
60
|
catch (error) {
|
|
64
61
|
this.logger.error({ err: error }, "Reconciliation pass failed");
|
|
@@ -130,7 +127,15 @@ export class WorkspaceReconciliationService {
|
|
|
130
127
|
if (changes.length > 0 && this.onChanges) {
|
|
131
128
|
this.onChanges(changes);
|
|
132
129
|
}
|
|
133
|
-
|
|
130
|
+
const result = { changesApplied: changes, durationMs: Date.now() - start };
|
|
131
|
+
if (changes.length > 0) {
|
|
132
|
+
this.logger.info({
|
|
133
|
+
changeCount: changes.length,
|
|
134
|
+
durationMs: result.durationMs,
|
|
135
|
+
changes,
|
|
136
|
+
}, "Workspace reconciliation applied changes");
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
134
139
|
}
|
|
135
140
|
async mergeDuplicateProjectsByRoot(activeProjects, workspacesByProject, changes) {
|
|
136
141
|
const projectsByRoot = new Map();
|
|
@@ -205,6 +205,7 @@ const PullRequestTimelineReviewNodeSchema = z.object({
|
|
|
205
205
|
id: z.string().catch(""),
|
|
206
206
|
state: z.string().catch(""),
|
|
207
207
|
body: z.string().nullable().catch(null),
|
|
208
|
+
bodyHTML: z.string().nullable().catch(null),
|
|
208
209
|
url: z.string().catch(""),
|
|
209
210
|
submittedAt: z.string().nullable().catch(null),
|
|
210
211
|
author: TimelineAuthorSchema,
|
|
@@ -212,6 +213,7 @@ const PullRequestTimelineReviewNodeSchema = z.object({
|
|
|
212
213
|
const PullRequestTimelineCommentNodeSchema = z.object({
|
|
213
214
|
id: z.string().catch(""),
|
|
214
215
|
body: z.string().nullable().catch(null),
|
|
216
|
+
bodyHTML: z.string().nullable().catch(null),
|
|
215
217
|
url: z.string().catch(""),
|
|
216
218
|
createdAt: z.string().nullable().catch(null),
|
|
217
219
|
author: TimelineAuthorSchema,
|
|
@@ -379,6 +381,7 @@ query PullRequestTimeline($owner: String!, $name: String!, $number: Int!) {
|
|
|
379
381
|
id
|
|
380
382
|
state
|
|
381
383
|
body
|
|
384
|
+
bodyHTML
|
|
382
385
|
url
|
|
383
386
|
submittedAt
|
|
384
387
|
author {
|
|
@@ -395,6 +398,7 @@ query PullRequestTimeline($owner: String!, $name: String!, $number: Int!) {
|
|
|
395
398
|
nodes {
|
|
396
399
|
id
|
|
397
400
|
body
|
|
401
|
+
bodyHTML
|
|
398
402
|
url
|
|
399
403
|
createdAt
|
|
400
404
|
author {
|
|
@@ -419,6 +423,7 @@ query PullRequestTimeline($owner: String!, $name: String!, $number: Int!) {
|
|
|
419
423
|
nodes {
|
|
420
424
|
id
|
|
421
425
|
body
|
|
426
|
+
bodyHTML
|
|
422
427
|
url
|
|
423
428
|
createdAt
|
|
424
429
|
author {
|
|
@@ -1585,7 +1590,7 @@ function toPullRequestTimelineReviewItem(review) {
|
|
|
1585
1590
|
author: review.author?.login ?? "unknown",
|
|
1586
1591
|
authorUrl: review.author?.url ?? null,
|
|
1587
1592
|
avatarUrl: review.author?.avatarUrl ?? null,
|
|
1588
|
-
body: review.body ?? "",
|
|
1593
|
+
body: normalizeGitHubTimelineBody(review.body ?? "", review.bodyHTML ?? ""),
|
|
1589
1594
|
createdAt: parseOptionalTime(review.submittedAt ?? null),
|
|
1590
1595
|
url: review.url,
|
|
1591
1596
|
reviewState,
|
|
@@ -1599,11 +1604,111 @@ function toPullRequestTimelineCommentItem(comment) {
|
|
|
1599
1604
|
author: comment.author?.login ?? "unknown",
|
|
1600
1605
|
authorUrl: comment.author?.url ?? null,
|
|
1601
1606
|
avatarUrl: comment.author?.avatarUrl ?? null,
|
|
1602
|
-
body: comment.body ?? "",
|
|
1607
|
+
body: normalizeGitHubTimelineBody(comment.body ?? "", comment.bodyHTML ?? ""),
|
|
1603
1608
|
createdAt: parseOptionalTime(comment.createdAt ?? null),
|
|
1604
1609
|
url: comment.url,
|
|
1605
1610
|
};
|
|
1606
1611
|
}
|
|
1612
|
+
const RAW_MARKDOWN_IMAGE_RE = /!\[[^\]]*\]\(\s*([^\s)]+)(?:\s+["'][^)]*["'])?\s*\)/g;
|
|
1613
|
+
const HTML_IMAGE_RE = /<img\b[^>]*\bsrc\s*=\s*(["'])(.*?)\1[^>]*>/gi;
|
|
1614
|
+
const GITHUB_RENDERED_IMAGE_HOSTS = new Set([
|
|
1615
|
+
"camo.githubusercontent.com",
|
|
1616
|
+
"private-user-images.githubusercontent.com",
|
|
1617
|
+
]);
|
|
1618
|
+
function normalizeGitHubTimelineBody(body, bodyHTML) {
|
|
1619
|
+
const rawImages = extractRawImageSourceReferences(body);
|
|
1620
|
+
if (rawImages.length === 0) {
|
|
1621
|
+
return body;
|
|
1622
|
+
}
|
|
1623
|
+
const renderedSources = extractRenderedImageSources(bodyHTML);
|
|
1624
|
+
if (renderedSources.length !== rawImages.length) {
|
|
1625
|
+
return body;
|
|
1626
|
+
}
|
|
1627
|
+
let cursor = 0;
|
|
1628
|
+
let normalized = "";
|
|
1629
|
+
for (let index = 0; index < rawImages.length; index += 1) {
|
|
1630
|
+
const rawImage = rawImages[index];
|
|
1631
|
+
const renderedSrc = renderedSources[index];
|
|
1632
|
+
if (!rawImage ||
|
|
1633
|
+
!renderedSrc ||
|
|
1634
|
+
!isRawGitHubAttachmentSource(rawImage.src) ||
|
|
1635
|
+
!isGitHubRenderedImageSource(renderedSrc)) {
|
|
1636
|
+
return body;
|
|
1637
|
+
}
|
|
1638
|
+
normalized += body.slice(cursor, rawImage.start);
|
|
1639
|
+
normalized += renderedSrc;
|
|
1640
|
+
cursor = rawImage.end;
|
|
1641
|
+
}
|
|
1642
|
+
normalized += body.slice(cursor);
|
|
1643
|
+
return normalized;
|
|
1644
|
+
}
|
|
1645
|
+
function extractRawImageSourceReferences(source) {
|
|
1646
|
+
const references = [
|
|
1647
|
+
...extractHtmlImageSourceReferences(source),
|
|
1648
|
+
...extractMarkdownImageSourceReferences(source),
|
|
1649
|
+
];
|
|
1650
|
+
return references.sort((left, right) => left.start - right.start);
|
|
1651
|
+
}
|
|
1652
|
+
function extractRenderedImageSources(source) {
|
|
1653
|
+
return extractHtmlImageSourceReferences(source).map((reference) => reference.src);
|
|
1654
|
+
}
|
|
1655
|
+
function extractHtmlImageSourceReferences(source) {
|
|
1656
|
+
const references = [];
|
|
1657
|
+
HTML_IMAGE_RE.lastIndex = 0;
|
|
1658
|
+
let match;
|
|
1659
|
+
while ((match = HTML_IMAGE_RE.exec(source)) !== null) {
|
|
1660
|
+
const src = decodeHtmlAttribute(match[2] ?? "");
|
|
1661
|
+
if (!src) {
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
const rawAttributeSrc = match[2] ?? "";
|
|
1665
|
+
const start = match.index + match[0].indexOf(rawAttributeSrc);
|
|
1666
|
+
references.push({ src, start, end: start + rawAttributeSrc.length });
|
|
1667
|
+
}
|
|
1668
|
+
return references;
|
|
1669
|
+
}
|
|
1670
|
+
function extractMarkdownImageSourceReferences(source) {
|
|
1671
|
+
const references = [];
|
|
1672
|
+
RAW_MARKDOWN_IMAGE_RE.lastIndex = 0;
|
|
1673
|
+
let match;
|
|
1674
|
+
while ((match = RAW_MARKDOWN_IMAGE_RE.exec(source)) !== null) {
|
|
1675
|
+
const src = match[1] ?? "";
|
|
1676
|
+
if (!src) {
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
const start = match.index + match[0].indexOf(src);
|
|
1680
|
+
references.push({ src, start, end: start + src.length });
|
|
1681
|
+
}
|
|
1682
|
+
return references;
|
|
1683
|
+
}
|
|
1684
|
+
function isRawGitHubAttachmentSource(src) {
|
|
1685
|
+
try {
|
|
1686
|
+
const url = new URL(src);
|
|
1687
|
+
return (url.protocol === "https:" &&
|
|
1688
|
+
url.hostname === "github.com" &&
|
|
1689
|
+
url.pathname.startsWith("/user-attachments/assets/"));
|
|
1690
|
+
}
|
|
1691
|
+
catch {
|
|
1692
|
+
return false;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
function isGitHubRenderedImageSource(src) {
|
|
1696
|
+
try {
|
|
1697
|
+
const url = new URL(src);
|
|
1698
|
+
return url.protocol === "https:" && GITHUB_RENDERED_IMAGE_HOSTS.has(url.hostname);
|
|
1699
|
+
}
|
|
1700
|
+
catch {
|
|
1701
|
+
return false;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
function decodeHtmlAttribute(value) {
|
|
1705
|
+
return value
|
|
1706
|
+
.replaceAll("&", "&")
|
|
1707
|
+
.replaceAll(""", '"')
|
|
1708
|
+
.replaceAll("'", "'")
|
|
1709
|
+
.replaceAll("<", "<")
|
|
1710
|
+
.replaceAll(">", ">");
|
|
1711
|
+
}
|
|
1607
1712
|
function toPullRequestTimelineReviewThreadItems(thread) {
|
|
1608
1713
|
return thread.comments.nodes.map((comment) => ({
|
|
1609
1714
|
...toPullRequestTimelineCommentItem(comment),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpaseo/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.95",
|
|
4
4
|
"description": "Paseo backend server",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/server",
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@agentclientprotocol/sdk": "^0.17.1",
|
|
61
61
|
"@anthropic-ai/claude-agent-sdk": "^0.2.133",
|
|
62
|
-
"@getpaseo/client": "0.1.
|
|
63
|
-
"@getpaseo/highlight": "0.1.
|
|
64
|
-
"@getpaseo/protocol": "0.1.
|
|
65
|
-
"@getpaseo/relay": "0.1.
|
|
62
|
+
"@getpaseo/client": "0.1.95",
|
|
63
|
+
"@getpaseo/highlight": "0.1.95",
|
|
64
|
+
"@getpaseo/protocol": "0.1.95",
|
|
65
|
+
"@getpaseo/relay": "0.1.95",
|
|
66
66
|
"@isaacs/ttlcache": "^2.1.4",
|
|
67
67
|
"@modelcontextprotocol/sdk": "^1.20.1",
|
|
68
68
|
"@opencode-ai/sdk": "1.14.46",
|