@mastra/factory 0.17.0-alpha.1 → 0.17.0-alpha.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"subscriptions.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/subscriptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,4CAA4C,CAAC;AAEpH,MAAM,MAAM,8BAA8B,GAAG,mBAAmB,GAAG,mBAAmB,GAAG,eAAe,CAAC;AACzG,MAAM,MAAM,8BAA8B,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE1E,MAAM,WAAW,4BAA4B;IAC3C,sBAAsB,EAAE,MAAM,CAAC;IAC/B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,8BAA8B,CAAC;IACvC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,MAAM,2BAA2B,GAAG,uBAAuB,CAAC,4BAA4B,CAAC,CAAC;AAChG,MAAM,MAAM,yBAAyB,GAAG,wBAAwB,CAC9D,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvB,4BAA4B,CAC7B,CAAC;AAEF,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB,EAAE,MAAM,CAAC;IAC/B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,8BAA8B,CAAC;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB,EAAE,MAAM,CAAC;IAC/B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,8BAA8B,GAAG,IAAI,CAAC,6BAA6B,EAAE,OAAO,CAAC,CAAC;AAE1F,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,8BAA8B,GAAG,MAAM,CAEpF;AAYD,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,2BAA2B,EAClC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,2BAA2B,CAAC,CA2BtC;AAED,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,2BAA2B,EAClC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,qCAAqC,CACzD,KAAK,EAAE,wBAAwB,EAC/B,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,2BAA2B,EAAE,CAAC,CASxC;AAED,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,6BAA6B,EACpC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,2BAA2B,EAAE,CAAC,CAGxC;AAED,wBAAsB,sCAAsC,CAC1D,KAAK,EAAE,8BAA8B,EACrC,OAAO,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,EAClD,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,2BAA2B,EAAE,CAAC,CAGxC;AAED,wBAAgB,6BAA6B,CAC3C,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,8BAA8B,EACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAsB,8BAA8B,CAClD,KAAK,EAAE,6BAA6B,EACpC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAGf"}
1
+ {"version":3,"file":"subscriptions.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/subscriptions.ts"],"names":[],"mappings":"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"}
@@ -40,7 +40,9 @@ async function unsubscribeFromPullRequest(input, storage) {
40
40
  await Promise.all(rows.filter((row) => sameSession(row, input)).map((row) => storage.subscriptions.delete(row.id)));
41
41
  }
42
42
  async function listPullRequestSubscriptionsForThread(input, storage) {
43
- return (await storage.subscriptions.listByThread(input.resourceId, input.threadId)).filter((row) => row.orgId === input.orgId && row.resourceId === input.resourceId && row.threadId === input.threadId && (row.sessionScope ?? "") === (input.sessionScope ?? ""));
43
+ const matching = (await storage.subscriptions.listByThread(input.resourceId, input.threadId)).filter((row) => row.orgId === input.orgId && row.resourceId === input.resourceId && row.threadId === input.threadId && (row.sessionScope ?? "") === (input.sessionScope ?? ""));
44
+ if (matching.length > 0 || input.sessionScope) return matching;
45
+ return (await storage.subscriptions.listBySession(input.resourceId)).filter((row) => row.orgId === input.orgId && row.threadId === input.threadId && !row.sessionScope);
44
46
  }
45
47
  async function listPullRequestSubscriptions(input, storage) {
46
48
  return (await storage.subscriptions.listByTarget(changeRequestTargetKey(input))).filter((row) => row.orgId === input.orgId && row.status === "open");
@@ -1 +1 @@
1
- {"version":3,"file":"subscriptions.js","names":[],"sources":["../../../src/integrations/github/subscriptions.ts"],"sourcesContent":["import type { IntegrationStorageHandle, IntegrationSubscription } from '../../storage/domains/integrations/base.js';\n\nexport type GithubSignalSubscriptionSource = 'auto-gh-pr-create' | 'factory-pr-create' | 'explicit-tool';\nexport type GithubSignalSubscriptionStatus = 'open' | 'closed' | 'merged';\n\nexport interface GithubSignalSubscriptionData {\n installationExternalId: string;\n projectRepositoryId: string;\n repositoryExternalId: string;\n repositorySlug: string;\n changeRequestId: string;\n ownerId: string;\n source: GithubSignalSubscriptionSource;\n subscribedByUserId: string | null;\n}\n\nexport type GithubSignalSubscriptionRow = IntegrationSubscription<GithubSignalSubscriptionData>;\nexport type GithubSubscriptionStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n Record<string, unknown>,\n GithubSignalSubscriptionData\n>;\n\nexport interface SubscribeToPullRequestInput {\n orgId: string;\n installationExternalId: string;\n projectRepositoryId: string;\n repositoryExternalId: string;\n repositorySlug: string;\n changeRequestId: string;\n sessionId: string;\n ownerId: string;\n resourceId: string;\n threadId: string;\n sessionScope?: string;\n source: GithubSignalSubscriptionSource;\n subscribedByUserId?: string;\n}\n\nexport interface ThreadSubscriptionTarget {\n orgId: string;\n resourceId: string;\n threadId: string;\n sessionScope?: string;\n}\n\nexport interface PullRequestSubscriptionTarget {\n orgId: string;\n installationExternalId: string;\n repositoryExternalId: string;\n changeRequestId: string;\n}\n\nexport type GithubWebhookPullRequestTarget = Omit<PullRequestSubscriptionTarget, 'orgId'>;\n\nexport function changeRequestTargetKey(input: GithubWebhookPullRequestTarget): string {\n return `change-request:${input.installationExternalId}:${input.repositoryExternalId}:${input.changeRequestId}`;\n}\n\nfunction sameSession(row: GithubSignalSubscriptionRow, input: SubscribeToPullRequestInput): boolean {\n return (\n row.orgId === input.orgId &&\n row.sessionId === input.sessionId &&\n row.resourceId === input.resourceId &&\n row.threadId === input.threadId &&\n (row.sessionScope ?? '') === (input.sessionScope ?? '')\n );\n}\n\nexport async function subscribeToPullRequest(\n input: SubscribeToPullRequestInput,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow> {\n const targetKey = changeRequestTargetKey(input);\n const existing = (await storage.subscriptions.listByTarget(targetKey)).find(row => sameSession(row, input));\n if (existing) {\n if (existing.status !== 'open') await storage.subscriptions.updateStatus(existing.id, 'open');\n return { ...existing, status: 'open' };\n }\n\n return storage.subscriptions.create({\n orgId: input.orgId,\n targetKey,\n sessionId: input.sessionId,\n resourceId: input.resourceId,\n threadId: input.threadId,\n sessionScope: input.sessionScope ?? '',\n status: 'open',\n data: {\n installationExternalId: input.installationExternalId,\n projectRepositoryId: input.projectRepositoryId,\n repositoryExternalId: input.repositoryExternalId,\n repositorySlug: input.repositorySlug,\n changeRequestId: input.changeRequestId,\n ownerId: input.ownerId,\n source: input.source,\n subscribedByUserId: input.subscribedByUserId ?? null,\n },\n });\n}\n\nexport async function unsubscribeFromPullRequest(\n input: SubscribeToPullRequestInput,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n await Promise.all(rows.filter(row => sameSession(row, input)).map(row => storage.subscriptions.delete(row.id)));\n}\n\nexport async function listPullRequestSubscriptionsForThread(\n input: ThreadSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByThread(input.resourceId, input.threadId);\n return rows.filter(\n row =>\n row.orgId === input.orgId &&\n row.resourceId === input.resourceId &&\n row.threadId === input.threadId &&\n (row.sessionScope ?? '') === (input.sessionScope ?? ''),\n );\n}\n\nexport async function listPullRequestSubscriptions(\n input: PullRequestSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n return rows.filter(row => row.orgId === input.orgId && row.status === 'open');\n}\n\nexport async function listPullRequestSubscriptionsForWebhook(\n input: GithubWebhookPullRequestTarget,\n options: { includeTerminal?: boolean } | undefined,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n return options?.includeTerminal ? rows : rows.filter(row => row.status === 'open');\n}\n\nexport function retirePullRequestSubscription(\n id: string,\n status: GithubSignalSubscriptionStatus,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n return storage.subscriptions.updateStatus(id, status);\n}\n\nexport async function retirePullRequestSubscriptions(\n input: PullRequestSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n const rows = await listPullRequestSubscriptions(input, storage);\n await Promise.all(rows.map(row => storage.subscriptions.updateStatus(row.id, 'closed')));\n}\n"],"mappings":";AAuDA,SAAgB,uBAAuB,OAA+C;CACpF,OAAO,kBAAkB,MAAM,uBAAuB,GAAG,MAAM,qBAAqB,GAAG,MAAM;AAC/F;AAEA,SAAS,YAAY,KAAkC,OAA6C;CAClG,OACE,IAAI,UAAU,MAAM,SACpB,IAAI,cAAc,MAAM,aACxB,IAAI,eAAe,MAAM,cACzB,IAAI,aAAa,MAAM,aACtB,IAAI,gBAAgB,SAAS,MAAM,gBAAgB;AAExD;AAEA,eAAsB,uBACpB,OACA,SACsC;CACtC,MAAM,YAAY,uBAAuB,KAAK;CAC9C,MAAM,YAAY,MAAM,QAAQ,cAAc,aAAa,SAAS,EAAA,CAAG,MAAK,QAAO,YAAY,KAAK,KAAK,CAAC;CAC1G,IAAI,UAAU;EACZ,IAAI,SAAS,WAAW,QAAQ,MAAM,QAAQ,cAAc,aAAa,SAAS,IAAI,MAAM;EAC5F,OAAO;GAAE,GAAG;GAAU,QAAQ;EAAO;CACvC;CAEA,OAAO,QAAQ,cAAc,OAAO;EAClC,OAAO,MAAM;EACb;EACA,WAAW,MAAM;EACjB,YAAY,MAAM;EAClB,UAAU,MAAM;EAChB,cAAc,MAAM,gBAAgB;EACpC,QAAQ;EACR,MAAM;GACJ,wBAAwB,MAAM;GAC9B,qBAAqB,MAAM;GAC3B,sBAAsB,MAAM;GAC5B,gBAAgB,MAAM;GACtB,iBAAiB,MAAM;GACvB,SAAS,MAAM;GACf,QAAQ,MAAM;GACd,oBAAoB,MAAM,sBAAsB;EAClD;CACF,CAAC;AACH;AAEA,eAAsB,2BACpB,OACA,SACe;CACf,MAAM,OAAO,MAAM,QAAQ,cAAc,aAAa,uBAAuB,KAAK,CAAC;CACnF,MAAM,QAAQ,IAAI,KAAK,QAAO,QAAO,YAAY,KAAK,KAAK,CAAC,CAAC,CAAC,KAAI,QAAO,QAAQ,cAAc,OAAO,IAAI,EAAE,CAAC,CAAC;AAChH;AAEA,eAAsB,sCACpB,OACA,SACwC;CAExC,QAAO,MADY,QAAQ,cAAc,aAAa,MAAM,YAAY,MAAM,QAAQ,EAAA,CAC1E,QACV,QACE,IAAI,UAAU,MAAM,SACpB,IAAI,eAAe,MAAM,cACzB,IAAI,aAAa,MAAM,aACtB,IAAI,gBAAgB,SAAS,MAAM,gBAAgB,GACxD;AACF;AAEA,eAAsB,6BACpB,OACA,SACwC;CAExC,QAAO,MADY,QAAQ,cAAc,aAAa,uBAAuB,KAAK,CAAC,EAAA,CACvE,QAAO,QAAO,IAAI,UAAU,MAAM,SAAS,IAAI,WAAW,MAAM;AAC9E;AAEA,eAAsB,uCACpB,OACA,SACA,SACwC;CACxC,MAAM,OAAO,MAAM,QAAQ,cAAc,aAAa,uBAAuB,KAAK,CAAC;CACnF,OAAO,SAAS,kBAAkB,OAAO,KAAK,QAAO,QAAO,IAAI,WAAW,MAAM;AACnF;AAEA,SAAgB,8BACd,IACA,QACA,SACe;CACf,OAAO,QAAQ,cAAc,aAAa,IAAI,MAAM;AACtD;AAEA,eAAsB,+BACpB,OACA,SACe;CACf,MAAM,OAAO,MAAM,6BAA6B,OAAO,OAAO;CAC9D,MAAM,QAAQ,IAAI,KAAK,KAAI,QAAO,QAAQ,cAAc,aAAa,IAAI,IAAI,QAAQ,CAAC,CAAC;AACzF"}
1
+ {"version":3,"file":"subscriptions.js","names":[],"sources":["../../../src/integrations/github/subscriptions.ts"],"sourcesContent":["import type { IntegrationStorageHandle, IntegrationSubscription } from '../../storage/domains/integrations/base.js';\n\nexport type GithubSignalSubscriptionSource = 'auto-gh-pr-create' | 'factory-pr-create' | 'explicit-tool';\nexport type GithubSignalSubscriptionStatus = 'open' | 'closed' | 'merged';\n\nexport interface GithubSignalSubscriptionData {\n installationExternalId: string;\n projectRepositoryId: string;\n repositoryExternalId: string;\n repositorySlug: string;\n changeRequestId: string;\n ownerId: string;\n source: GithubSignalSubscriptionSource;\n subscribedByUserId: string | null;\n}\n\nexport type GithubSignalSubscriptionRow = IntegrationSubscription<GithubSignalSubscriptionData>;\nexport type GithubSubscriptionStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n Record<string, unknown>,\n GithubSignalSubscriptionData\n>;\n\nexport interface SubscribeToPullRequestInput {\n orgId: string;\n installationExternalId: string;\n projectRepositoryId: string;\n repositoryExternalId: string;\n repositorySlug: string;\n changeRequestId: string;\n sessionId: string;\n ownerId: string;\n resourceId: string;\n threadId: string;\n sessionScope?: string;\n source: GithubSignalSubscriptionSource;\n subscribedByUserId?: string;\n}\n\nexport interface ThreadSubscriptionTarget {\n orgId: string;\n resourceId: string;\n threadId: string;\n sessionScope?: string;\n}\n\nexport interface PullRequestSubscriptionTarget {\n orgId: string;\n installationExternalId: string;\n repositoryExternalId: string;\n changeRequestId: string;\n}\n\nexport type GithubWebhookPullRequestTarget = Omit<PullRequestSubscriptionTarget, 'orgId'>;\n\nexport function changeRequestTargetKey(input: GithubWebhookPullRequestTarget): string {\n return `change-request:${input.installationExternalId}:${input.repositoryExternalId}:${input.changeRequestId}`;\n}\n\nfunction sameSession(row: GithubSignalSubscriptionRow, input: SubscribeToPullRequestInput): boolean {\n return (\n row.orgId === input.orgId &&\n row.sessionId === input.sessionId &&\n row.resourceId === input.resourceId &&\n row.threadId === input.threadId &&\n (row.sessionScope ?? '') === (input.sessionScope ?? '')\n );\n}\n\nexport async function subscribeToPullRequest(\n input: SubscribeToPullRequestInput,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow> {\n const targetKey = changeRequestTargetKey(input);\n const existing = (await storage.subscriptions.listByTarget(targetKey)).find(row => sameSession(row, input));\n if (existing) {\n if (existing.status !== 'open') await storage.subscriptions.updateStatus(existing.id, 'open');\n return { ...existing, status: 'open' };\n }\n\n return storage.subscriptions.create({\n orgId: input.orgId,\n targetKey,\n sessionId: input.sessionId,\n resourceId: input.resourceId,\n threadId: input.threadId,\n sessionScope: input.sessionScope ?? '',\n status: 'open',\n data: {\n installationExternalId: input.installationExternalId,\n projectRepositoryId: input.projectRepositoryId,\n repositoryExternalId: input.repositoryExternalId,\n repositorySlug: input.repositorySlug,\n changeRequestId: input.changeRequestId,\n ownerId: input.ownerId,\n source: input.source,\n subscribedByUserId: input.subscribedByUserId ?? null,\n },\n });\n}\n\nexport async function unsubscribeFromPullRequest(\n input: SubscribeToPullRequestInput,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n await Promise.all(rows.filter(row => sameSession(row, input)).map(row => storage.subscriptions.delete(row.id)));\n}\n\nexport async function listPullRequestSubscriptionsForThread(\n input: ThreadSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByThread(input.resourceId, input.threadId);\n const matching = rows.filter(\n row =>\n row.orgId === input.orgId &&\n row.resourceId === input.resourceId &&\n row.threadId === input.threadId &&\n (row.sessionScope ?? '') === (input.sessionScope ?? ''),\n );\n if (matching.length > 0 || input.sessionScope) return matching;\n // A user session is addressed by its own id (its thread is its session), but\n // its subscriptions were recorded under the Factory project that owns the\n // repository. Answer those rows for the session that created them.\n const owned = await storage.subscriptions.listBySession(input.resourceId);\n return owned.filter(row => row.orgId === input.orgId && row.threadId === input.threadId && !row.sessionScope);\n}\n\nexport async function listPullRequestSubscriptions(\n input: PullRequestSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n return rows.filter(row => row.orgId === input.orgId && row.status === 'open');\n}\n\nexport async function listPullRequestSubscriptionsForWebhook(\n input: GithubWebhookPullRequestTarget,\n options: { includeTerminal?: boolean } | undefined,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n return options?.includeTerminal ? rows : rows.filter(row => row.status === 'open');\n}\n\nexport function retirePullRequestSubscription(\n id: string,\n status: GithubSignalSubscriptionStatus,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n return storage.subscriptions.updateStatus(id, status);\n}\n\nexport async function retirePullRequestSubscriptions(\n input: PullRequestSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n const rows = await listPullRequestSubscriptions(input, storage);\n await Promise.all(rows.map(row => storage.subscriptions.updateStatus(row.id, 'closed')));\n}\n"],"mappings":";AAuDA,SAAgB,uBAAuB,OAA+C;CACpF,OAAO,kBAAkB,MAAM,uBAAuB,GAAG,MAAM,qBAAqB,GAAG,MAAM;AAC/F;AAEA,SAAS,YAAY,KAAkC,OAA6C;CAClG,OACE,IAAI,UAAU,MAAM,SACpB,IAAI,cAAc,MAAM,aACxB,IAAI,eAAe,MAAM,cACzB,IAAI,aAAa,MAAM,aACtB,IAAI,gBAAgB,SAAS,MAAM,gBAAgB;AAExD;AAEA,eAAsB,uBACpB,OACA,SACsC;CACtC,MAAM,YAAY,uBAAuB,KAAK;CAC9C,MAAM,YAAY,MAAM,QAAQ,cAAc,aAAa,SAAS,EAAA,CAAG,MAAK,QAAO,YAAY,KAAK,KAAK,CAAC;CAC1G,IAAI,UAAU;EACZ,IAAI,SAAS,WAAW,QAAQ,MAAM,QAAQ,cAAc,aAAa,SAAS,IAAI,MAAM;EAC5F,OAAO;GAAE,GAAG;GAAU,QAAQ;EAAO;CACvC;CAEA,OAAO,QAAQ,cAAc,OAAO;EAClC,OAAO,MAAM;EACb;EACA,WAAW,MAAM;EACjB,YAAY,MAAM;EAClB,UAAU,MAAM;EAChB,cAAc,MAAM,gBAAgB;EACpC,QAAQ;EACR,MAAM;GACJ,wBAAwB,MAAM;GAC9B,qBAAqB,MAAM;GAC3B,sBAAsB,MAAM;GAC5B,gBAAgB,MAAM;GACtB,iBAAiB,MAAM;GACvB,SAAS,MAAM;GACf,QAAQ,MAAM;GACd,oBAAoB,MAAM,sBAAsB;EAClD;CACF,CAAC;AACH;AAEA,eAAsB,2BACpB,OACA,SACe;CACf,MAAM,OAAO,MAAM,QAAQ,cAAc,aAAa,uBAAuB,KAAK,CAAC;CACnF,MAAM,QAAQ,IAAI,KAAK,QAAO,QAAO,YAAY,KAAK,KAAK,CAAC,CAAC,CAAC,KAAI,QAAO,QAAQ,cAAc,OAAO,IAAI,EAAE,CAAC,CAAC;AAChH;AAEA,eAAsB,sCACpB,OACA,SACwC;CAExC,MAAM,YAAW,MADE,QAAQ,cAAc,aAAa,MAAM,YAAY,MAAM,QAAQ,EAAA,CAChE,QACpB,QACE,IAAI,UAAU,MAAM,SACpB,IAAI,eAAe,MAAM,cACzB,IAAI,aAAa,MAAM,aACtB,IAAI,gBAAgB,SAAS,MAAM,gBAAgB,GACxD;CACA,IAAI,SAAS,SAAS,KAAK,MAAM,cAAc,OAAO;CAKtD,QAAO,MADa,QAAQ,cAAc,cAAc,MAAM,UAAU,EAAA,CAC3D,QAAO,QAAO,IAAI,UAAU,MAAM,SAAS,IAAI,aAAa,MAAM,YAAY,CAAC,IAAI,YAAY;AAC9G;AAEA,eAAsB,6BACpB,OACA,SACwC;CAExC,QAAO,MADY,QAAQ,cAAc,aAAa,uBAAuB,KAAK,CAAC,EAAA,CACvE,QAAO,QAAO,IAAI,UAAU,MAAM,SAAS,IAAI,WAAW,MAAM;AAC9E;AAEA,eAAsB,uCACpB,OACA,SACA,SACwC;CACxC,MAAM,OAAO,MAAM,QAAQ,cAAc,aAAa,uBAAuB,KAAK,CAAC;CACnF,OAAO,SAAS,kBAAkB,OAAO,KAAK,QAAO,QAAO,IAAI,WAAW,MAAM;AACnF;AAEA,SAAgB,8BACd,IACA,QACA,SACe;CACf,OAAO,QAAQ,cAAc,aAAa,IAAI,MAAM;AACtD;AAEA,eAAsB,+BACpB,OACA,SACe;CACf,MAAM,OAAO,MAAM,6BAA6B,OAAO,OAAO;CAC9D,MAAM,QAAQ,IAAI,KAAK,KAAI,QAAO,QAAQ,cAAc,aAAa,IAAI,IAAI,QAAQ,CAAC,CAAC;AACzF"}
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAE/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6CAA6C,CAAC;AACzF,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAMhG;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,yBAAyB,CAAC;CAC5C;AA8BD,qBAAa,gBAAiB,YAAW,kBAAkB;;IACzD,QAAQ,CAAC,EAAE,WAAW;IACtB;;;;OAIG;IACH,QAAQ,CAAC,yBAAyB,QAAQ;gBAU9B,MAAM,EAAE,sBAAsB;IAS1C,QAAQ,CAAC,GAAG,EAAE,kBAAkB,GAAG,qBAAqB;IA2BxD,aAAa,CAAC,GAAG,EAAE,kBAAkB,GAAG,qBAAqB;IAI7D,MAAM,CAAC,GAAG,EAAE,kBAAkB,GAAG,QAAQ,EAAE;IAc3C,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CASvC"}
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAE/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6CAA6C,CAAC;AACzF,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAMhG;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,yBAAyB,CAAC;CAC5C;AAyBD,qBAAa,gBAAiB,YAAW,kBAAkB;;IACzD,QAAQ,CAAC,EAAE,WAAW;IACtB;;;;OAIG;IACH,QAAQ,CAAC,yBAAyB,QAAQ;gBAU9B,MAAM,EAAE,sBAAsB;IAS1C,QAAQ,CAAC,GAAG,EAAE,kBAAkB,GAAG,qBAAqB;IA2BxD,aAAa,CAAC,GAAG,EAAE,kBAAkB,GAAG,qBAAqB;IAI7D,MAAM,CAAC,GAAG,EAAE,kBAAkB,GAAG,QAAQ,EAAE;IAc3C,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CASvC"}
@@ -2,18 +2,14 @@ import { createSlackConnectRoutes } from "./connect-route.js";
2
2
  import { SlackFeedPublisher } from "./feed-publisher.js";
3
3
  import { createSlackChannelsConfig } from "./slack.js";
4
4
  //#region src/integrations/slack/integration.ts
5
- const factoryTypingStatus = (chunk, ctx) => {
5
+ const factoryTypingStatus = (chunk) => {
6
6
  switch (chunk.type) {
7
- case "reasoning-delta": return `is thinking${nextDots(ctx.currentStatus)}`;
7
+ case "reasoning-delta": return "is thinking...";
8
8
  case "text-delta": return "is typing...";
9
- case "tool-call": return `is working${nextDots(ctx.currentStatus)}`;
9
+ case "tool-call": return "is working...";
10
10
  default: return;
11
11
  }
12
12
  };
13
- function nextDots(currentStatus) {
14
- const dotCount = currentStatus?.match(/\.*$/)?.[0].length ?? 1;
15
- return ".".repeat(dotCount >= 5 ? 2 : dotCount + 1);
16
- }
17
13
  /**
18
14
  * Drops keys the caller set to `undefined` so spreading the overrides cannot
19
15
  * erase the Factory defaults they sit on top of.
@@ -1 +1 @@
1
- {"version":3,"file":"integration.js","names":["#config","#repoBackedSessions"],"sources":["../../../src/integrations/slack/integration.ts"],"sourcesContent":["/**\n * `SlackIntegration` — Slack as a `FactoryIntegration`.\n *\n * Slack contributes two things to a factory: the chat channels that carry\n * inbound messages into agent runs (`channels()`), and the browser-facing\n * account-link routes that bind a Slack sender to a Mastra tenant (`routes()`).\n * Both used to be assembled by hand in the deploy entry, which had to reach\n * into the prepared agent controller by string key, resolve storage domains\n * itself, mint its own state signer, and splice routes onto the factory's\n * assembled server config. Implementing the integration interface instead means\n * the factory does all of that the same way it already does for GitHub and\n * Linear, and the entry's job shrinks to reading Slack's env vars once.\n *\n * Slack ships as a factory built-in alongside GitHub and Linear (it originally\n * lived in `mastracode/web` to keep `@mastra/slack`/`chat` out of this package;\n * the team reversed that so `create-factory` consumers get Slack channels out\n * of the box). The integration interface remains open — third parties still add\n * capabilities by implementing `FactoryIntegration` from outside the package.\n */\n\nimport type { TypingStatusFn } from '@mastra/core/channels';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { SlackAdapterChannelConfig } from '@mastra/slack';\n\nimport type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';\nimport type { FactoryChannelsConfig, FactoryIntegration, IntegrationContext } from '../base.js';\n\nimport { createSlackConnectRoutes } from './connect-route.js';\nimport { SlackFeedPublisher } from './feed-publisher.js';\nimport { createSlackChannelsConfig } from './slack.js';\n\n/**\n * Slack app credentials, read from env ONCE by the deploy entry. `signingSecret`\n * is required because the Slack adapter validates it at construction — an\n * integration constructed without it would throw during `prepare()` rather than\n * reporting itself unconfigured.\n */\nexport interface SlackIntegrationConfig {\n /** Verifies inbound Slack request signatures. Required. */\n signingSecret: string;\n /** Bot token used to post replies and ephemeral cards. */\n botToken?: string;\n /**\n * OAuth client credentials. Required for account linking: \"Sign in with\n * Slack\" (OIDC) is the only flow that writes a link, so without these the\n * settings surface reports `canConnect: false` and no sender can link.\n */\n clientId?: string;\n clientSecret?: string;\n /**\n * HTTPS origin Slack redirects back to. Slack requires HTTPS, so locally this\n * is the tunnel origin rather than the app's own public URL.\n */\n oidcRedirectBaseUrl?: string;\n /** SPA origin the post-connect redirect returns to. */\n uiOrigin?: string;\n /**\n * Overrides for the Slack channel adapter entry.\n *\n * @example\n * ```ts\n * new SlackIntegration({\n * signingSecret,\n * adapterOptions: {\n * streaming: true,\n * toolDisplay: 'grouped',\n * },\n * });\n * ```\n */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\nconst factoryTypingStatus: TypingStatusFn = (chunk, ctx) => {\n switch (chunk.type) {\n case 'reasoning-delta':\n return `is thinking${nextDots(ctx.currentStatus)}`;\n case 'text-delta':\n return 'is typing...';\n case 'tool-call':\n return `is working${nextDots(ctx.currentStatus)}`;\n default:\n return undefined;\n }\n};\n\nfunction nextDots(currentStatus: string | undefined): string {\n const dotCount = currentStatus?.match(/\\.*$/)?.[0].length ?? 1;\n return '.'.repeat(dotCount >= 5 ? 2 : dotCount + 1);\n}\n\n/**\n * Drops keys the caller set to `undefined` so spreading the overrides cannot\n * erase the Factory defaults they sit on top of.\n */\nfunction adapterOverrides(options: SlackAdapterChannelConfig | undefined): SlackAdapterChannelConfig {\n return Object.fromEntries(\n Object.entries(options ?? {}).filter(([, value]) => value !== undefined),\n ) as SlackAdapterChannelConfig;\n}\n\nexport class SlackIntegration implements FactoryIntegration {\n readonly id = 'slack';\n /**\n * The OIDC connect flow round-trips a signed `state` through Slack, so the\n * replica handling the callback must be able to verify a state a different\n * replica signed.\n */\n readonly requiresStableStateSigner = true;\n\n readonly #config: SlackIntegrationConfig;\n /**\n * Whether `channels()` found a source-control owner on the context and wired\n * repo-backed sessions. Set at the channels() attach path, which runs once at\n * boot before diagnostics are served.\n */\n #repoBackedSessions = false;\n\n constructor(config: SlackIntegrationConfig) {\n if (!config.signingSecret) {\n throw new Error(\n \"SlackIntegration: 'signingSecret' is required — Slack cannot verify inbound requests without it.\",\n );\n }\n this.#config = config;\n }\n\n channels(ctx: IntegrationContext): FactoryChannelsConfig {\n // Repo-backed sessions select the source-control provider from the linked\n // repository on each Factory project.\n const sourceControls =\n ctx.storage.sourceControls ?? (ctx.storage.sourceControlOwner ? [ctx.storage.sourceControlOwner] : []);\n this.#repoBackedSessions = sourceControls.length > 0;\n return createSlackChannelsConfig({\n slack: {\n clientId: this.#config.clientId,\n clientSecret: this.#config.clientSecret,\n signingSecret: this.#config.signingSecret,\n botToken: this.#config.botToken,\n },\n accountLinks: ctx.storage.channelIdentity,\n projects: ctx.storage.projects,\n sourceControls,\n memorySettings: ctx.storage.memorySettings,\n workItems: ctx.runtime?.workItems,\n feed: ctx.feed,\n adapterOptions: {\n toolDisplay: 'hidden',\n typingStatus: factoryTypingStatus,\n ...adapterOverrides(this.#config.adapterOptions),\n },\n });\n }\n\n feedPublisher(ctx: IntegrationContext): WorkItemFeedPublisher {\n return new SlackFeedPublisher({ controller: ctx.controller });\n }\n\n routes(ctx: IntegrationContext): ApiRoute[] {\n const { clientId, clientSecret, oidcRedirectBaseUrl, uiOrigin } = this.#config;\n return createSlackConnectRoutes({\n auth: ctx.auth,\n accountLinks: ctx.storage.channelIdentity,\n tenantStateSigner: ctx.stateSigner,\n oidc:\n clientId && clientSecret && oidcRedirectBaseUrl\n ? { clientId, clientSecret, redirectBaseUrl: oidcRedirectBaseUrl, uiOrigin }\n : undefined,\n projects: ctx.storage.projects,\n });\n }\n\n diagnostics(): Record<string, unknown> {\n const { clientId, clientSecret, botToken, oidcRedirectBaseUrl } = this.#config;\n return {\n configured: true,\n botTokenConfigured: Boolean(botToken),\n oidcConfigured: Boolean(clientId && clientSecret && oidcRedirectBaseUrl),\n repoBackedSessions: this.#repoBackedSessions,\n };\n }\n}\n"],"mappings":";;;;AAyEA,MAAM,uBAAuC,OAAO,QAAQ;CAC1D,QAAQ,MAAM,MAAd;EACE,KAAK,mBACH,OAAO,cAAc,SAAS,IAAI,aAAa;EACjD,KAAK,cACH,OAAO;EACT,KAAK,aACH,OAAO,aAAa,SAAS,IAAI,aAAa;EAChD,SACE;CACJ;AACF;AAEA,SAAS,SAAS,eAA2C;CAC3D,MAAM,WAAW,eAAe,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,UAAU;CAC7D,OAAO,IAAI,OAAO,YAAY,IAAI,IAAI,WAAW,CAAC;AACpD;;;;;AAMA,SAAS,iBAAiB,SAA2E;CACnG,OAAO,OAAO,YACZ,OAAO,QAAQ,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CACzE;AACF;AAEA,IAAa,mBAAb,MAA4D;CAC1D,KAAc;;;;;;CAMd,4BAAqC;CAErC;;;;;;CAMA,sBAAsB;CAEtB,YAAY,QAAgC;EAC1C,IAAI,CAAC,OAAO,eACV,MAAM,IAAI,MACR,kGACF;EAEF,KAAKA,UAAU;CACjB;CAEA,SAAS,KAAgD;EAGvD,MAAM,iBACJ,IAAI,QAAQ,mBAAmB,IAAI,QAAQ,qBAAqB,CAAC,IAAI,QAAQ,kBAAkB,IAAI,CAAC;EACtG,KAAKC,sBAAsB,eAAe,SAAS;EACnD,OAAO,0BAA0B;GAC/B,OAAO;IACL,UAAU,KAAKD,QAAQ;IACvB,cAAc,KAAKA,QAAQ;IAC3B,eAAe,KAAKA,QAAQ;IAC5B,UAAU,KAAKA,QAAQ;GACzB;GACA,cAAc,IAAI,QAAQ;GAC1B,UAAU,IAAI,QAAQ;GACtB;GACA,gBAAgB,IAAI,QAAQ;GAC5B,WAAW,IAAI,SAAS;GACxB,MAAM,IAAI;GACV,gBAAgB;IACd,aAAa;IACb,cAAc;IACd,GAAG,iBAAiB,KAAKA,QAAQ,cAAc;GACjD;EACF,CAAC;CACH;CAEA,cAAc,KAAgD;EAC5D,OAAO,IAAI,mBAAmB,EAAE,YAAY,IAAI,WAAW,CAAC;CAC9D;CAEA,OAAO,KAAqC;EAC1C,MAAM,EAAE,UAAU,cAAc,qBAAqB,aAAa,KAAKA;EACvE,OAAO,yBAAyB;GAC9B,MAAM,IAAI;GACV,cAAc,IAAI,QAAQ;GAC1B,mBAAmB,IAAI;GACvB,MACE,YAAY,gBAAgB,sBACxB;IAAE;IAAU;IAAc,iBAAiB;IAAqB;GAAS,IACzE,KAAA;GACN,UAAU,IAAI,QAAQ;EACxB,CAAC;CACH;CAEA,cAAuC;EACrC,MAAM,EAAE,UAAU,cAAc,UAAU,wBAAwB,KAAKA;EACvE,OAAO;GACL,YAAY;GACZ,oBAAoB,QAAQ,QAAQ;GACpC,gBAAgB,QAAQ,YAAY,gBAAgB,mBAAmB;GACvE,oBAAoB,KAAKC;EAC3B;CACF;AACF"}
1
+ {"version":3,"file":"integration.js","names":["#config","#repoBackedSessions"],"sources":["../../../src/integrations/slack/integration.ts"],"sourcesContent":["/**\n * `SlackIntegration` — Slack as a `FactoryIntegration`.\n *\n * Slack contributes two things to a factory: the chat channels that carry\n * inbound messages into agent runs (`channels()`), and the browser-facing\n * account-link routes that bind a Slack sender to a Mastra tenant (`routes()`).\n * Both used to be assembled by hand in the deploy entry, which had to reach\n * into the prepared agent controller by string key, resolve storage domains\n * itself, mint its own state signer, and splice routes onto the factory's\n * assembled server config. Implementing the integration interface instead means\n * the factory does all of that the same way it already does for GitHub and\n * Linear, and the entry's job shrinks to reading Slack's env vars once.\n *\n * Slack ships as a factory built-in alongside GitHub and Linear (it originally\n * lived in `mastracode/web` to keep `@mastra/slack`/`chat` out of this package;\n * the team reversed that so `create-factory` consumers get Slack channels out\n * of the box). The integration interface remains open — third parties still add\n * capabilities by implementing `FactoryIntegration` from outside the package.\n */\n\nimport type { TypingStatusFn } from '@mastra/core/channels';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { SlackAdapterChannelConfig } from '@mastra/slack';\n\nimport type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';\nimport type { FactoryChannelsConfig, FactoryIntegration, IntegrationContext } from '../base.js';\n\nimport { createSlackConnectRoutes } from './connect-route.js';\nimport { SlackFeedPublisher } from './feed-publisher.js';\nimport { createSlackChannelsConfig } from './slack.js';\n\n/**\n * Slack app credentials, read from env ONCE by the deploy entry. `signingSecret`\n * is required because the Slack adapter validates it at construction — an\n * integration constructed without it would throw during `prepare()` rather than\n * reporting itself unconfigured.\n */\nexport interface SlackIntegrationConfig {\n /** Verifies inbound Slack request signatures. Required. */\n signingSecret: string;\n /** Bot token used to post replies and ephemeral cards. */\n botToken?: string;\n /**\n * OAuth client credentials. Required for account linking: \"Sign in with\n * Slack\" (OIDC) is the only flow that writes a link, so without these the\n * settings surface reports `canConnect: false` and no sender can link.\n */\n clientId?: string;\n clientSecret?: string;\n /**\n * HTTPS origin Slack redirects back to. Slack requires HTTPS, so locally this\n * is the tunnel origin rather than the app's own public URL.\n */\n oidcRedirectBaseUrl?: string;\n /** SPA origin the post-connect redirect returns to. */\n uiOrigin?: string;\n /**\n * Overrides for the Slack channel adapter entry.\n *\n * @example\n * ```ts\n * new SlackIntegration({\n * signingSecret,\n * adapterOptions: {\n * streaming: true,\n * toolDisplay: 'grouped',\n * },\n * });\n * ```\n */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\nconst factoryTypingStatus: TypingStatusFn = chunk => {\n switch (chunk.type) {\n case 'reasoning-delta':\n return 'is thinking...';\n case 'text-delta':\n return 'is typing...';\n case 'tool-call':\n return 'is working...';\n default:\n return undefined;\n }\n};\n\n/**\n * Drops keys the caller set to `undefined` so spreading the overrides cannot\n * erase the Factory defaults they sit on top of.\n */\nfunction adapterOverrides(options: SlackAdapterChannelConfig | undefined): SlackAdapterChannelConfig {\n return Object.fromEntries(\n Object.entries(options ?? {}).filter(([, value]) => value !== undefined),\n ) as SlackAdapterChannelConfig;\n}\n\nexport class SlackIntegration implements FactoryIntegration {\n readonly id = 'slack';\n /**\n * The OIDC connect flow round-trips a signed `state` through Slack, so the\n * replica handling the callback must be able to verify a state a different\n * replica signed.\n */\n readonly requiresStableStateSigner = true;\n\n readonly #config: SlackIntegrationConfig;\n /**\n * Whether `channels()` found a source-control owner on the context and wired\n * repo-backed sessions. Set at the channels() attach path, which runs once at\n * boot before diagnostics are served.\n */\n #repoBackedSessions = false;\n\n constructor(config: SlackIntegrationConfig) {\n if (!config.signingSecret) {\n throw new Error(\n \"SlackIntegration: 'signingSecret' is required — Slack cannot verify inbound requests without it.\",\n );\n }\n this.#config = config;\n }\n\n channels(ctx: IntegrationContext): FactoryChannelsConfig {\n // Repo-backed sessions select the source-control provider from the linked\n // repository on each Factory project.\n const sourceControls =\n ctx.storage.sourceControls ?? (ctx.storage.sourceControlOwner ? [ctx.storage.sourceControlOwner] : []);\n this.#repoBackedSessions = sourceControls.length > 0;\n return createSlackChannelsConfig({\n slack: {\n clientId: this.#config.clientId,\n clientSecret: this.#config.clientSecret,\n signingSecret: this.#config.signingSecret,\n botToken: this.#config.botToken,\n },\n accountLinks: ctx.storage.channelIdentity,\n projects: ctx.storage.projects,\n sourceControls,\n memorySettings: ctx.storage.memorySettings,\n workItems: ctx.runtime?.workItems,\n feed: ctx.feed,\n adapterOptions: {\n toolDisplay: 'hidden',\n typingStatus: factoryTypingStatus,\n ...adapterOverrides(this.#config.adapterOptions),\n },\n });\n }\n\n feedPublisher(ctx: IntegrationContext): WorkItemFeedPublisher {\n return new SlackFeedPublisher({ controller: ctx.controller });\n }\n\n routes(ctx: IntegrationContext): ApiRoute[] {\n const { clientId, clientSecret, oidcRedirectBaseUrl, uiOrigin } = this.#config;\n return createSlackConnectRoutes({\n auth: ctx.auth,\n accountLinks: ctx.storage.channelIdentity,\n tenantStateSigner: ctx.stateSigner,\n oidc:\n clientId && clientSecret && oidcRedirectBaseUrl\n ? { clientId, clientSecret, redirectBaseUrl: oidcRedirectBaseUrl, uiOrigin }\n : undefined,\n projects: ctx.storage.projects,\n });\n }\n\n diagnostics(): Record<string, unknown> {\n const { clientId, clientSecret, botToken, oidcRedirectBaseUrl } = this.#config;\n return {\n configured: true,\n botTokenConfigured: Boolean(botToken),\n oidcConfigured: Boolean(clientId && clientSecret && oidcRedirectBaseUrl),\n repoBackedSessions: this.#repoBackedSessions,\n };\n }\n}\n"],"mappings":";;;;AAyEA,MAAM,uBAAsC,UAAS;CACnD,QAAQ,MAAM,MAAd;EACE,KAAK,mBACH,OAAO;EACT,KAAK,cACH,OAAO;EACT,KAAK,aACH,OAAO;EACT,SACE;CACJ;AACF;;;;;AAMA,SAAS,iBAAiB,SAA2E;CACnG,OAAO,OAAO,YACZ,OAAO,QAAQ,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CACzE;AACF;AAEA,IAAa,mBAAb,MAA4D;CAC1D,KAAc;;;;;;CAMd,4BAAqC;CAErC;;;;;;CAMA,sBAAsB;CAEtB,YAAY,QAAgC;EAC1C,IAAI,CAAC,OAAO,eACV,MAAM,IAAI,MACR,kGACF;EAEF,KAAKA,UAAU;CACjB;CAEA,SAAS,KAAgD;EAGvD,MAAM,iBACJ,IAAI,QAAQ,mBAAmB,IAAI,QAAQ,qBAAqB,CAAC,IAAI,QAAQ,kBAAkB,IAAI,CAAC;EACtG,KAAKC,sBAAsB,eAAe,SAAS;EACnD,OAAO,0BAA0B;GAC/B,OAAO;IACL,UAAU,KAAKD,QAAQ;IACvB,cAAc,KAAKA,QAAQ;IAC3B,eAAe,KAAKA,QAAQ;IAC5B,UAAU,KAAKA,QAAQ;GACzB;GACA,cAAc,IAAI,QAAQ;GAC1B,UAAU,IAAI,QAAQ;GACtB;GACA,gBAAgB,IAAI,QAAQ;GAC5B,WAAW,IAAI,SAAS;GACxB,MAAM,IAAI;GACV,gBAAgB;IACd,aAAa;IACb,cAAc;IACd,GAAG,iBAAiB,KAAKA,QAAQ,cAAc;GACjD;EACF,CAAC;CACH;CAEA,cAAc,KAAgD;EAC5D,OAAO,IAAI,mBAAmB,EAAE,YAAY,IAAI,WAAW,CAAC;CAC9D;CAEA,OAAO,KAAqC;EAC1C,MAAM,EAAE,UAAU,cAAc,qBAAqB,aAAa,KAAKA;EACvE,OAAO,yBAAyB;GAC9B,MAAM,IAAI;GACV,cAAc,IAAI,QAAQ;GAC1B,mBAAmB,IAAI;GACvB,MACE,YAAY,gBAAgB,sBACxB;IAAE;IAAU;IAAc,iBAAiB;IAAqB;GAAS,IACzE,KAAA;GACN,UAAU,IAAI,QAAQ;EACxB,CAAC;CACH;CAEA,cAAuC;EACrC,MAAM,EAAE,UAAU,cAAc,UAAU,wBAAwB,KAAKA;EACvE,OAAO;GACL,YAAY;GACZ,oBAAoB,QAAQ,QAAQ;GACpC,gBAAgB,QAAQ,YAAY,gBAAgB,mBAAmB;GACvE,oBAAoB,KAAKC;EAC3B;CACF;AACF"}
@@ -124,6 +124,7 @@ export declare function readSessionWorkspaceFile(session: SourceControlSession,
124
124
  allowUnapprovedPath?: boolean;
125
125
  }): Promise<WorkspaceFile>;
126
126
  export declare function parseWorkspaceChanges(output: string): WorkspaceChange[];
127
+ export declare function parseWorkspaceTrackedChanges(output: string): WorkspaceChange[];
127
128
  export declare function parseWorkspaceChangeStats(output: string): Map<string, {
128
129
  additions?: number;
129
130
  deletions?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/routes/fs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/routes/fs.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAOpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,4EAA4E;IAC5E,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,wBAAwB;IACvC,2CAA2C;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,sBAAsB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,aAAa,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChC;AAED,MAAM,MAAM,qBAAqB,GAC7B,UAAU,GACV,OAAO,GACP,SAAS,GACT,SAAS,GACT,QAAQ,GACR,WAAW,GACX,YAAY,CAAC;AAEjB,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,sBAAsB,CAAC;AAEnD,MAAM,WAAW,eAAe;IAC9B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAkDD,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnD;AA0ED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuCnG;AAqCD,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,wBAAwB,CAAC,CAkBnC;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA4CjH;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAOjG;AAUD,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE;QAAE,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAA;KAAE,CAAC;IACtF,UAAU,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAC;CAClD;AA0BD,wBAAsB,0BAA0B,CAC9C,UAAU,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,EAChD,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,qBAAqB,CAAC,CAchC;AA6BD,iFAAiF;AACjF,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,oBAAoB,EAC7B,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,wBAAwB,CAAC,CAsCnC;AAED,kIAAkI;AAClI,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,oBAAoB,EAC7B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9C,OAAO,CAAC,aAAa,CAAC,CAyBxB;AAYD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,CAoBvE;AACD,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,CAiB9E;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM;gBAEV,MAAM;gBAAc,MAAM;aAAW,OAAO;GA+BzF;AAoBD,wBAAsB,2BAA2B,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgD1G;AAiBD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,oBAAoB,EAC7B,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,aAAa,CAAC,CA4DxB;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CAUrE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,aAAa,CAAA;CAAO,GAAG,QAAQ,EAAE,CA2LpG"}
package/dist/routes/fs.js CHANGED
@@ -13,12 +13,13 @@ const MAX_DIFF_BYTES = 512 * 1024;
13
13
  const WORKSPACE_NUMSTAT_SCRIPT = `
14
14
  set -e
15
15
  workdir=$1
16
+ base=$2
16
17
  index_file=$(mktemp)
17
18
  untracked_file=$(mktemp)
18
19
  object_dir=$(mktemp -d)
19
20
  trap 'rm -f "$index_file" "$untracked_file"; rm -rf "$object_dir"' EXIT
20
21
  rm -f "$index_file"
21
- git -C "$workdir" diff --numstat -z --find-renames --no-ext-diff --no-textconv HEAD
22
+ git -C "$workdir" diff --numstat -z --find-renames --no-ext-diff --no-textconv "$base"
22
23
  git -C "$workdir" ls-files --others --exclude-standard -z >"$untracked_file"
23
24
  git_dir=$(git -C "$workdir" rev-parse --absolute-git-dir)
24
25
  export GIT_INDEX_FILE="$index_file"
@@ -413,6 +414,31 @@ function parseWorkspaceChanges(output) {
413
414
  }
414
415
  return changes.toSorted((a, b) => a.path.localeCompare(b.path));
415
416
  }
417
+ function parseWorkspaceTrackedChanges(output) {
418
+ const records = output.split("\0");
419
+ const changes = [];
420
+ for (let index = 0; index < records.length;) {
421
+ const code = records[index++];
422
+ if (!code) continue;
423
+ const status = changeStatus(code);
424
+ if (status === "renamed" || status === "copied") {
425
+ const previousPath = records[index++];
426
+ const path = records[index++];
427
+ if (path) changes.push({
428
+ path,
429
+ previousPath: previousPath || void 0,
430
+ status
431
+ });
432
+ continue;
433
+ }
434
+ const path = records[index++];
435
+ if (path) changes.push({
436
+ path,
437
+ status
438
+ });
439
+ }
440
+ return changes.toSorted((a, b) => a.path.localeCompare(b.path));
441
+ }
416
442
  function parseWorkspaceChangeStats(output) {
417
443
  const records = output.split("\0");
418
444
  const stats = /* @__PURE__ */ new Map();
@@ -452,24 +478,53 @@ function unavailableWorkspaceChanges(workspacePath) {
452
478
  changes: []
453
479
  };
454
480
  }
481
+ async function resolveSessionComparisonBase(session, sandbox, workdir) {
482
+ const result = await sandbox.executeCommand("git", [
483
+ "-C",
484
+ workdir,
485
+ "merge-base",
486
+ "HEAD",
487
+ `origin/${session.baseBranch}`
488
+ ], { timeout: 3e4 });
489
+ const sha = result.stdout.trim();
490
+ return result.exitCode === 0 && /^[0-9a-f]{40,64}$/i.test(sha) ? sha : "HEAD";
491
+ }
455
492
  async function listSessionWorkspaceChanges(session) {
456
493
  const handle = await sessionSandbox(session);
457
494
  if (!handle) return unavailableWorkspaceChanges(session.sessionId);
458
- const [statusResult, statsResult] = await Promise.all([handle.sandbox.executeCommand("git", [
459
- "-C",
460
- handle.workdir,
461
- "status",
462
- "--porcelain=v1",
463
- "-z",
464
- "--untracked-files=all"
465
- ], { timeout: 3e4 }), handle.sandbox.executeCommand("sh", [
466
- "-c",
467
- WORKSPACE_NUMSTAT_SCRIPT,
468
- "mastracode-numstat",
469
- handle.workdir
470
- ], { timeout: 3e4 })]);
495
+ const comparisonBase = await resolveSessionComparisonBase(session, handle.sandbox, handle.workdir);
496
+ const [statusResult, trackedResult, statsResult] = await Promise.all([
497
+ handle.sandbox.executeCommand("git", [
498
+ "-C",
499
+ handle.workdir,
500
+ "status",
501
+ "--porcelain=v1",
502
+ "-z",
503
+ "--untracked-files=all"
504
+ ], { timeout: 3e4 }),
505
+ handle.sandbox.executeCommand("git", [
506
+ "-C",
507
+ handle.workdir,
508
+ "diff",
509
+ "--name-status",
510
+ "-z",
511
+ "--find-renames",
512
+ comparisonBase
513
+ ], { timeout: 3e4 }),
514
+ handle.sandbox.executeCommand("sh", [
515
+ "-c",
516
+ WORKSPACE_NUMSTAT_SCRIPT,
517
+ "mastracode-numstat",
518
+ handle.workdir,
519
+ comparisonBase
520
+ ], { timeout: 3e4 })
521
+ ]);
471
522
  if (statusResult.exitCode !== 0) return unavailableWorkspaceChanges(session.sessionId);
472
- const changes = parseWorkspaceChanges(statusResult.stdout);
523
+ const statusChanges = parseWorkspaceChanges(statusResult.stdout);
524
+ const trackedChanges = trackedResult.exitCode === 0 ? parseWorkspaceTrackedChanges(trackedResult.stdout) : [];
525
+ const changesByPath = new Map(trackedChanges.map((change) => [change.path, change]));
526
+ for (const change of statusChanges) if (trackedResult.exitCode !== 0 || change.status === "untracked" || change.status === "conflicted") changesByPath.set(change.path, change);
527
+ const changes = [...changesByPath.values()].toSorted((a, b) => a.path.localeCompare(b.path));
473
528
  if (statsResult.exitCode !== 0) return {
474
529
  workspacePath: session.sessionId,
475
530
  available: true,
@@ -515,6 +570,7 @@ async function readSessionWorkspaceDiff(session, path, previousPath) {
515
570
  const safePreviousPath = previousPath ? assertRelativePath(previousPath, "previousPath") : void 0;
516
571
  const handle = await sessionSandbox(session);
517
572
  if (!handle) throw new Error("Session workspace is not available");
573
+ const comparisonBase = await resolveSessionComparisonBase(session, handle.sandbox, handle.workdir);
518
574
  const pathspecs = safePreviousPath ? [safePreviousPath, safePath] : [safePath];
519
575
  let result = await executeBoundedGitDiff(handle.sandbox, [
520
576
  "--literal-pathspecs",
@@ -525,7 +581,7 @@ async function readSessionWorkspaceDiff(session, path, previousPath) {
525
581
  "--no-ext-diff",
526
582
  "--no-color",
527
583
  "--unified=3",
528
- "HEAD",
584
+ comparisonBase,
529
585
  "--",
530
586
  ...pathspecs
531
587
  ]);
@@ -744,6 +800,6 @@ function buildFsRoutes(options = {}) {
744
800
  ];
745
801
  }
746
802
  //#endregion
747
- export { buildFsRoutes, listArtifacts, listDirectory, listSessionFilesystemFiles, listSessionRenderedPath, listSessionWorkspaceChanges, listWorkspaceRenderedPath, parseWorkspaceChangeStats, parseWorkspaceChanges, readSessionWorkspaceDiff, readSessionWorkspaceFile, readWorkspaceFile, resolveCodebase, resolveFsRoot };
803
+ export { buildFsRoutes, listArtifacts, listDirectory, listSessionFilesystemFiles, listSessionRenderedPath, listSessionWorkspaceChanges, listWorkspaceRenderedPath, parseWorkspaceChangeStats, parseWorkspaceChanges, parseWorkspaceTrackedChanges, readSessionWorkspaceDiff, readSessionWorkspaceFile, readWorkspaceFile, resolveCodebase, resolveFsRoot };
748
804
 
749
805
  //# sourceMappingURL=fs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","names":["posixPath"],"sources":["../../src/routes/fs.ts"],"sourcesContent":["import { lstat, open, readdir, realpath, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { isAbsolute, join, posix as posixPath, resolve, sep } from 'node:path';\n\nimport { SandboxFilesystem } from '@mastra/code-sdk/agents/sandbox-filesystem';\nimport { detectProject, getResourceIdOverride } from '@mastra/code-sdk/utils/project';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport { requireExec } from '../sandbox/materialization.js';\nimport type { ExecutableSandbox } from '../sandbox/materialization.js';\nimport { peekSessionSandbox } from '../sandbox/session-sandbox.js';\nimport { waitForPendingFilesystemCapture } from '../session/filesystem-capture.js';\nimport type { FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { SourceControlSession } from '../storage/domains/source-control/base.js';\nimport type { RouteAuth } from './route.js';\n\n/**\n * Server-side directory browser for the web project picker.\n *\n * The browser cannot read absolute filesystem paths (the File System Access API\n * only exposes a directory *name*), so the picker must ask the server — which\n * does have filesystem access — to enumerate directories. The result is real\n * absolute paths the user can select without typing.\n *\n * All access is confined to a configured `root` (default: the user's home\n * directory). Requests that try to escape the root via `..` or symlinks are\n * clamped back to the root.\n */\n\nexport interface DirectoryEntry {\n name: string;\n /** Absolute path to the entry. */\n path: string;\n}\n\nexport interface DirectoryListing {\n /** The allowed root; clients cannot browse above this. */\n root: string;\n /** The absolute path that was listed. */\n path: string;\n /** Parent directory path, or null when `path` is the root. */\n parent: string | null;\n /** Subdirectories of `path` (directories only, sorted, hidden excluded). */\n entries: DirectoryEntry[];\n}\n\nexport interface WorkspaceRenderedEntry {\n name: string;\n /** Path relative to the configured rendered root. */\n path: string;\n type: 'file' | 'directory';\n size: number;\n updatedAt: string;\n}\n\nexport interface WorkspaceRenderedListing {\n /** The confined workspace/project root. */\n workspacePath: string;\n /** Configured workspace-relative rendered root, e.g. `.artifacts`. */\n root: string;\n /** The confined absolute path for the rendered root. */\n rootPath: string;\n entries: WorkspaceRenderedEntry[];\n}\n\nexport interface WorkspaceFile {\n /** The confined workspace/project root. */\n workspacePath: string;\n /** Workspace-relative file path. */\n path: string;\n name: string;\n size: number;\n updatedAt: string;\n contentType: 'text' | 'unsupported';\n content?: string;\n truncated?: boolean;\n}\n\nexport interface WorkspaceFilesListing {\n /** The Factory session resource id. */\n workspacePath: string;\n /** The agent thread whose terminal file list was captured. */\n threadId: string;\n files: Array<{ path: string }>;\n}\n\nexport type WorkspaceChangeStatus =\n | 'modified'\n | 'added'\n | 'deleted'\n | 'renamed'\n | 'copied'\n | 'untracked'\n | 'conflicted';\n\nexport interface WorkspaceChange {\n path: string;\n previousPath?: string;\n status: WorkspaceChangeStatus;\n additions?: number;\n deletions?: number;\n binary?: boolean;\n}\n\nexport interface WorkspaceChanges {\n workspacePath: string;\n available: boolean;\n changes: WorkspaceChange[];\n additions?: number;\n deletions?: number;\n}\n\nexport interface WorkspaceDiff {\n workspacePath: string;\n path: string;\n patch: string;\n truncated: boolean;\n}\n\nexport type ArtifactEntry = WorkspaceRenderedEntry;\n\nexport interface ArtifactListing {\n /** The confined workspace/project root. */\n rootPath: string;\n /** The workspace artifact directory. */\n artifactsPath: string;\n entries: ArtifactEntry[];\n}\n\nconst MAX_TEXT_FILE_BYTES = 512 * 1024;\nconst MAX_DIFF_BYTES = 512 * 1024;\nconst WORKSPACE_NUMSTAT_SCRIPT = `\nset -e\nworkdir=$1\nindex_file=$(mktemp)\nuntracked_file=$(mktemp)\nobject_dir=$(mktemp -d)\ntrap 'rm -f \"$index_file\" \"$untracked_file\"; rm -rf \"$object_dir\"' EXIT\nrm -f \"$index_file\"\ngit -C \"$workdir\" diff --numstat -z --find-renames --no-ext-diff --no-textconv HEAD\ngit -C \"$workdir\" ls-files --others --exclude-standard -z >\"$untracked_file\"\ngit_dir=$(git -C \"$workdir\" rev-parse --absolute-git-dir)\nexport GIT_INDEX_FILE=\"$index_file\"\nexport GIT_OBJECT_DIRECTORY=\"$object_dir\"\nexport GIT_ALTERNATE_OBJECT_DIRECTORIES=\"$git_dir/objects\"\ngit -C \"$workdir\" read-tree --empty\ngit -C \"$workdir\" update-index --add -z --stdin <\"$untracked_file\"\nempty_tree=$(git -C \"$workdir\" hash-object -t tree /dev/null)\ngit -C \"$workdir\" diff --cached --numstat -z --no-ext-diff --no-textconv \"$empty_tree\"\n`;\nconst BOUNDED_GIT_DIFF_SCRIPT = `\nallow_exit_one=$1\nshift\nstatus_file=$(mktemp)\nstderr_file=$(mktemp)\ntrap 'rm -f \"$status_file\" \"$stderr_file\"' EXIT\n(\n git \"$@\" 2>\"$stderr_file\"\n printf '%s' \"$?\" >\"$status_file\"\n) | head -c ${MAX_DIFF_BYTES + 1}\nstatus=$(cat \"$status_file\")\ncase \"$status\" in\n 0|141) exit 0 ;;\n 1) [ \"$allow_exit_one\" = \"1\" ] && exit 0 ;;\nesac\ncat \"$stderr_file\" >&2\nexit \"\\${status:-1}\"\n`;\nconst TEXT_DECODER = new TextDecoder('utf-8', { fatal: true });\nconst APPROVED_RENDERED_ROOTS = new Set(['.artifacts']);\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\n/** Resolve the browsable root, defaulting to the user's home directory. */\nexport function resolveFsRoot(root?: string): string {\n return resolve(root && root.trim() ? root : homedir());\n}\n\n/** True when `candidate` is `root` or nested under it. */\nfunction isWithinRoot(candidate: string, root: string): boolean {\n if (candidate === root) return true;\n const rootWithSep = root.endsWith(sep) ? root : root + sep;\n return candidate.startsWith(rootWithSep);\n}\n\nasync function realOrResolved(path: string): Promise<string> {\n try {\n return await realpath(path);\n } catch {\n return path;\n }\n}\n\n/**\n * Resolve a path's real location (following symlinks) and confirm it stays\n * within `root`. Returns the real path when confined, or `null` when it escapes\n * the root or does not exist. Used so a symlink inside the root that points\n * outside it cannot be browsed or selected.\n */\nasync function realPathWithinRoot(candidate: string, root: string): Promise<string | null> {\n try {\n const real = await realpath(candidate);\n return isWithinRoot(real, root) ? real : null;\n } catch {\n return null;\n }\n}\n\nfunction assertRelativePath(path: string, label: string): string {\n const trimmed = path.trim();\n if (!trimmed) throw new Error(`Missing required query param: ${label}`);\n if (isAbsolute(trimmed)) throw new Error(`${label} must be relative`);\n if (trimmed.split(/[\\\\/]+/).includes('..')) throw new Error(`${label} escapes workspace`);\n const normalized = resolve('/', trimmed).slice(1);\n if (!normalized || normalized === '..' || normalized.startsWith(`..${sep}`))\n throw new Error(`${label} escapes workspace`);\n return normalized;\n}\n\nfunction assertApprovedRenderedRoot(renderedRoot: string): string {\n const safeRoot = assertRelativePath(renderedRoot, 'root');\n if (!APPROVED_RENDERED_ROOTS.has(safeRoot)) throw new Error('Root is not approved for rendered workspace access');\n return safeRoot;\n}\n\nasync function confinedWorkspacePath(\n root: string,\n workspacePath: string,\n): Promise<{ resolvedRoot: string; workspace: string }> {\n const resolvedRoot = await realOrResolved(resolveFsRoot(root));\n const candidate = isAbsolute(workspacePath) ? resolve(workspacePath) : resolve(resolvedRoot, workspacePath);\n const workspace = await realPathWithinRoot(candidate, resolvedRoot);\n if (!workspace) throw new Error('Path is outside the browsable root');\n return { resolvedRoot, workspace };\n}\n\nasync function confinedWorkspaceRelativePath(\n root: string,\n workspacePath: string,\n relativePath: string,\n): Promise<{ workspace: string; path: string; relativePath: string }> {\n const safeRelativePath = assertRelativePath(relativePath, 'path');\n const { workspace } = await confinedWorkspacePath(root, workspacePath);\n const candidate = resolve(workspace, safeRelativePath);\n if (!isWithinRoot(candidate, workspace)) throw new Error('Path escapes workspace');\n const confinedPath = await realPathWithinRoot(candidate, workspace);\n if (!confinedPath) throw new Error('Path is outside the workspace');\n return { workspace, path: confinedPath, relativePath: safeRelativePath };\n}\n\n/**\n * List the directories inside `requestedPath`, confined to `root`. An absent or\n * out-of-root path is clamped to the root, so the worst a malicious client can\n * do is browse within the allowed root.\n */\nexport async function listDirectory(root: string, requestedPath?: string): Promise<DirectoryListing> {\n // Resolve the root through symlinks so all confinement checks compare real\n // paths; a symlink that escapes the root is then reliably detectable.\n const resolvedRoot = await realOrResolved(resolveFsRoot(root));\n\n let target = resolvedRoot;\n if (requestedPath && requestedPath.trim()) {\n const candidate = isAbsolute(requestedPath) ? resolve(requestedPath) : resolve(resolvedRoot, requestedPath);\n // Follow symlinks and re-confirm the real target stays within the root.\n target = (await realPathWithinRoot(candidate, resolvedRoot)) ?? resolvedRoot;\n }\n\n // Confirm the target is a real directory; fall back to root otherwise.\n try {\n const info = await stat(target);\n if (!info.isDirectory()) target = resolvedRoot;\n } catch {\n target = resolvedRoot;\n }\n\n const dirents = await readdir(target, { withFileTypes: true });\n const entries: DirectoryEntry[] = [];\n for (const dirent of dirents) {\n if (dirent.name.startsWith('.')) continue; // skip dotfiles/dirs\n const entryPath = join(target, dirent.name);\n let isDir = dirent.isDirectory();\n if (dirent.isSymbolicLink()) {\n // Only surface symlinks whose real target is a directory inside the root,\n // so a link pointing outside the root can't be browsed or selected.\n const real = await realPathWithinRoot(entryPath, resolvedRoot);\n isDir = real ? (await stat(real).catch(() => null))?.isDirectory() === true : false;\n }\n if (isDir) entries.push({ name: dirent.name, path: entryPath });\n }\n entries.sort((a, b) => a.name.localeCompare(b.name));\n\n const parent = target === resolvedRoot ? null : resolve(target, '..');\n\n return { root: resolvedRoot, path: target, parent, entries };\n}\n\nasync function listRenderedEntries(rootPath: string, currentPath = rootPath): Promise<WorkspaceRenderedEntry[]> {\n const dirents = await readdir(currentPath, { withFileTypes: true });\n const entries: WorkspaceRenderedEntry[] = [];\n\n for (const dirent of dirents) {\n const entryPath = join(currentPath, dirent.name);\n const info = await lstat(entryPath);\n const relativePath = entryPath.slice(rootPath.length + 1);\n\n if (info.isDirectory()) {\n entries.push({\n name: dirent.name,\n path: relativePath,\n type: 'directory',\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n });\n entries.push(...(await listRenderedEntries(rootPath, entryPath)));\n continue;\n }\n\n if (info.isFile()) {\n entries.push({\n name: dirent.name,\n path: relativePath,\n type: 'file',\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n });\n }\n }\n\n return entries.sort((a, b) => a.path.localeCompare(b.path));\n}\n\nexport async function listWorkspaceRenderedPath(\n root: string,\n workspacePath: string,\n renderedRoot: string,\n): Promise<WorkspaceRenderedListing> {\n const safeRoot = assertApprovedRenderedRoot(renderedRoot);\n const { workspace } = await confinedWorkspacePath(root, workspacePath);\n const renderedPath = resolve(workspace, safeRoot);\n if (!isWithinRoot(renderedPath, workspace)) throw new Error('Root escapes workspace');\n\n const confinedRootPath = await realPathWithinRoot(renderedPath, workspace);\n if (!confinedRootPath) return { workspacePath: workspace, root: safeRoot, rootPath: renderedPath, entries: [] };\n\n const info = await stat(confinedRootPath);\n if (!info.isDirectory()) return { workspacePath: workspace, root: safeRoot, rootPath: confinedRootPath, entries: [] };\n\n return {\n workspacePath: workspace,\n root: safeRoot,\n rootPath: confinedRootPath,\n entries: await listRenderedEntries(confinedRootPath),\n };\n}\n\nexport async function readWorkspaceFile(root: string, workspacePath: string, path: string): Promise<WorkspaceFile> {\n const safePath = assertRelativePath(path, 'path');\n const relativeRoot = safePath.split('/')[0] ?? '';\n assertApprovedRenderedRoot(relativeRoot);\n const {\n workspace,\n path: confinedPath,\n relativePath,\n } = await confinedWorkspaceRelativePath(root, workspacePath, path);\n const info = await lstat(confinedPath);\n if (info.isDirectory()) throw new Error('Path is a directory');\n if (!info.isFile()) throw new Error('Unsupported file type');\n\n const bytesToRead = Math.min(info.size, MAX_TEXT_FILE_BYTES);\n const contentBuffer = Buffer.alloc(bytesToRead);\n const handle = await open(confinedPath, 'r');\n try {\n await handle.read(contentBuffer, 0, bytesToRead, 0);\n } finally {\n await handle.close();\n }\n\n try {\n const content = TEXT_DECODER.decode(contentBuffer);\n return {\n workspacePath: workspace,\n path: relativePath,\n name: relativePath.split('/').pop() ?? relativePath,\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n contentType: 'text',\n content,\n truncated: info.size > MAX_TEXT_FILE_BYTES,\n };\n } catch {\n return {\n workspacePath: workspace,\n path: relativePath,\n name: relativePath.split('/').pop() ?? relativePath,\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n contentType: 'unsupported',\n };\n }\n}\n\nexport async function listArtifacts(root: string, workspacePath: string): Promise<ArtifactListing> {\n const listing = await listWorkspaceRenderedPath(root, workspacePath, '.artifacts');\n return {\n rootPath: listing.workspacePath,\n artifactsPath: listing.rootPath,\n entries: listing.entries,\n };\n}\n\n// ── Session-backed workspace access ──────────────────────────────────────────\n//\n// The web UI identifies a Factory session workspace by its session id (a UUID),\n// not by a server-local filesystem path — the session's files live inside the\n// session's sandbox (a remote VM on deployed factories). These helpers resolve\n// the session, enforce that the caller owns it, reattach to its sandbox, and\n// serve the approved rendered roots through `SandboxFilesystem`.\n\n/** Dependencies for resolving a `workspacePath` that is a Factory session id. */\nexport interface SessionFsDeps {\n auth: RouteAuth;\n sessions: { getBySessionId(sessionId: string): Promise<SourceControlSession | null> };\n filesystem: Pick<FilesystemStorage, 'listFiles'>;\n}\n\n/**\n * Resolve a `workspacePath` query param as a Factory session id. Returns the\n * session when one exists and the caller owns it, `null` when no session\n * matches (the caller should fall back to local-path handling), and throws\n * when a session exists but belongs to another tenant.\n */\nasync function resolveAuthorizedSession(\n c: Context,\n deps: SessionFsDeps | undefined,\n workspacePath: string,\n): Promise<SourceControlSession | null> {\n if (!deps) return null;\n const session = await deps.sessions.getBySessionId(workspacePath);\n if (!session) return null;\n if (deps.auth.enabled()) {\n await deps.auth.ensureUser(c);\n const tenant = deps.auth.tenant(c);\n if (!tenant || tenant.orgId !== session.orgId || tenant.userId !== session.userId) {\n throw new Error('Session is not available to the current user');\n }\n }\n return session;\n}\n\nexport async function listSessionFilesystemFiles(\n filesystem: Pick<FilesystemStorage, 'listFiles'>,\n session: SourceControlSession,\n threadId: string,\n): Promise<WorkspaceFilesListing> {\n const safeThreadId = threadId.trim();\n if (!safeThreadId) throw new Error('Missing required query param: threadId');\n\n // The turn-end capture no longer gates agent_end, so a reader refetching on\n // run completion could otherwise race it and serve the previous turn's\n // listing. Await the in-flight capture (bounded) before reading.\n await waitForPendingFilesystemCapture(session.sessionId);\n\n return {\n workspacePath: session.sessionId,\n threadId: safeThreadId,\n files: await filesystem.listFiles({ resourceId: session.sessionId, threadId: safeThreadId }),\n };\n}\n\ninterface SessionSandboxHandle {\n sandbox: ExecutableSandbox;\n filesystem: SandboxFilesystem;\n workdir: string;\n}\n\n/**\n * Resolve the session's sandbox from the per-process memo and wrap its\n * workdir in a `SandboxFilesystem`. Returns `null` when the session has no\n * sandbox in this process (never opened here, or evicted by retirement).\n * This is a passive read path, so it never constructs or provisions: the\n * session's files come back the next time the workspace is actually opened\n * (e.g. by sending a message).\n */\nasync function sessionSandbox(session: SourceControlSession): Promise<SessionSandboxHandle | null> {\n const entry = peekSessionSandbox(session.id);\n // An unresolved workdir means the sandbox never started in this process —\n // nothing is materialized, so there are no files to browse.\n if (!entry?.workdir) return null;\n const sandbox = requireExec(entry.sandbox);\n return {\n sandbox,\n filesystem: new SandboxFilesystem({ sandbox, workdir: entry.workdir }),\n workdir: entry.workdir,\n };\n}\n\n/** List an approved rendered root inside a Factory session's sandbox workdir. */\nexport async function listSessionRenderedPath(\n session: SourceControlSession,\n renderedRoot: string,\n): Promise<WorkspaceRenderedListing> {\n const safeRoot = assertApprovedRenderedRoot(renderedRoot);\n const handle = await sessionSandbox(session);\n const rootPath = posixPath.join(handle?.workdir ?? '', safeRoot);\n const empty: WorkspaceRenderedListing = { workspacePath: session.sessionId, root: safeRoot, rootPath, entries: [] };\n if (!handle) return empty;\n\n // One round trip: emit \"type\\tsize\\tmtime\\tpath\" per entry. `safeRoot` comes\n // from a fixed allowlist so interpolating it (quoted) is safe.\n const quotedRoot = `'${rootPath.replace(/'/g, `'\\\\''`)}'`;\n const result = await handle.sandbox.executeCommand(\n 'sh',\n [\n '-c',\n `test -d ${quotedRoot} && find ${quotedRoot} -mindepth 1 -printf '%y\\\\t%s\\\\t%T@\\\\t%p\\\\n' 2>/dev/null || true`,\n ],\n { timeout: 30_000 },\n );\n if (result.exitCode !== 0) return empty;\n\n const entries: WorkspaceRenderedEntry[] = [];\n for (const line of result.stdout.split('\\n')) {\n if (!line) continue;\n const [type, sizeStr, mtimeStr, ...pathParts] = line.split('\\t');\n const fullPath = pathParts.join('\\t');\n if (!fullPath || !fullPath.startsWith(`${rootPath}/`)) continue;\n const relativePath = fullPath.slice(rootPath.length + 1);\n entries.push({\n name: posixPath.basename(relativePath),\n path: relativePath,\n type: type === 'd' ? 'directory' : 'file',\n size: type === 'd' ? 0 : Number(sizeStr) || 0,\n updatedAt: new Date((Number(mtimeStr) || 0) * 1000).toISOString(),\n });\n }\n entries.sort((a, b) => a.path.localeCompare(b.path));\n\n return { workspacePath: session.sessionId, root: safeRoot, rootPath, entries };\n}\n\n/** Read a file inside a session's sandbox. Paths outside rendered roots require a persisted-file allowlist check in the route. */\nexport async function readSessionWorkspaceFile(\n session: SourceControlSession,\n path: string,\n options: { allowUnapprovedPath?: boolean } = {},\n): Promise<WorkspaceFile> {\n const safePath = assertRelativePath(path, 'path');\n if (!options.allowUnapprovedPath) assertApprovedRenderedRoot(safePath.split('/')[0] ?? '');\n\n const handle = await sessionSandbox(session);\n if (!handle) throw new Error('Session workspace is not available');\n const { filesystem } = handle;\n const info = await filesystem.stat(safePath);\n if (info.type === 'directory') throw new Error('Path is a directory');\n\n const buffer = (await filesystem.readFile(safePath)) as Buffer;\n const truncated = buffer.length > MAX_TEXT_FILE_BYTES;\n const base = {\n workspacePath: session.sessionId,\n path: safePath,\n name: posixPath.basename(safePath),\n size: buffer.length,\n updatedAt: info.modifiedAt.toISOString(),\n };\n try {\n const content = TEXT_DECODER.decode(truncated ? buffer.subarray(0, MAX_TEXT_FILE_BYTES) : buffer);\n return { ...base, contentType: 'text', content, truncated };\n } catch {\n return { ...base, contentType: 'unsupported' };\n }\n}\n\nfunction changeStatus(code: string): WorkspaceChangeStatus {\n if (code === '??') return 'untracked';\n if (code.includes('U') || code === 'AA' || code === 'DD') return 'conflicted';\n if (code.includes('R')) return 'renamed';\n if (code.includes('C')) return 'copied';\n if (code.includes('D')) return 'deleted';\n if (code.includes('A')) return 'added';\n return 'modified';\n}\n\nexport function parseWorkspaceChanges(output: string): WorkspaceChange[] {\n const records = output.split('\\0');\n const changes: WorkspaceChange[] = [];\n\n for (let index = 0; index < records.length; index += 1) {\n const record = records[index];\n if (!record || record.length < 4) continue;\n const code = record.slice(0, 2);\n const path = record.slice(3);\n const status = changeStatus(code);\n if (status === 'renamed' || status === 'copied') {\n const previousPath = records[index + 1];\n if (previousPath) index += 1;\n changes.push({ path, previousPath: previousPath || undefined, status });\n continue;\n }\n changes.push({ path, status });\n }\n\n return changes.toSorted((a, b) => a.path.localeCompare(b.path));\n}\n\nexport function parseWorkspaceChangeStats(output: string) {\n const records = output.split('\\0');\n const stats = new Map<string, { additions?: number; deletions?: number; binary?: boolean }>();\n\n for (let index = 0; index < records.length; index += 1) {\n const record = records[index];\n if (!record) continue;\n const firstTab = record.indexOf('\\t');\n const secondTab = record.indexOf('\\t', firstTab + 1);\n if (firstTab < 0 || secondTab < 0) continue;\n\n const additionsText = record.slice(0, firstTab);\n const deletionsText = record.slice(firstTab + 1, secondTab);\n let path = record.slice(secondTab + 1);\n if (!path) {\n const renamedPath = records[index + 2];\n if (!renamedPath) continue;\n path = renamedPath;\n index += 2;\n }\n if (path.startsWith('./')) path = path.slice(2);\n\n if (additionsText === '-' || deletionsText === '-') {\n stats.set(path, { binary: true });\n continue;\n }\n\n const additions = Number(additionsText);\n const deletions = Number(deletionsText);\n if (Number.isFinite(additions) && Number.isFinite(deletions)) stats.set(path, { additions, deletions });\n }\n\n return stats;\n}\n\nfunction unavailableWorkspaceChanges(workspacePath: string): WorkspaceChanges {\n return { workspacePath, available: false, changes: [] };\n}\n\nexport async function listSessionWorkspaceChanges(session: SourceControlSession): Promise<WorkspaceChanges> {\n const handle = await sessionSandbox(session);\n if (!handle) return unavailableWorkspaceChanges(session.sessionId);\n\n const [statusResult, statsResult] = await Promise.all([\n handle.sandbox.executeCommand(\n 'git',\n ['-C', handle.workdir, 'status', '--porcelain=v1', '-z', '--untracked-files=all'],\n { timeout: 30_000 },\n ),\n handle.sandbox.executeCommand('sh', ['-c', WORKSPACE_NUMSTAT_SCRIPT, 'mastracode-numstat', handle.workdir], {\n timeout: 30_000,\n }),\n ]);\n if (statusResult.exitCode !== 0) return unavailableWorkspaceChanges(session.sessionId);\n\n const changes = parseWorkspaceChanges(statusResult.stdout);\n if (statsResult.exitCode !== 0) {\n return { workspacePath: session.sessionId, available: true, changes };\n }\n\n const stats = parseWorkspaceChangeStats(statsResult.stdout);\n let additions = 0;\n let deletions = 0;\n const changesWithStats = changes.map(change => {\n const changeStats = stats.get(change.path);\n additions += changeStats?.additions ?? 0;\n deletions += changeStats?.deletions ?? 0;\n return { ...change, ...changeStats };\n });\n\n return { workspacePath: session.sessionId, available: true, changes: changesWithStats, additions, deletions };\n}\n\nasync function executeBoundedGitDiff(sandbox: ExecutableSandbox, args: string[], allowExitOne = false) {\n return sandbox.executeCommand(\n 'sh',\n ['-c', BOUNDED_GIT_DIFF_SCRIPT, 'mastracode-diff', allowExitOne ? '1' : '0', ...args],\n { timeout: 30_000 },\n );\n}\n\nfunction truncatePatch(patchBuffer: Buffer): string {\n const patch = patchBuffer.subarray(0, MAX_DIFF_BYTES).toString('utf8');\n const lastNewline = patch.lastIndexOf('\\n');\n if (lastNewline >= 0) return patch.slice(0, lastNewline + 1);\n return patch.replace(/\\uFFFD+$/, '');\n}\n\nexport async function readSessionWorkspaceDiff(\n session: SourceControlSession,\n path: string,\n previousPath?: string,\n): Promise<WorkspaceDiff> {\n const safePath = assertRelativePath(path, 'path');\n const safePreviousPath = previousPath ? assertRelativePath(previousPath, 'previousPath') : undefined;\n const handle = await sessionSandbox(session);\n if (!handle) throw new Error('Session workspace is not available');\n\n const pathspecs = safePreviousPath ? [safePreviousPath, safePath] : [safePath];\n let result = await executeBoundedGitDiff(handle.sandbox, [\n '--literal-pathspecs',\n '-C',\n handle.workdir,\n 'diff',\n '--find-renames',\n '--no-ext-diff',\n '--no-color',\n '--unified=3',\n 'HEAD',\n '--',\n ...pathspecs,\n ]);\n if (result.exitCode !== 0) throw new Error(result.stderr || 'Unable to read workspace diff');\n\n if (!result.stdout) {\n const untracked = await handle.sandbox.executeCommand(\n 'git',\n ['--literal-pathspecs', '-C', handle.workdir, 'ls-files', '--others', '--exclude-standard', '--', safePath],\n { timeout: 30_000 },\n );\n if (untracked.exitCode === 0 && untracked.stdout.trim()) {\n result = await executeBoundedGitDiff(\n handle.sandbox,\n [\n '-C',\n handle.workdir,\n 'diff',\n '--no-index',\n '--no-ext-diff',\n '--no-color',\n '--unified=3',\n '--',\n '/dev/null',\n safePath,\n ],\n true,\n );\n if (result.exitCode !== 0 && result.exitCode !== 1) {\n throw new Error(result.stderr || 'Unable to read workspace diff');\n }\n }\n }\n\n const patchBuffer = Buffer.from(result.stdout);\n const truncated = patchBuffer.length > MAX_DIFF_BYTES;\n return {\n workspacePath: session.sessionId,\n path: safePath,\n patch: truncated ? truncatePatch(patchBuffer) : result.stdout,\n truncated,\n };\n}\n\nexport interface ResolvedCodebase {\n /**\n * The resourceId the TUI would use for this path — derived identically so a\n * project opened in the terminal and in the web app resolve to the SAME\n * session (and therefore the same threads).\n */\n resourceId: string;\n name: string;\n rootPath: string;\n gitUrl?: string;\n gitBranch?: string;\n}\n\n/**\n * Resolve a project path to the same resourceId the TUI uses. Mirrors\n * `createMastraCode`: detect the project, then apply any resourceId override\n * (MASTRA_RESOURCE_ID env var or `.mastracode/database.json`). This is the\n * shared continuity point — start in the TUI, continue on the web, same path\n * → same resourceId → same session.\n */\nexport function resolveCodebase(projectPath: string): ResolvedCodebase {\n const info = detectProject(projectPath);\n const override = getResourceIdOverride(info.rootPath);\n return {\n resourceId: override ?? info.resourceId,\n name: info.name,\n rootPath: info.rootPath,\n gitUrl: info.gitUrl,\n gitBranch: info.gitBranch,\n };\n}\n\n/**\n * Build the web filesystem routes as Mastra `apiRoutes`:\n * - `GET /web/fs/list?path=...` — browse directories (confined to root)\n * - `GET /web/codebase/resolve?path=...` — TUI-compatible codebase resourceId\n */\nexport function buildFsRoutes(options: { root?: string; sessionFs?: SessionFsDeps } = {}): ApiRoute[] {\n const root = resolveFsRoot(options.root);\n const sessionFs = options.sessionFs;\n\n return [\n registerApiRoute('/web/fs/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n try {\n const listing = await listDirectory(root, path);\n return c.json(listing);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n },\n }),\n registerApiRoute('/web/artifacts/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n try {\n return c.json(await listArtifacts(root, path));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status = message === 'Path is outside the browsable root' ? 403 : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/rendered/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const renderedRoot = c.req.query('root');\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!renderedRoot) return c.json({ error: 'Missing required query param: root' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (session && sessionFs) {\n return c.json(await listSessionRenderedPath(session, renderedRoot));\n }\n return c.json(await listWorkspaceRenderedPath(root, workspacePath, renderedRoot));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status =\n message.includes('outside') ||\n message.includes('relative') ||\n message.includes('escapes') ||\n message.includes('not approved') ||\n message.includes('not available')\n ? 403\n : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/files', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const threadId = c.req.query('threadId')?.trim();\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!threadId) return c.json({ error: 'Missing required query param: threadId' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (!session || !sessionFs) return c.json({ error: 'Session workspace is not available' }, 403);\n return c.json(await listSessionFilesystemFiles(sessionFs.filesystem, session, threadId));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status = message.includes('not available') ? 403 : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/changes', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (!session || !sessionFs) return c.json(unavailableWorkspaceChanges(workspacePath));\n return c.json(await listSessionWorkspaceChanges(session));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, message.includes('not available') ? 403 : 500);\n }\n },\n }),\n registerApiRoute('/web/workspace/changes/diff', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const path = c.req.query('path');\n const previousPath = c.req.query('previousPath');\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (!session || !sessionFs) return c.json({ error: 'Session workspace is not available' }, 403);\n return c.json(await readSessionWorkspaceDiff(session, path, previousPath));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status =\n message.includes('relative') || message.includes('escapes') || message.includes('not available')\n ? 403\n : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/file', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const path = c.req.query('path');\n const requestedThreadId = c.req.query('threadId');\n const threadId = requestedThreadId?.trim();\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n if (requestedThreadId !== undefined && !threadId) {\n return c.json({ error: 'Missing required query param: threadId' }, 400);\n }\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (session && sessionFs) {\n if (threadId) {\n const safePath = assertRelativePath(path, 'path');\n const listing = await listSessionFilesystemFiles(sessionFs.filesystem, session, threadId);\n if (!listing.files.some(file => file.path === safePath)) {\n return c.json({ error: 'Path is not available for this thread' }, 404);\n }\n return c.json(await readSessionWorkspaceFile(session, safePath, { allowUnapprovedPath: true }));\n }\n return c.json(await readSessionWorkspaceFile(session, path));\n }\n if (threadId) return c.json({ error: 'Session workspace is not available' }, 403);\n return c.json(await readWorkspaceFile(root, workspacePath, path));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status =\n message.includes('outside') ||\n message.includes('relative') ||\n message.includes('escapes') ||\n message.includes('not approved') ||\n message.includes('not available')\n ? 403\n : message.includes('directory')\n ? 400\n : message.includes('not found')\n ? 404\n : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/codebase/resolve', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n // Confine resolution to the browsable root (following symlinks), so this\n // endpoint can't be used to probe arbitrary filesystem paths. The web UI\n // only ever resolves directories the user picked via the root-confined\n // browser, so legitimate requests are always within the root.\n const confined = await realPathWithinRoot(isAbsolute(path) ? resolve(path) : resolve(root, path), root);\n if (!confined) return c.json({ error: 'Path is outside the browsable root' }, 403);\n try {\n return c.json(resolveCodebase(confined));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n },\n }),\n ];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fs.js","names":["posixPath"],"sources":["../../src/routes/fs.ts"],"sourcesContent":["import { lstat, open, readdir, realpath, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { isAbsolute, join, posix as posixPath, resolve, sep } from 'node:path';\n\nimport { SandboxFilesystem } from '@mastra/code-sdk/agents/sandbox-filesystem';\nimport { detectProject, getResourceIdOverride } from '@mastra/code-sdk/utils/project';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport { requireExec } from '../sandbox/materialization.js';\nimport type { ExecutableSandbox } from '../sandbox/materialization.js';\nimport { peekSessionSandbox } from '../sandbox/session-sandbox.js';\nimport { waitForPendingFilesystemCapture } from '../session/filesystem-capture.js';\nimport type { FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { SourceControlSession } from '../storage/domains/source-control/base.js';\nimport type { RouteAuth } from './route.js';\n\n/**\n * Server-side directory browser for the web project picker.\n *\n * The browser cannot read absolute filesystem paths (the File System Access API\n * only exposes a directory *name*), so the picker must ask the server — which\n * does have filesystem access — to enumerate directories. The result is real\n * absolute paths the user can select without typing.\n *\n * All access is confined to a configured `root` (default: the user's home\n * directory). Requests that try to escape the root via `..` or symlinks are\n * clamped back to the root.\n */\n\nexport interface DirectoryEntry {\n name: string;\n /** Absolute path to the entry. */\n path: string;\n}\n\nexport interface DirectoryListing {\n /** The allowed root; clients cannot browse above this. */\n root: string;\n /** The absolute path that was listed. */\n path: string;\n /** Parent directory path, or null when `path` is the root. */\n parent: string | null;\n /** Subdirectories of `path` (directories only, sorted, hidden excluded). */\n entries: DirectoryEntry[];\n}\n\nexport interface WorkspaceRenderedEntry {\n name: string;\n /** Path relative to the configured rendered root. */\n path: string;\n type: 'file' | 'directory';\n size: number;\n updatedAt: string;\n}\n\nexport interface WorkspaceRenderedListing {\n /** The confined workspace/project root. */\n workspacePath: string;\n /** Configured workspace-relative rendered root, e.g. `.artifacts`. */\n root: string;\n /** The confined absolute path for the rendered root. */\n rootPath: string;\n entries: WorkspaceRenderedEntry[];\n}\n\nexport interface WorkspaceFile {\n /** The confined workspace/project root. */\n workspacePath: string;\n /** Workspace-relative file path. */\n path: string;\n name: string;\n size: number;\n updatedAt: string;\n contentType: 'text' | 'unsupported';\n content?: string;\n truncated?: boolean;\n}\n\nexport interface WorkspaceFilesListing {\n /** The Factory session resource id. */\n workspacePath: string;\n /** The agent thread whose terminal file list was captured. */\n threadId: string;\n files: Array<{ path: string }>;\n}\n\nexport type WorkspaceChangeStatus =\n | 'modified'\n | 'added'\n | 'deleted'\n | 'renamed'\n | 'copied'\n | 'untracked'\n | 'conflicted';\n\nexport interface WorkspaceChange {\n path: string;\n previousPath?: string;\n status: WorkspaceChangeStatus;\n additions?: number;\n deletions?: number;\n binary?: boolean;\n}\n\nexport interface WorkspaceChanges {\n workspacePath: string;\n available: boolean;\n changes: WorkspaceChange[];\n additions?: number;\n deletions?: number;\n}\n\nexport interface WorkspaceDiff {\n workspacePath: string;\n path: string;\n patch: string;\n truncated: boolean;\n}\n\nexport type ArtifactEntry = WorkspaceRenderedEntry;\n\nexport interface ArtifactListing {\n /** The confined workspace/project root. */\n rootPath: string;\n /** The workspace artifact directory. */\n artifactsPath: string;\n entries: ArtifactEntry[];\n}\n\nconst MAX_TEXT_FILE_BYTES = 512 * 1024;\nconst MAX_DIFF_BYTES = 512 * 1024;\nconst WORKSPACE_NUMSTAT_SCRIPT = `\nset -e\nworkdir=$1\nbase=$2\nindex_file=$(mktemp)\nuntracked_file=$(mktemp)\nobject_dir=$(mktemp -d)\ntrap 'rm -f \"$index_file\" \"$untracked_file\"; rm -rf \"$object_dir\"' EXIT\nrm -f \"$index_file\"\ngit -C \"$workdir\" diff --numstat -z --find-renames --no-ext-diff --no-textconv \"$base\"\ngit -C \"$workdir\" ls-files --others --exclude-standard -z >\"$untracked_file\"\ngit_dir=$(git -C \"$workdir\" rev-parse --absolute-git-dir)\nexport GIT_INDEX_FILE=\"$index_file\"\nexport GIT_OBJECT_DIRECTORY=\"$object_dir\"\nexport GIT_ALTERNATE_OBJECT_DIRECTORIES=\"$git_dir/objects\"\ngit -C \"$workdir\" read-tree --empty\ngit -C \"$workdir\" update-index --add -z --stdin <\"$untracked_file\"\nempty_tree=$(git -C \"$workdir\" hash-object -t tree /dev/null)\ngit -C \"$workdir\" diff --cached --numstat -z --no-ext-diff --no-textconv \"$empty_tree\"\n`;\nconst BOUNDED_GIT_DIFF_SCRIPT = `\nallow_exit_one=$1\nshift\nstatus_file=$(mktemp)\nstderr_file=$(mktemp)\ntrap 'rm -f \"$status_file\" \"$stderr_file\"' EXIT\n(\n git \"$@\" 2>\"$stderr_file\"\n printf '%s' \"$?\" >\"$status_file\"\n) | head -c ${MAX_DIFF_BYTES + 1}\nstatus=$(cat \"$status_file\")\ncase \"$status\" in\n 0|141) exit 0 ;;\n 1) [ \"$allow_exit_one\" = \"1\" ] && exit 0 ;;\nesac\ncat \"$stderr_file\" >&2\nexit \"\\${status:-1}\"\n`;\nconst TEXT_DECODER = new TextDecoder('utf-8', { fatal: true });\nconst APPROVED_RENDERED_ROOTS = new Set(['.artifacts']);\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\n/** Resolve the browsable root, defaulting to the user's home directory. */\nexport function resolveFsRoot(root?: string): string {\n return resolve(root && root.trim() ? root : homedir());\n}\n\n/** True when `candidate` is `root` or nested under it. */\nfunction isWithinRoot(candidate: string, root: string): boolean {\n if (candidate === root) return true;\n const rootWithSep = root.endsWith(sep) ? root : root + sep;\n return candidate.startsWith(rootWithSep);\n}\n\nasync function realOrResolved(path: string): Promise<string> {\n try {\n return await realpath(path);\n } catch {\n return path;\n }\n}\n\n/**\n * Resolve a path's real location (following symlinks) and confirm it stays\n * within `root`. Returns the real path when confined, or `null` when it escapes\n * the root or does not exist. Used so a symlink inside the root that points\n * outside it cannot be browsed or selected.\n */\nasync function realPathWithinRoot(candidate: string, root: string): Promise<string | null> {\n try {\n const real = await realpath(candidate);\n return isWithinRoot(real, root) ? real : null;\n } catch {\n return null;\n }\n}\n\nfunction assertRelativePath(path: string, label: string): string {\n const trimmed = path.trim();\n if (!trimmed) throw new Error(`Missing required query param: ${label}`);\n if (isAbsolute(trimmed)) throw new Error(`${label} must be relative`);\n if (trimmed.split(/[\\\\/]+/).includes('..')) throw new Error(`${label} escapes workspace`);\n const normalized = resolve('/', trimmed).slice(1);\n if (!normalized || normalized === '..' || normalized.startsWith(`..${sep}`))\n throw new Error(`${label} escapes workspace`);\n return normalized;\n}\n\nfunction assertApprovedRenderedRoot(renderedRoot: string): string {\n const safeRoot = assertRelativePath(renderedRoot, 'root');\n if (!APPROVED_RENDERED_ROOTS.has(safeRoot)) throw new Error('Root is not approved for rendered workspace access');\n return safeRoot;\n}\n\nasync function confinedWorkspacePath(\n root: string,\n workspacePath: string,\n): Promise<{ resolvedRoot: string; workspace: string }> {\n const resolvedRoot = await realOrResolved(resolveFsRoot(root));\n const candidate = isAbsolute(workspacePath) ? resolve(workspacePath) : resolve(resolvedRoot, workspacePath);\n const workspace = await realPathWithinRoot(candidate, resolvedRoot);\n if (!workspace) throw new Error('Path is outside the browsable root');\n return { resolvedRoot, workspace };\n}\n\nasync function confinedWorkspaceRelativePath(\n root: string,\n workspacePath: string,\n relativePath: string,\n): Promise<{ workspace: string; path: string; relativePath: string }> {\n const safeRelativePath = assertRelativePath(relativePath, 'path');\n const { workspace } = await confinedWorkspacePath(root, workspacePath);\n const candidate = resolve(workspace, safeRelativePath);\n if (!isWithinRoot(candidate, workspace)) throw new Error('Path escapes workspace');\n const confinedPath = await realPathWithinRoot(candidate, workspace);\n if (!confinedPath) throw new Error('Path is outside the workspace');\n return { workspace, path: confinedPath, relativePath: safeRelativePath };\n}\n\n/**\n * List the directories inside `requestedPath`, confined to `root`. An absent or\n * out-of-root path is clamped to the root, so the worst a malicious client can\n * do is browse within the allowed root.\n */\nexport async function listDirectory(root: string, requestedPath?: string): Promise<DirectoryListing> {\n // Resolve the root through symlinks so all confinement checks compare real\n // paths; a symlink that escapes the root is then reliably detectable.\n const resolvedRoot = await realOrResolved(resolveFsRoot(root));\n\n let target = resolvedRoot;\n if (requestedPath && requestedPath.trim()) {\n const candidate = isAbsolute(requestedPath) ? resolve(requestedPath) : resolve(resolvedRoot, requestedPath);\n // Follow symlinks and re-confirm the real target stays within the root.\n target = (await realPathWithinRoot(candidate, resolvedRoot)) ?? resolvedRoot;\n }\n\n // Confirm the target is a real directory; fall back to root otherwise.\n try {\n const info = await stat(target);\n if (!info.isDirectory()) target = resolvedRoot;\n } catch {\n target = resolvedRoot;\n }\n\n const dirents = await readdir(target, { withFileTypes: true });\n const entries: DirectoryEntry[] = [];\n for (const dirent of dirents) {\n if (dirent.name.startsWith('.')) continue; // skip dotfiles/dirs\n const entryPath = join(target, dirent.name);\n let isDir = dirent.isDirectory();\n if (dirent.isSymbolicLink()) {\n // Only surface symlinks whose real target is a directory inside the root,\n // so a link pointing outside the root can't be browsed or selected.\n const real = await realPathWithinRoot(entryPath, resolvedRoot);\n isDir = real ? (await stat(real).catch(() => null))?.isDirectory() === true : false;\n }\n if (isDir) entries.push({ name: dirent.name, path: entryPath });\n }\n entries.sort((a, b) => a.name.localeCompare(b.name));\n\n const parent = target === resolvedRoot ? null : resolve(target, '..');\n\n return { root: resolvedRoot, path: target, parent, entries };\n}\n\nasync function listRenderedEntries(rootPath: string, currentPath = rootPath): Promise<WorkspaceRenderedEntry[]> {\n const dirents = await readdir(currentPath, { withFileTypes: true });\n const entries: WorkspaceRenderedEntry[] = [];\n\n for (const dirent of dirents) {\n const entryPath = join(currentPath, dirent.name);\n const info = await lstat(entryPath);\n const relativePath = entryPath.slice(rootPath.length + 1);\n\n if (info.isDirectory()) {\n entries.push({\n name: dirent.name,\n path: relativePath,\n type: 'directory',\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n });\n entries.push(...(await listRenderedEntries(rootPath, entryPath)));\n continue;\n }\n\n if (info.isFile()) {\n entries.push({\n name: dirent.name,\n path: relativePath,\n type: 'file',\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n });\n }\n }\n\n return entries.sort((a, b) => a.path.localeCompare(b.path));\n}\n\nexport async function listWorkspaceRenderedPath(\n root: string,\n workspacePath: string,\n renderedRoot: string,\n): Promise<WorkspaceRenderedListing> {\n const safeRoot = assertApprovedRenderedRoot(renderedRoot);\n const { workspace } = await confinedWorkspacePath(root, workspacePath);\n const renderedPath = resolve(workspace, safeRoot);\n if (!isWithinRoot(renderedPath, workspace)) throw new Error('Root escapes workspace');\n\n const confinedRootPath = await realPathWithinRoot(renderedPath, workspace);\n if (!confinedRootPath) return { workspacePath: workspace, root: safeRoot, rootPath: renderedPath, entries: [] };\n\n const info = await stat(confinedRootPath);\n if (!info.isDirectory()) return { workspacePath: workspace, root: safeRoot, rootPath: confinedRootPath, entries: [] };\n\n return {\n workspacePath: workspace,\n root: safeRoot,\n rootPath: confinedRootPath,\n entries: await listRenderedEntries(confinedRootPath),\n };\n}\n\nexport async function readWorkspaceFile(root: string, workspacePath: string, path: string): Promise<WorkspaceFile> {\n const safePath = assertRelativePath(path, 'path');\n const relativeRoot = safePath.split('/')[0] ?? '';\n assertApprovedRenderedRoot(relativeRoot);\n const {\n workspace,\n path: confinedPath,\n relativePath,\n } = await confinedWorkspaceRelativePath(root, workspacePath, path);\n const info = await lstat(confinedPath);\n if (info.isDirectory()) throw new Error('Path is a directory');\n if (!info.isFile()) throw new Error('Unsupported file type');\n\n const bytesToRead = Math.min(info.size, MAX_TEXT_FILE_BYTES);\n const contentBuffer = Buffer.alloc(bytesToRead);\n const handle = await open(confinedPath, 'r');\n try {\n await handle.read(contentBuffer, 0, bytesToRead, 0);\n } finally {\n await handle.close();\n }\n\n try {\n const content = TEXT_DECODER.decode(contentBuffer);\n return {\n workspacePath: workspace,\n path: relativePath,\n name: relativePath.split('/').pop() ?? relativePath,\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n contentType: 'text',\n content,\n truncated: info.size > MAX_TEXT_FILE_BYTES,\n };\n } catch {\n return {\n workspacePath: workspace,\n path: relativePath,\n name: relativePath.split('/').pop() ?? relativePath,\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n contentType: 'unsupported',\n };\n }\n}\n\nexport async function listArtifacts(root: string, workspacePath: string): Promise<ArtifactListing> {\n const listing = await listWorkspaceRenderedPath(root, workspacePath, '.artifacts');\n return {\n rootPath: listing.workspacePath,\n artifactsPath: listing.rootPath,\n entries: listing.entries,\n };\n}\n\n// ── Session-backed workspace access ──────────────────────────────────────────\n//\n// The web UI identifies a Factory session workspace by its session id (a UUID),\n// not by a server-local filesystem path — the session's files live inside the\n// session's sandbox (a remote VM on deployed factories). These helpers resolve\n// the session, enforce that the caller owns it, reattach to its sandbox, and\n// serve the approved rendered roots through `SandboxFilesystem`.\n\n/** Dependencies for resolving a `workspacePath` that is a Factory session id. */\nexport interface SessionFsDeps {\n auth: RouteAuth;\n sessions: { getBySessionId(sessionId: string): Promise<SourceControlSession | null> };\n filesystem: Pick<FilesystemStorage, 'listFiles'>;\n}\n\n/**\n * Resolve a `workspacePath` query param as a Factory session id. Returns the\n * session when one exists and the caller owns it, `null` when no session\n * matches (the caller should fall back to local-path handling), and throws\n * when a session exists but belongs to another tenant.\n */\nasync function resolveAuthorizedSession(\n c: Context,\n deps: SessionFsDeps | undefined,\n workspacePath: string,\n): Promise<SourceControlSession | null> {\n if (!deps) return null;\n const session = await deps.sessions.getBySessionId(workspacePath);\n if (!session) return null;\n if (deps.auth.enabled()) {\n await deps.auth.ensureUser(c);\n const tenant = deps.auth.tenant(c);\n if (!tenant || tenant.orgId !== session.orgId || tenant.userId !== session.userId) {\n throw new Error('Session is not available to the current user');\n }\n }\n return session;\n}\n\nexport async function listSessionFilesystemFiles(\n filesystem: Pick<FilesystemStorage, 'listFiles'>,\n session: SourceControlSession,\n threadId: string,\n): Promise<WorkspaceFilesListing> {\n const safeThreadId = threadId.trim();\n if (!safeThreadId) throw new Error('Missing required query param: threadId');\n\n // The turn-end capture no longer gates agent_end, so a reader refetching on\n // run completion could otherwise race it and serve the previous turn's\n // listing. Await the in-flight capture (bounded) before reading.\n await waitForPendingFilesystemCapture(session.sessionId);\n\n return {\n workspacePath: session.sessionId,\n threadId: safeThreadId,\n files: await filesystem.listFiles({ resourceId: session.sessionId, threadId: safeThreadId }),\n };\n}\n\ninterface SessionSandboxHandle {\n sandbox: ExecutableSandbox;\n filesystem: SandboxFilesystem;\n workdir: string;\n}\n\n/**\n * Resolve the session's sandbox from the per-process memo and wrap its\n * workdir in a `SandboxFilesystem`. Returns `null` when the session has no\n * sandbox in this process (never opened here, or evicted by retirement).\n * This is a passive read path, so it never constructs or provisions: the\n * session's files come back the next time the workspace is actually opened\n * (e.g. by sending a message).\n */\nasync function sessionSandbox(session: SourceControlSession): Promise<SessionSandboxHandle | null> {\n const entry = peekSessionSandbox(session.id);\n // An unresolved workdir means the sandbox never started in this process —\n // nothing is materialized, so there are no files to browse.\n if (!entry?.workdir) return null;\n const sandbox = requireExec(entry.sandbox);\n return {\n sandbox,\n filesystem: new SandboxFilesystem({ sandbox, workdir: entry.workdir }),\n workdir: entry.workdir,\n };\n}\n\n/** List an approved rendered root inside a Factory session's sandbox workdir. */\nexport async function listSessionRenderedPath(\n session: SourceControlSession,\n renderedRoot: string,\n): Promise<WorkspaceRenderedListing> {\n const safeRoot = assertApprovedRenderedRoot(renderedRoot);\n const handle = await sessionSandbox(session);\n const rootPath = posixPath.join(handle?.workdir ?? '', safeRoot);\n const empty: WorkspaceRenderedListing = { workspacePath: session.sessionId, root: safeRoot, rootPath, entries: [] };\n if (!handle) return empty;\n\n // One round trip: emit \"type\\tsize\\tmtime\\tpath\" per entry. `safeRoot` comes\n // from a fixed allowlist so interpolating it (quoted) is safe.\n const quotedRoot = `'${rootPath.replace(/'/g, `'\\\\''`)}'`;\n const result = await handle.sandbox.executeCommand(\n 'sh',\n [\n '-c',\n `test -d ${quotedRoot} && find ${quotedRoot} -mindepth 1 -printf '%y\\\\t%s\\\\t%T@\\\\t%p\\\\n' 2>/dev/null || true`,\n ],\n { timeout: 30_000 },\n );\n if (result.exitCode !== 0) return empty;\n\n const entries: WorkspaceRenderedEntry[] = [];\n for (const line of result.stdout.split('\\n')) {\n if (!line) continue;\n const [type, sizeStr, mtimeStr, ...pathParts] = line.split('\\t');\n const fullPath = pathParts.join('\\t');\n if (!fullPath || !fullPath.startsWith(`${rootPath}/`)) continue;\n const relativePath = fullPath.slice(rootPath.length + 1);\n entries.push({\n name: posixPath.basename(relativePath),\n path: relativePath,\n type: type === 'd' ? 'directory' : 'file',\n size: type === 'd' ? 0 : Number(sizeStr) || 0,\n updatedAt: new Date((Number(mtimeStr) || 0) * 1000).toISOString(),\n });\n }\n entries.sort((a, b) => a.path.localeCompare(b.path));\n\n return { workspacePath: session.sessionId, root: safeRoot, rootPath, entries };\n}\n\n/** Read a file inside a session's sandbox. Paths outside rendered roots require a persisted-file allowlist check in the route. */\nexport async function readSessionWorkspaceFile(\n session: SourceControlSession,\n path: string,\n options: { allowUnapprovedPath?: boolean } = {},\n): Promise<WorkspaceFile> {\n const safePath = assertRelativePath(path, 'path');\n if (!options.allowUnapprovedPath) assertApprovedRenderedRoot(safePath.split('/')[0] ?? '');\n\n const handle = await sessionSandbox(session);\n if (!handle) throw new Error('Session workspace is not available');\n const { filesystem } = handle;\n const info = await filesystem.stat(safePath);\n if (info.type === 'directory') throw new Error('Path is a directory');\n\n const buffer = (await filesystem.readFile(safePath)) as Buffer;\n const truncated = buffer.length > MAX_TEXT_FILE_BYTES;\n const base = {\n workspacePath: session.sessionId,\n path: safePath,\n name: posixPath.basename(safePath),\n size: buffer.length,\n updatedAt: info.modifiedAt.toISOString(),\n };\n try {\n const content = TEXT_DECODER.decode(truncated ? buffer.subarray(0, MAX_TEXT_FILE_BYTES) : buffer);\n return { ...base, contentType: 'text', content, truncated };\n } catch {\n return { ...base, contentType: 'unsupported' };\n }\n}\n\nfunction changeStatus(code: string): WorkspaceChangeStatus {\n if (code === '??') return 'untracked';\n if (code.includes('U') || code === 'AA' || code === 'DD') return 'conflicted';\n if (code.includes('R')) return 'renamed';\n if (code.includes('C')) return 'copied';\n if (code.includes('D')) return 'deleted';\n if (code.includes('A')) return 'added';\n return 'modified';\n}\n\nexport function parseWorkspaceChanges(output: string): WorkspaceChange[] {\n const records = output.split('\\0');\n const changes: WorkspaceChange[] = [];\n\n for (let index = 0; index < records.length; index += 1) {\n const record = records[index];\n if (!record || record.length < 4) continue;\n const code = record.slice(0, 2);\n const path = record.slice(3);\n const status = changeStatus(code);\n if (status === 'renamed' || status === 'copied') {\n const previousPath = records[index + 1];\n if (previousPath) index += 1;\n changes.push({ path, previousPath: previousPath || undefined, status });\n continue;\n }\n changes.push({ path, status });\n }\n\n return changes.toSorted((a, b) => a.path.localeCompare(b.path));\n}\nexport function parseWorkspaceTrackedChanges(output: string): WorkspaceChange[] {\n const records = output.split('\\0');\n const changes: WorkspaceChange[] = [];\n for (let index = 0; index < records.length;) {\n const code = records[index++];\n if (!code) continue;\n const status = changeStatus(code);\n if (status === 'renamed' || status === 'copied') {\n const previousPath = records[index++];\n const path = records[index++];\n if (path) changes.push({ path, previousPath: previousPath || undefined, status });\n continue;\n }\n const path = records[index++];\n if (path) changes.push({ path, status });\n }\n return changes.toSorted((a, b) => a.path.localeCompare(b.path));\n}\n\nexport function parseWorkspaceChangeStats(output: string) {\n const records = output.split('\\0');\n const stats = new Map<string, { additions?: number; deletions?: number; binary?: boolean }>();\n\n for (let index = 0; index < records.length; index += 1) {\n const record = records[index];\n if (!record) continue;\n const firstTab = record.indexOf('\\t');\n const secondTab = record.indexOf('\\t', firstTab + 1);\n if (firstTab < 0 || secondTab < 0) continue;\n\n const additionsText = record.slice(0, firstTab);\n const deletionsText = record.slice(firstTab + 1, secondTab);\n let path = record.slice(secondTab + 1);\n if (!path) {\n const renamedPath = records[index + 2];\n if (!renamedPath) continue;\n path = renamedPath;\n index += 2;\n }\n if (path.startsWith('./')) path = path.slice(2);\n\n if (additionsText === '-' || deletionsText === '-') {\n stats.set(path, { binary: true });\n continue;\n }\n\n const additions = Number(additionsText);\n const deletions = Number(deletionsText);\n if (Number.isFinite(additions) && Number.isFinite(deletions)) stats.set(path, { additions, deletions });\n }\n\n return stats;\n}\n\nfunction unavailableWorkspaceChanges(workspacePath: string): WorkspaceChanges {\n return { workspacePath, available: false, changes: [] };\n}\n\nasync function resolveSessionComparisonBase(\n session: SourceControlSession,\n sandbox: ExecutableSandbox,\n workdir: string,\n): Promise<string> {\n const result = await sandbox.executeCommand(\n 'git',\n ['-C', workdir, 'merge-base', 'HEAD', `origin/${session.baseBranch}`],\n { timeout: 30_000 },\n );\n const sha = result.stdout.trim();\n return result.exitCode === 0 && /^[0-9a-f]{40,64}$/i.test(sha) ? sha : 'HEAD';\n}\n\nexport async function listSessionWorkspaceChanges(session: SourceControlSession): Promise<WorkspaceChanges> {\n const handle = await sessionSandbox(session);\n if (!handle) return unavailableWorkspaceChanges(session.sessionId);\n const comparisonBase = await resolveSessionComparisonBase(session, handle.sandbox, handle.workdir);\n\n const [statusResult, trackedResult, statsResult] = await Promise.all([\n handle.sandbox.executeCommand(\n 'git',\n ['-C', handle.workdir, 'status', '--porcelain=v1', '-z', '--untracked-files=all'],\n { timeout: 30_000 },\n ),\n handle.sandbox.executeCommand(\n 'git',\n ['-C', handle.workdir, 'diff', '--name-status', '-z', '--find-renames', comparisonBase],\n { timeout: 30_000 },\n ),\n handle.sandbox.executeCommand(\n 'sh',\n ['-c', WORKSPACE_NUMSTAT_SCRIPT, 'mastracode-numstat', handle.workdir, comparisonBase],\n { timeout: 30_000 },\n ),\n ]);\n if (statusResult.exitCode !== 0) return unavailableWorkspaceChanges(session.sessionId);\n\n const statusChanges = parseWorkspaceChanges(statusResult.stdout);\n const trackedChanges = trackedResult.exitCode === 0 ? parseWorkspaceTrackedChanges(trackedResult.stdout) : [];\n const changesByPath = new Map(trackedChanges.map(change => [change.path, change]));\n for (const change of statusChanges) {\n if (trackedResult.exitCode !== 0 || change.status === 'untracked' || change.status === 'conflicted') {\n changesByPath.set(change.path, change);\n }\n }\n const changes = [...changesByPath.values()].toSorted((a, b) => a.path.localeCompare(b.path));\n if (statsResult.exitCode !== 0) {\n return { workspacePath: session.sessionId, available: true, changes };\n }\n\n const stats = parseWorkspaceChangeStats(statsResult.stdout);\n let additions = 0;\n let deletions = 0;\n const changesWithStats = changes.map(change => {\n const changeStats = stats.get(change.path);\n additions += changeStats?.additions ?? 0;\n deletions += changeStats?.deletions ?? 0;\n return { ...change, ...changeStats };\n });\n\n return { workspacePath: session.sessionId, available: true, changes: changesWithStats, additions, deletions };\n}\n\nasync function executeBoundedGitDiff(sandbox: ExecutableSandbox, args: string[], allowExitOne = false) {\n return sandbox.executeCommand(\n 'sh',\n ['-c', BOUNDED_GIT_DIFF_SCRIPT, 'mastracode-diff', allowExitOne ? '1' : '0', ...args],\n { timeout: 30_000 },\n );\n}\n\nfunction truncatePatch(patchBuffer: Buffer): string {\n const patch = patchBuffer.subarray(0, MAX_DIFF_BYTES).toString('utf8');\n const lastNewline = patch.lastIndexOf('\\n');\n if (lastNewline >= 0) return patch.slice(0, lastNewline + 1);\n return patch.replace(/\\uFFFD+$/, '');\n}\n\nexport async function readSessionWorkspaceDiff(\n session: SourceControlSession,\n path: string,\n previousPath?: string,\n): Promise<WorkspaceDiff> {\n const safePath = assertRelativePath(path, 'path');\n const safePreviousPath = previousPath ? assertRelativePath(previousPath, 'previousPath') : undefined;\n const handle = await sessionSandbox(session);\n if (!handle) throw new Error('Session workspace is not available');\n const comparisonBase = await resolveSessionComparisonBase(session, handle.sandbox, handle.workdir);\n\n const pathspecs = safePreviousPath ? [safePreviousPath, safePath] : [safePath];\n let result = await executeBoundedGitDiff(handle.sandbox, [\n '--literal-pathspecs',\n '-C',\n handle.workdir,\n 'diff',\n '--find-renames',\n '--no-ext-diff',\n '--no-color',\n '--unified=3',\n comparisonBase,\n '--',\n ...pathspecs,\n ]);\n if (result.exitCode !== 0) throw new Error(result.stderr || 'Unable to read workspace diff');\n\n if (!result.stdout) {\n const untracked = await handle.sandbox.executeCommand(\n 'git',\n ['--literal-pathspecs', '-C', handle.workdir, 'ls-files', '--others', '--exclude-standard', '--', safePath],\n { timeout: 30_000 },\n );\n if (untracked.exitCode === 0 && untracked.stdout.trim()) {\n result = await executeBoundedGitDiff(\n handle.sandbox,\n [\n '-C',\n handle.workdir,\n 'diff',\n '--no-index',\n '--no-ext-diff',\n '--no-color',\n '--unified=3',\n '--',\n '/dev/null',\n safePath,\n ],\n true,\n );\n if (result.exitCode !== 0 && result.exitCode !== 1) {\n throw new Error(result.stderr || 'Unable to read workspace diff');\n }\n }\n }\n\n const patchBuffer = Buffer.from(result.stdout);\n const truncated = patchBuffer.length > MAX_DIFF_BYTES;\n return {\n workspacePath: session.sessionId,\n path: safePath,\n patch: truncated ? truncatePatch(patchBuffer) : result.stdout,\n truncated,\n };\n}\n\nexport interface ResolvedCodebase {\n /**\n * The resourceId the TUI would use for this path — derived identically so a\n * project opened in the terminal and in the web app resolve to the SAME\n * session (and therefore the same threads).\n */\n resourceId: string;\n name: string;\n rootPath: string;\n gitUrl?: string;\n gitBranch?: string;\n}\n\n/**\n * Resolve a project path to the same resourceId the TUI uses. Mirrors\n * `createMastraCode`: detect the project, then apply any resourceId override\n * (MASTRA_RESOURCE_ID env var or `.mastracode/database.json`). This is the\n * shared continuity point — start in the TUI, continue on the web, same path\n * → same resourceId → same session.\n */\nexport function resolveCodebase(projectPath: string): ResolvedCodebase {\n const info = detectProject(projectPath);\n const override = getResourceIdOverride(info.rootPath);\n return {\n resourceId: override ?? info.resourceId,\n name: info.name,\n rootPath: info.rootPath,\n gitUrl: info.gitUrl,\n gitBranch: info.gitBranch,\n };\n}\n\n/**\n * Build the web filesystem routes as Mastra `apiRoutes`:\n * - `GET /web/fs/list?path=...` — browse directories (confined to root)\n * - `GET /web/codebase/resolve?path=...` — TUI-compatible codebase resourceId\n */\nexport function buildFsRoutes(options: { root?: string; sessionFs?: SessionFsDeps } = {}): ApiRoute[] {\n const root = resolveFsRoot(options.root);\n const sessionFs = options.sessionFs;\n\n return [\n registerApiRoute('/web/fs/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n try {\n const listing = await listDirectory(root, path);\n return c.json(listing);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n },\n }),\n registerApiRoute('/web/artifacts/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n try {\n return c.json(await listArtifacts(root, path));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status = message === 'Path is outside the browsable root' ? 403 : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/rendered/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const renderedRoot = c.req.query('root');\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!renderedRoot) return c.json({ error: 'Missing required query param: root' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (session && sessionFs) {\n return c.json(await listSessionRenderedPath(session, renderedRoot));\n }\n return c.json(await listWorkspaceRenderedPath(root, workspacePath, renderedRoot));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status =\n message.includes('outside') ||\n message.includes('relative') ||\n message.includes('escapes') ||\n message.includes('not approved') ||\n message.includes('not available')\n ? 403\n : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/files', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const threadId = c.req.query('threadId')?.trim();\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!threadId) return c.json({ error: 'Missing required query param: threadId' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (!session || !sessionFs) return c.json({ error: 'Session workspace is not available' }, 403);\n return c.json(await listSessionFilesystemFiles(sessionFs.filesystem, session, threadId));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status = message.includes('not available') ? 403 : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/changes', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (!session || !sessionFs) return c.json(unavailableWorkspaceChanges(workspacePath));\n return c.json(await listSessionWorkspaceChanges(session));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, message.includes('not available') ? 403 : 500);\n }\n },\n }),\n registerApiRoute('/web/workspace/changes/diff', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const path = c.req.query('path');\n const previousPath = c.req.query('previousPath');\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (!session || !sessionFs) return c.json({ error: 'Session workspace is not available' }, 403);\n return c.json(await readSessionWorkspaceDiff(session, path, previousPath));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status =\n message.includes('relative') || message.includes('escapes') || message.includes('not available')\n ? 403\n : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/file', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const path = c.req.query('path');\n const requestedThreadId = c.req.query('threadId');\n const threadId = requestedThreadId?.trim();\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n if (requestedThreadId !== undefined && !threadId) {\n return c.json({ error: 'Missing required query param: threadId' }, 400);\n }\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (session && sessionFs) {\n if (threadId) {\n const safePath = assertRelativePath(path, 'path');\n const listing = await listSessionFilesystemFiles(sessionFs.filesystem, session, threadId);\n if (!listing.files.some(file => file.path === safePath)) {\n return c.json({ error: 'Path is not available for this thread' }, 404);\n }\n return c.json(await readSessionWorkspaceFile(session, safePath, { allowUnapprovedPath: true }));\n }\n return c.json(await readSessionWorkspaceFile(session, path));\n }\n if (threadId) return c.json({ error: 'Session workspace is not available' }, 403);\n return c.json(await readWorkspaceFile(root, workspacePath, path));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status =\n message.includes('outside') ||\n message.includes('relative') ||\n message.includes('escapes') ||\n message.includes('not approved') ||\n message.includes('not available')\n ? 403\n : message.includes('directory')\n ? 400\n : message.includes('not found')\n ? 404\n : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/codebase/resolve', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n // Confine resolution to the browsable root (following symlinks), so this\n // endpoint can't be used to probe arbitrary filesystem paths. The web UI\n // only ever resolves directories the user picked via the root-confined\n // browser, so legitimate requests are always within the root.\n const confined = await realPathWithinRoot(isAbsolute(path) ? resolve(path) : resolve(root, path), root);\n if (!confined) return c.json({ error: 'Path is outside the browsable root' }, 403);\n try {\n return c.json(resolveCodebase(confined));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n },\n }),\n ];\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem-capture.d.ts","sourceRoot":"","sources":["../../src/session/filesystem-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACzG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC/F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAM5F,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,QAAQ,CAAC,MAAM,EAAE;QAAE,SAAS,IAAI,MAAM,CAAA;KAAE,CAAC;IACzC,YAAY,IAAI;QAAE,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;IACnF,gBAAgB,CAAC,QAAQ,EAAE,6BAA6B,GAAG,MAAM,IAAI,CAAC;IACtE,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;IACpD,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;KAC1E,CAAC;CACH;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAmB5E;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,wBAAwB,EACjC,EAAE,UAAU,EAAE,aAAa,EAAE,EAAE,6BAA6B,GAC3D,OAAO,CAAC,IAAI,CAAC,CA2Df;AAYD;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAc3G;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,wBAAwB,EACjC,YAAY,EAAE,6BAA6B,GAC1C,MAAM,IAAI,CAoFZ"}
1
+ {"version":3,"file":"filesystem-capture.d.ts","sourceRoot":"","sources":["../../src/session/filesystem-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACzG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC/F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAgB5F,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,QAAQ,CAAC,MAAM,EAAE;QAAE,SAAS,IAAI,MAAM,CAAA;KAAE,CAAC;IACzC,YAAY,IAAI;QAAE,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;IACnF,gBAAgB,CAAC,QAAQ,EAAE,6BAA6B,GAAG,MAAM,IAAI,CAAC;IACtE,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;IACpD,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;KAC1E,CAAC;CACH;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAS5E;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,wBAAwB,EACjC,EAAE,UAAU,EAAE,aAAa,EAAE,EAAE,6BAA6B,GAC3D,OAAO,CAAC,IAAI,CAAC,CA6Df;AAYD;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAc3G;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,wBAAwB,EACjC,YAAY,EAAE,6BAA6B,GAC1C,MAAM,IAAI,CAoFZ"}
@@ -1,26 +1,24 @@
1
1
  import { peekSessionSandbox } from "../sandbox/session-sandbox.js";
2
2
  import { isMeaningfulToolName } from "./first-exec-capture.js";
3
3
  //#region src/session/filesystem-capture.ts
4
- const GIT_STATUS_ARGS = [
5
- "status",
6
- "--porcelain=v1",
7
- "-z",
8
- "--untracked-files=all"
9
- ];
4
+ const GIT_CHANGED_FILES_SCRIPT = `
5
+ set -e
6
+ workdir=$1
7
+ base_branch=$2
8
+ base=HEAD
9
+ if merge_base=$(git -C "$workdir" merge-base HEAD "origin/$base_branch" 2>/dev/null); then
10
+ base=$merge_base
11
+ fi
12
+ git -C "$workdir" diff --name-only -z --find-renames --diff-filter=ACMRTUXB "$base"
13
+ git -C "$workdir" ls-files --others --exclude-standard -z
14
+ `;
10
15
  const ARTIFACTS_LIST_COMMAND = "cd \"$1\" && test -d .artifacts && find .artifacts -type f -print0 || true";
11
16
  function parseFilesystemCaptureFiles(output) {
12
17
  const files = /* @__PURE__ */ new Map();
13
- const records = output.split("\0");
14
- for (let index = 0; index < records.length; index += 1) {
15
- const record = records[index];
16
- if (!record || record.length < 4) continue;
17
- const code = record.slice(0, 2);
18
- let path = record.slice(3);
19
- const moved = code.includes("R") || code.includes("C");
20
- if (moved) index += 1;
21
- if (path.startsWith("./")) path = path.slice(2);
22
- if (!path || !code.includes("U") && code.includes("D") && !moved) continue;
23
- files.set(path, { path });
18
+ for (const record of output.split("\0")) {
19
+ if (!record) continue;
20
+ const path = record.replace(/^\.\//, "");
21
+ if (path) files.set(path, { path });
24
22
  }
25
23
  return [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path));
26
24
  }
@@ -36,13 +34,15 @@ async function captureSessionFilesystem(session, { filesystem, sourceControl })
36
34
  if (entry.sandbox.status !== "running") return;
37
35
  const workdir = entry.workdir;
38
36
  if (!workdir) return;
39
- const result = await sandbox.executeCommand("git", [
40
- "-C",
37
+ const result = await sandbox.executeCommand("sh", [
38
+ "-c",
39
+ GIT_CHANGED_FILES_SCRIPT,
40
+ "mastracode-changed-files",
41
41
  workdir,
42
- ...GIT_STATUS_ARGS
42
+ sourceSession.baseBranch
43
43
  ], { timeout: 3e4 });
44
44
  if (result.exitCode !== 0) {
45
- console.warn("[Factory filesystem capture] Unable to inspect Git status.", result.stderr);
45
+ console.warn("[Factory filesystem capture] Unable to inspect Git changes.", result.stderr);
46
46
  return;
47
47
  }
48
48
  const artifacts = await sandbox.executeCommand("sh", [
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem-capture.js","names":[],"sources":["../../src/session/filesystem-capture.ts"],"sourcesContent":["import type { AgentControllerEvent, SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\nimport { peekSessionSandbox } from '../sandbox/session-sandbox.js';\n\nimport type { FilesystemFile, FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { isMeaningfulToolName } from './first-exec-capture.js';\n\nconst GIT_STATUS_ARGS = ['status', '--porcelain=v1', '-z', '--untracked-files=all'];\nconst ARTIFACTS_LIST_COMMAND = 'cd \"$1\" && test -d .artifacts && find .artifacts -type f -print0 || true';\n\nexport interface FilesystemCaptureSession {\n readonly identity: { getResourceId(): string };\n readonly thread: { requireId(): string };\n getWorkspace(): { sandbox?: Pick<WorkspaceSandbox, 'executeCommand'> } | undefined;\n onBeforeAgentEnd(listener: SessionBeforeAgentEndListener): () => void;\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FilesystemCaptureDependencies {\n filesystem: Pick<FilesystemStorage, 'replaceFiles'>;\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n}\n\nexport function parseFilesystemCaptureFiles(output: string): FilesystemFile[] {\n const files = new Map<string, FilesystemFile>();\n const records = output.split('\\0');\n\n for (let index = 0; index < records.length; index += 1) {\n const record = records[index];\n if (!record || record.length < 4) continue;\n\n const code = record.slice(0, 2);\n let path = record.slice(3);\n const moved = code.includes('R') || code.includes('C');\n if (moved) index += 1;\n if (path.startsWith('./')) path = path.slice(2);\n if (!path || (!code.includes('U') && code.includes('D') && !moved)) continue;\n\n files.set(path, { path });\n }\n\n return [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path));\n}\n\nexport async function captureSessionFilesystem(\n session: FilesystemCaptureSession,\n { filesystem, sourceControl }: FilesystemCaptureDependencies,\n): Promise<void> {\n try {\n const resourceId = session.identity.getResourceId();\n const threadId = session.thread.requireId();\n const sourceSession = await sourceControl.sessions.getBySessionId(resourceId);\n // Chat-only sessions run without a workspace; there is nothing to capture.\n const sandbox = session.getWorkspace()?.sandbox;\n if (!sourceSession || !sandbox?.executeCommand) return;\n // The live workdir comes from the per-process memo (the deterministic\n // truth) ONLY — never the persisted observability column, which a row\n // written under a previous provider can point at a workdir that no\n // longer exists (the stale-workdir incident class). No memo entry means\n // no live sandbox worth capturing in this replica; capture is\n // best-effort telemetry, so skip.\n const entry = peekSessionSandbox(sourceSession.id);\n if (!entry) return;\n // Telemetry must never provision a VM: executeCommand lazily starts the\n // sandbox via ensureRunning, so a chat turn that never touched the\n // workspace would otherwise boot (and clone into) a fresh VM just to\n // capture an empty git status. Only capture when the turn already has a\n // running sandbox.\n if (entry.sandbox.status !== 'running') return;\n // Running-but-unresolved should not happen (the start hook resolves the\n // workdir), but capture is best-effort — skip rather than guess.\n const workdir = entry.workdir;\n if (!workdir) return;\n\n const result = await sandbox.executeCommand('git', ['-C', workdir, ...GIT_STATUS_ARGS], {\n timeout: 30_000,\n });\n if (result.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to inspect Git status.', result.stderr);\n return;\n }\n\n const artifacts = await sandbox.executeCommand('sh', ['-c', ARTIFACTS_LIST_COMMAND, 'sh', workdir], {\n timeout: 30_000,\n });\n if (artifacts.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to list workspace artifacts.', artifacts.stderr);\n return;\n }\n\n const files = new Map(parseFilesystemCaptureFiles(result.stdout).map(file => [file.path, file]));\n for (const path of artifacts.stdout.split('\\0')) {\n const normalizedPath = path.replace(/^\\.\\//, '');\n if (normalizedPath) {\n files.set(normalizedPath, { path: normalizedPath });\n }\n }\n\n await filesystem.replaceFiles({\n resourceId,\n threadId,\n files: [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path)),\n });\n } catch (error) {\n console.warn('[Factory filesystem capture] Unable to persist files.', error);\n }\n}\n\n/**\n * In-flight capture chains keyed by session resource id. Serves two purposes:\n * readers of the persisted file listing (the /web/workspace routes) await the\n * capture for the turn that just ended instead of racing it, and captures for\n * the same resource stay sequential (last write wins) even when multiple\n * scoped sessions observe the same resource id. In-process only: the factory\n * server that runs the session controller also serves those routes.\n */\nconst pendingCaptures = new Map<string, Promise<void>>();\n\n/**\n * Resolve once the session's in-flight filesystem capture (if any) has\n * persisted, or after `timeoutMs` so a reader that arrives mid-capture is\n * never blocked by the execs' worst-case 30s timeouts. Never rejects: the\n * capture body is fully try/catch contained.\n */\nexport async function waitForPendingFilesystemCapture(resourceId: string, timeoutMs = 10_000): Promise<void> {\n const pending = pendingCaptures.get(resourceId);\n if (!pending) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n await Promise.race([\n pending,\n new Promise<void>(resolve => {\n timer = setTimeout(resolve, timeoutMs);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function observeSessionFilesystem(\n session: FilesystemCaptureSession,\n dependencies: FilesystemCaptureDependencies,\n): () => void {\n let fallbackChain = Promise.resolve();\n // Capture only runs after a turn that actually touched the workspace: a\n // successful workspace tool call both proves the sandbox is awake and is\n // the only way the listing can have changed. Without this gate every chat\n // turn would run two sandbox execs — and since executeCommand resumes an\n // idle VM, a pure-chat turn would keep waking a sandbox it never used.\n // Same bookkeeping as first-exec capture: tool names resolve at tool_end\n // via the tool_start map, and suspended calls survive agent_end so a\n // resumed run's eventual completion still counts for that turn.\n let workspaceTouched = false;\n const toolNames = new Map<string, string>();\n const suspended = new Set<string>();\n const unsubscribeEvents = session.subscribe(event => {\n switch (event.type) {\n case 'tool_start': {\n toolNames.set(event.toolCallId, event.toolName);\n return;\n }\n case 'tool_suspended': {\n suspended.add(event.toolCallId);\n return;\n }\n case 'tool_suspension_cancelled': {\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n return;\n }\n case 'agent_end': {\n for (const id of toolNames.keys()) {\n if (!suspended.has(id)) toolNames.delete(id);\n }\n return;\n }\n case 'tool_end': {\n const toolName = toolNames.get(event.toolCallId);\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n if (event.isError || event.denied) return;\n if (isMeaningfulToolName(toolName)) workspaceTouched = true;\n return;\n }\n }\n });\n const unsubscribeEnd = session.onBeforeAgentEnd(() => {\n // onBeforeAgentEnd fires before agent_end is emitted, so every tool_end\n // of the finishing run has already been observed; read then reset.\n if (!workspaceTouched) return;\n workspaceTouched = false;\n // Chain so captures stay sequential (last write wins), but do NOT return\n // the chain: finishAgentRun awaits every listener before emitting\n // agent_end, and the capture's sandbox execs (git status + artifacts\n // find, 30s timeouts each) must not gate turn completion. Readers that\n // need the fresh listing await the pending capture through\n // waitForPendingFilesystemCapture instead. The un-returned chain cannot\n // leak an unhandled rejection: captureSessionFilesystem's entire body\n // runs inside its own try/catch, and disposal of this listener does not\n // cancel an in-flight chain, so a final turn's capture still persists.\n let resourceId: string | undefined;\n try {\n resourceId = session.identity.getResourceId();\n } catch {\n // No resource id means no route can address this session's listing;\n // keep a per-observer chain so captures still run sequentially.\n fallbackChain = fallbackChain.then(() => captureSessionFilesystem(session, dependencies));\n return;\n }\n // Extend the resource-level chain rather than a per-session one: sessions\n // are deduplicated by (resourceId, scope), so multiple scoped sessions can\n // observe the same resource, and their captures must not interleave.\n const chain = (pendingCaptures.get(resourceId) ?? Promise.resolve()).then(() =>\n captureSessionFilesystem(session, dependencies),\n );\n pendingCaptures.set(resourceId, chain);\n void chain.finally(() => {\n if (pendingCaptures.get(resourceId) === chain) {\n pendingCaptures.delete(resourceId);\n }\n });\n });\n return () => {\n unsubscribeEvents();\n unsubscribeEnd();\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"filesystem-capture.js","names":[],"sources":["../../src/session/filesystem-capture.ts"],"sourcesContent":["import type { AgentControllerEvent, SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\nimport { peekSessionSandbox } from '../sandbox/session-sandbox.js';\n\nimport type { FilesystemFile, FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { isMeaningfulToolName } from './first-exec-capture.js';\n\nconst GIT_CHANGED_FILES_SCRIPT = `\nset -e\nworkdir=$1\nbase_branch=$2\nbase=HEAD\nif merge_base=$(git -C \"$workdir\" merge-base HEAD \"origin/$base_branch\" 2>/dev/null); then\n base=$merge_base\nfi\ngit -C \"$workdir\" diff --name-only -z --find-renames --diff-filter=ACMRTUXB \"$base\"\ngit -C \"$workdir\" ls-files --others --exclude-standard -z\n`;\nconst ARTIFACTS_LIST_COMMAND = 'cd \"$1\" && test -d .artifacts && find .artifacts -type f -print0 || true';\n\nexport interface FilesystemCaptureSession {\n readonly identity: { getResourceId(): string };\n readonly thread: { requireId(): string };\n getWorkspace(): { sandbox?: Pick<WorkspaceSandbox, 'executeCommand'> } | undefined;\n onBeforeAgentEnd(listener: SessionBeforeAgentEndListener): () => void;\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FilesystemCaptureDependencies {\n filesystem: Pick<FilesystemStorage, 'replaceFiles'>;\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n}\n\nexport function parseFilesystemCaptureFiles(output: string): FilesystemFile[] {\n const files = new Map<string, FilesystemFile>();\n for (const record of output.split('\\0')) {\n if (!record) continue;\n const path = record.replace(/^\\.\\//, '');\n if (path) files.set(path, { path });\n }\n\n return [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path));\n}\n\nexport async function captureSessionFilesystem(\n session: FilesystemCaptureSession,\n { filesystem, sourceControl }: FilesystemCaptureDependencies,\n): Promise<void> {\n try {\n const resourceId = session.identity.getResourceId();\n const threadId = session.thread.requireId();\n const sourceSession = await sourceControl.sessions.getBySessionId(resourceId);\n // Chat-only sessions run without a workspace; there is nothing to capture.\n const sandbox = session.getWorkspace()?.sandbox;\n if (!sourceSession || !sandbox?.executeCommand) return;\n // The live workdir comes from the per-process memo (the deterministic\n // truth) ONLY — never the persisted observability column, which a row\n // written under a previous provider can point at a workdir that no\n // longer exists (the stale-workdir incident class). No memo entry means\n // no live sandbox worth capturing in this replica; capture is\n // best-effort telemetry, so skip.\n const entry = peekSessionSandbox(sourceSession.id);\n if (!entry) return;\n // Telemetry must never provision a VM: executeCommand lazily starts the\n // sandbox via ensureRunning, so a chat turn that never touched the\n // workspace would otherwise boot (and clone into) a fresh VM just to\n // capture an empty git status. Only capture when the turn already has a\n // running sandbox.\n if (entry.sandbox.status !== 'running') return;\n // Running-but-unresolved should not happen (the start hook resolves the\n // workdir), but capture is best-effort — skip rather than guess.\n const workdir = entry.workdir;\n if (!workdir) return;\n\n const result = await sandbox.executeCommand(\n 'sh',\n ['-c', GIT_CHANGED_FILES_SCRIPT, 'mastracode-changed-files', workdir, sourceSession.baseBranch],\n { timeout: 30_000 },\n );\n if (result.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to inspect Git changes.', result.stderr);\n return;\n }\n\n const artifacts = await sandbox.executeCommand('sh', ['-c', ARTIFACTS_LIST_COMMAND, 'sh', workdir], {\n timeout: 30_000,\n });\n if (artifacts.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to list workspace artifacts.', artifacts.stderr);\n return;\n }\n\n const files = new Map(parseFilesystemCaptureFiles(result.stdout).map(file => [file.path, file]));\n for (const path of artifacts.stdout.split('\\0')) {\n const normalizedPath = path.replace(/^\\.\\//, '');\n if (normalizedPath) {\n files.set(normalizedPath, { path: normalizedPath });\n }\n }\n\n await filesystem.replaceFiles({\n resourceId,\n threadId,\n files: [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path)),\n });\n } catch (error) {\n console.warn('[Factory filesystem capture] Unable to persist files.', error);\n }\n}\n\n/**\n * In-flight capture chains keyed by session resource id. Serves two purposes:\n * readers of the persisted file listing (the /web/workspace routes) await the\n * capture for the turn that just ended instead of racing it, and captures for\n * the same resource stay sequential (last write wins) even when multiple\n * scoped sessions observe the same resource id. In-process only: the factory\n * server that runs the session controller also serves those routes.\n */\nconst pendingCaptures = new Map<string, Promise<void>>();\n\n/**\n * Resolve once the session's in-flight filesystem capture (if any) has\n * persisted, or after `timeoutMs` so a reader that arrives mid-capture is\n * never blocked by the execs' worst-case 30s timeouts. Never rejects: the\n * capture body is fully try/catch contained.\n */\nexport async function waitForPendingFilesystemCapture(resourceId: string, timeoutMs = 10_000): Promise<void> {\n const pending = pendingCaptures.get(resourceId);\n if (!pending) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n await Promise.race([\n pending,\n new Promise<void>(resolve => {\n timer = setTimeout(resolve, timeoutMs);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function observeSessionFilesystem(\n session: FilesystemCaptureSession,\n dependencies: FilesystemCaptureDependencies,\n): () => void {\n let fallbackChain = Promise.resolve();\n // Capture only runs after a turn that actually touched the workspace: a\n // successful workspace tool call both proves the sandbox is awake and is\n // the only way the listing can have changed. Without this gate every chat\n // turn would run two sandbox execs — and since executeCommand resumes an\n // idle VM, a pure-chat turn would keep waking a sandbox it never used.\n // Same bookkeeping as first-exec capture: tool names resolve at tool_end\n // via the tool_start map, and suspended calls survive agent_end so a\n // resumed run's eventual completion still counts for that turn.\n let workspaceTouched = false;\n const toolNames = new Map<string, string>();\n const suspended = new Set<string>();\n const unsubscribeEvents = session.subscribe(event => {\n switch (event.type) {\n case 'tool_start': {\n toolNames.set(event.toolCallId, event.toolName);\n return;\n }\n case 'tool_suspended': {\n suspended.add(event.toolCallId);\n return;\n }\n case 'tool_suspension_cancelled': {\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n return;\n }\n case 'agent_end': {\n for (const id of toolNames.keys()) {\n if (!suspended.has(id)) toolNames.delete(id);\n }\n return;\n }\n case 'tool_end': {\n const toolName = toolNames.get(event.toolCallId);\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n if (event.isError || event.denied) return;\n if (isMeaningfulToolName(toolName)) workspaceTouched = true;\n return;\n }\n }\n });\n const unsubscribeEnd = session.onBeforeAgentEnd(() => {\n // onBeforeAgentEnd fires before agent_end is emitted, so every tool_end\n // of the finishing run has already been observed; read then reset.\n if (!workspaceTouched) return;\n workspaceTouched = false;\n // Chain so captures stay sequential (last write wins), but do NOT return\n // the chain: finishAgentRun awaits every listener before emitting\n // agent_end, and the capture's sandbox execs (git status + artifacts\n // find, 30s timeouts each) must not gate turn completion. Readers that\n // need the fresh listing await the pending capture through\n // waitForPendingFilesystemCapture instead. The un-returned chain cannot\n // leak an unhandled rejection: captureSessionFilesystem's entire body\n // runs inside its own try/catch, and disposal of this listener does not\n // cancel an in-flight chain, so a final turn's capture still persists.\n let resourceId: string | undefined;\n try {\n resourceId = session.identity.getResourceId();\n } catch {\n // No resource id means no route can address this session's listing;\n // keep a per-observer chain so captures still run sequentially.\n fallbackChain = fallbackChain.then(() => captureSessionFilesystem(session, dependencies));\n return;\n }\n // Extend the resource-level chain rather than a per-session one: sessions\n // are deduplicated by (resourceId, scope), so multiple scoped sessions can\n // observe the same resource, and their captures must not interleave.\n const chain = (pendingCaptures.get(resourceId) ?? Promise.resolve()).then(() =>\n captureSessionFilesystem(session, dependencies),\n );\n pendingCaptures.set(resourceId, chain);\n void chain.finally(() => {\n if (pendingCaptures.get(resourceId) === chain) {\n pendingCaptures.delete(resourceId);\n }\n });\n });\n return () => {\n unsubscribeEvents();\n unsubscribeEnd();\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/factory",
3
- "version": "0.17.0-alpha.1",
3
+ "version": "0.17.0-alpha.2",
4
4
  "description": "Mastra Software Factory module: the server core behind the Mastra Software Factory — storage domains, integrations, and surfaces for agent-powered software delivery",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -59,9 +59,9 @@
59
59
  "zod": "^4.6.4",
60
60
  "@mastra/auth-studio": "1.3.7-alpha.0",
61
61
  "@mastra/auth-workos": "1.6.5",
62
- "@mastra/code-sdk": "1.8.1-alpha.0",
63
- "@mastra/slack": "1.6.4",
64
- "@mastra/core": "1.69.0-alpha.0"
62
+ "@mastra/code-sdk": "1.8.1-alpha.1",
63
+ "@mastra/core": "1.69.0-alpha.1",
64
+ "@mastra/slack": "1.6.4"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "22.20.1",