@getpaseo/server 0.1.94 → 0.1.96

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 workspace = await this.findOrCreateWorkspaceForDirectory(request.cwd);
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: request.cwd }, "Failed to open project");
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
- const result = await this.reconcile();
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
- return { changesApplied: changes, durationMs: Date.now() - start };
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("&amp;", "&")
1707
+ .replaceAll("&quot;", '"')
1708
+ .replaceAll("&#39;", "'")
1709
+ .replaceAll("&lt;", "<")
1710
+ .replaceAll("&gt;", ">");
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.94",
3
+ "version": "0.1.96",
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.94",
63
- "@getpaseo/highlight": "0.1.94",
64
- "@getpaseo/protocol": "0.1.94",
65
- "@getpaseo/relay": "0.1.94",
62
+ "@getpaseo/client": "0.1.96",
63
+ "@getpaseo/highlight": "0.1.96",
64
+ "@getpaseo/protocol": "0.1.96",
65
+ "@getpaseo/relay": "0.1.96",
66
66
  "@isaacs/ttlcache": "^2.1.4",
67
67
  "@modelcontextprotocol/sdk": "^1.20.1",
68
68
  "@opencode-ai/sdk": "1.14.46",