From dea03fc4fac186ccb11d6a7cd35b8653eb3f8218 Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Mon, 8 Jun 2026 17:30:39 +0200 Subject: [PATCH 1/5] fix(slack): emit thread replies as directory records to end file/dir collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A channel thread reply was the only Slack record emitted as a flat leaf file, `threads//replies/.json`, while its children (reactions) nest under a directory at the same stem, `threads//replies//...`. One name as both a file and a directory cannot be materialized on a POSIX mount: the relayfile mirror fails every sync cycle with `mkdir .../replies/.json: not a directory`, never completes bootstrap, and the teardown writeback flush hangs — which marked the daily-ship cron run FAILED even though its handler succeeded. Every other Slack record type already uses the `/meta.json` directory-record convention (messagePath, directMessagePath, threadPath). Bring thread replies in line: `threadReplyPath` now returns `replies//meta.json`, so the reply and its `reactions/` are siblings under one `replies//` directory — collision impossible by construction. - Add `threadReplyLegacyPath` + `slackThreadReplyReadCandidatePaths` for back-compat reads of replies mirrored by a pre-0.8.x adapter (mirrors the existing messagePath / messageLegacyPath / slackMessageReadCandidatePaths pattern). - `thread.ts` reply-listing regex accepts both the new `/meta.json` and the legacy `.json` form so a mid-migration mirror still maps a reply to its parent listing. - Update the LAYOUT.md prompt line and the two adapter tests that pinned the old flat path; add a regression test asserting the reply record and its reaction child cannot collide (and that legacy reads still resolve). Scope: channel thread replies only (the confirmed collision site). DM thread replies (`directMessageThreadReplyPath`) carry the same latent flat-file shape and should get the same treatment — left out here because that tree is under active change on the slack-dm-user-message-materialization branch; fold the same fix in there. The relayfile mount daemon is being hardened separately to quarantine any such collision regardless of adapter. Co-Authored-By: Claude Opus 4.8 --- .../src/__tests__/path-mapper-v2.test.ts | 53 +++++++++++++++++++ .../slack/src/__tests__/slack-adapter.test.ts | 4 +- packages/slack/src/layout-prompt.ts | 2 +- packages/slack/src/path-mapper.ts | 50 +++++++++++++++++ packages/slack/src/thread.ts | 7 ++- 5 files changed, 112 insertions(+), 4 deletions(-) diff --git a/packages/slack/src/__tests__/path-mapper-v2.test.ts b/packages/slack/src/__tests__/path-mapper-v2.test.ts index edc5b620..1b528fa6 100644 --- a/packages/slack/src/__tests__/path-mapper-v2.test.ts +++ b/packages/slack/src/__tests__/path-mapper-v2.test.ts @@ -11,7 +11,11 @@ import { messagePath, parseSlackDirectMessagePath, parseSlackDirectMessageThreadReplyPath, + reactionPath, slackBotsAliasPath, + slackThreadReplyReadCandidatePaths, + threadReplyLegacyPath, + threadReplyPath, slackByNameChannelAliasPath, slackByNameUserAliasPath, slackChannelsIndexPath, @@ -213,3 +217,52 @@ test('slackBotsAliasPath emits /slack/users/bots/__.json', () => { test('slackBotsAliasPath falls back to bare id when no name is given', () => { assert.equal(slackBotsAliasPath('B0123BOT'), '/slack/users/bots/B0123BOT.json'); }); + +test('threadReplyPath is a directory record and does not collide with its reaction children', () => { + const channelId = 'C123'; + const threadTs = '1711111111.000100'; + const replyTs = '1711111222.000200'; + + const reply = threadReplyPath(channelId, threadTs, replyTs); + assert.equal( + reply, + '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200/meta.json', + ); + + // The reply's children (reactions) must nest UNDER the reply's directory — + // never as a sibling that shares the reply's name with a different node type. + // This is the invariant whose violation wedged the mount: a flat leaf file + // `replies/.json` could not coexist with the `replies//` directory. + const replyDir = reply.replace(/\/meta\.json$/u, ''); + const reaction = reactionPath({ + targetType: 'thread_reply', + channelId, + threadTs, + replyTs, + reaction: 'tada', + userId: 'U1', + }); + assert.equal( + reaction, + `${replyDir}/reactions/tada--U1.json`, + ); + assert.ok( + reaction.startsWith(`${replyDir}/`), + 'reaction must nest under the reply directory', + ); + assert.ok( + !reaction.startsWith(`${replyDir}.json`), + 'reply stem must be a directory, not a flat .json file', + ); + + // Back-compat: readers can still resolve a reply mirrored by a pre-0.8.x + // adapter at the legacy flat path. + assert.deepEqual(slackThreadReplyReadCandidatePaths(channelId, threadTs, replyTs), [ + reply, + threadReplyLegacyPath(channelId, threadTs, replyTs), + ]); + assert.equal( + threadReplyLegacyPath(channelId, threadTs, replyTs), + '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200.json', + ); +}); diff --git a/packages/slack/src/__tests__/slack-adapter.test.ts b/packages/slack/src/__tests__/slack-adapter.test.ts index 1c857915..4e502642 100644 --- a/packages/slack/src/__tests__/slack-adapter.test.ts +++ b/packages/slack/src/__tests__/slack-adapter.test.ts @@ -386,11 +386,11 @@ test('message and thread path mapping is deterministic', () => { assert.equal( adapter.computePath('thread_reply', replyId), - '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200.json', + '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200/meta.json', ); assert.equal( computeSlackPath('thread_reply', replyId), - '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200.json', + '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200/meta.json', ); }); diff --git a/packages/slack/src/layout-prompt.ts b/packages/slack/src/layout-prompt.ts index 2ccdb528..a2a9a064 100644 --- a/packages/slack/src/layout-prompt.ts +++ b/packages/slack/src/layout-prompt.ts @@ -10,7 +10,7 @@ Always run \`ls\` before constructing a path. v2 standardizes resource directory \`/slack/channels/__/\` owns per-channel records: - \`meta.json\` — canonical channel record. - \`messages//meta.json\` — canonical top-level message records. Message text is mutable, so the stable Slack timestamp is the directory key. - - \`threads//meta.json\` and \`threads//replies/.json\` — thread roots and replies. + - \`threads//meta.json\` and \`threads//replies//meta.json\` — thread roots and replies (each a directory record, so a reply can carry \`reactions/\`). - \`messages//reactions/--.json\` — reaction records. \`/slack/users/__/meta.json\` — canonical user record. \`/slack/users//messages//meta.json\` — canonical 1:1 direct message records addressed by bare user id. Slack's internal \`D…\` IM channel id stays inside the JSON payload as source metadata; do not mount or write raw \`D…\` paths as the product contract. diff --git a/packages/slack/src/path-mapper.ts b/packages/slack/src/path-mapper.ts index 671073c3..6d1f5c50 100644 --- a/packages/slack/src/path-mapper.ts +++ b/packages/slack/src/path-mapper.ts @@ -403,11 +403,44 @@ export function threadPath(channelId: string, threadTs: string, channelName?: st ); } +/** + * Canonical thread-reply record path. The reply is a **directory record** + * (`replies//meta.json`) — matching `messagePath`, `directMessagePath`, and + * `threadPath`, all of which use `/meta.json`. This is deliberate: a reply + * can carry children (reactions live at `replies//reactions/...`, see + * {@link reactionPath}), so its stem MUST be a directory. The pre-0.8.x adapter + * wrote a flat leaf file `replies/.json`, which collided with that same + * `` directory — one name as both a file and a directory — and could not be + * materialized on a POSIX mount (`mkdir ... : not a directory`), wedging the + * whole mirror. Readers should fall back to the legacy filename via + * {@link slackThreadReplyReadCandidatePaths}. + */ export function threadReplyPath( channelId: string, threadTs: string, replyTs: string, channelName?: string, +): string { + return joinPath( + channelThreadsDirectory(channelId, channelName), + slackTimestampToPathToken(threadTs), + 'replies', + slackTimestampToPathToken(replyTs), + 'meta.json', + ); +} + +/** + * @deprecated Pre-0.8.x emitted a flat `.../replies/.json` leaf file, which + * collided with the `` reaction directory. Use {@link threadReplyPath}. + * Retained for back-compat reads only — see + * {@link slackThreadReplyReadCandidatePaths}. + */ +export function threadReplyLegacyPath( + channelId: string, + threadTs: string, + replyTs: string, + channelName?: string, ): string { return joinPath( channelThreadsDirectory(channelId, channelName), @@ -417,6 +450,23 @@ export function threadReplyPath( ); } +/** + * Reader hint: candidate paths for a Slack thread-reply canonical record, in + * order of preference — current (`/meta.json`) then legacy (`.json`) — + * so a reply mirrored by either the current or a pre-0.8.x adapter still reads. + */ +export function slackThreadReplyReadCandidatePaths( + channelId: string, + threadTs: string, + replyTs: string, + channelName?: string, +): string[] { + return [ + threadReplyPath(channelId, threadTs, replyTs, channelName), + threadReplyLegacyPath(channelId, threadTs, replyTs, channelName), + ]; +} + export function userMetadataPath(userId: string, userName?: string): string { return joinPath(SLACK_ROOT, 'users', slackNameWithId(userName, userId), 'meta.json'); } diff --git a/packages/slack/src/thread.ts b/packages/slack/src/thread.ts index 8219f7d8..6ae770ec 100644 --- a/packages/slack/src/thread.ts +++ b/packages/slack/src/thread.ts @@ -68,7 +68,12 @@ function extractRepliesPath(path: string): string | null { return path.replace(/\/meta\.json$/u, '/replies'); } - const match = path.match(/^(\/slack\/channels\/[^/]+\/threads\/[^/]+)\/replies\/[^/]+\.json$/u); + // Accept both the current reply record (`replies//meta.json`) and the + // legacy flat leaf (`replies/.json`) so a mirror mid-migration still maps + // a reply path back to its parent replies listing. + const match = path.match( + /^(\/slack\/channels\/[^/]+\/threads\/[^/]+)\/replies\/[^/]+(?:\/meta)?\.json$/u, + ); return match?.[1] ? `${match[1]}/replies` : null; } From f5ca1cebc34f2397596fe708dfe399805de1c167 Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Mon, 8 Jun 2026 17:50:21 +0200 Subject: [PATCH 2/5] fix(slack): apply directory-record fix to DM thread replies too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring 1:1 direct-message thread replies in line with channel thread replies and every other Slack record type: emit `users//messages//replies//meta.json` instead of the flat `replies/.json` leaf, so a reply's stem is a directory and cannot collide with a same-named child directory on a POSIX mount. - `directMessageThreadReplyPath` → `/meta.json`; add `directMessageThreadReplyLegacyPath` + `slackDirectMessageThreadReplyReadCandidatePaths`. - `parseSlackDirectMessageThreadReplyPath` accepts both the new `/meta.json` and legacy `.json` forms so routing works mid-migration. - Export the new helpers (and the channel-reply back-compat helpers from the prior commit) from the package index. - Update LAYOUT.md prompt + discovery `.adapter.md`; extend the DM path test with legacy-parse and read-candidate assertions. Typecheck clean; full slack suite green (104). Co-Authored-By: Claude Opus 4.8 --- packages/slack/discovery/slack/.adapter.md | 2 +- .../src/__tests__/path-mapper-v2.test.ts | 26 +++++++++- packages/slack/src/index.ts | 4 ++ packages/slack/src/layout-prompt.ts | 2 +- packages/slack/src/path-mapper.ts | 49 ++++++++++++++++++- 5 files changed, 79 insertions(+), 4 deletions(-) diff --git a/packages/slack/discovery/slack/.adapter.md b/packages/slack/discovery/slack/.adapter.md index a0c7af1a..8513b130 100644 --- a/packages/slack/discovery/slack/.adapter.md +++ b/packages/slack/discovery/slack/.adapter.md @@ -8,7 +8,7 @@ Read-only mounts: - `/slack/channels//messages//replies/.json` - Thread reply records. - `/slack/users/.json` - User records. - `/slack/users//messages//meta.json` - 1:1 direct message records addressed by bare Slack user id. -- `/slack/users//messages//replies/.json` - Threaded replies in a 1:1 direct message. +- `/slack/users//messages//replies//meta.json` - Threaded replies in a 1:1 direct message (directory record; legacy flat `.json` still readable). - `/discovery/slack/channels/_index.json` - History-independent channel id/name lookup rows for writeback context, materialized from Slack channel discovery syncs. - `/discovery/slack/users/_index.json` - History-independent user id/name lookup rows for direct-message writeback context, materialized from Slack user discovery syncs. diff --git a/packages/slack/src/__tests__/path-mapper-v2.test.ts b/packages/slack/src/__tests__/path-mapper-v2.test.ts index 1b528fa6..d634ccde 100644 --- a/packages/slack/src/__tests__/path-mapper-v2.test.ts +++ b/packages/slack/src/__tests__/path-mapper-v2.test.ts @@ -6,6 +6,7 @@ import { channelMessagesDirectory, directMessageDirectory, directMessagePath, + directMessageThreadReplyLegacyPath, directMessageThreadReplyPath, messageLegacyPath, messagePath, @@ -13,6 +14,7 @@ import { parseSlackDirectMessageThreadReplyPath, reactionPath, slackBotsAliasPath, + slackDirectMessageThreadReplyReadCandidatePaths, slackThreadReplyReadCandidatePaths, threadReplyLegacyPath, threadReplyPath, @@ -116,7 +118,7 @@ test('direct message paths use bare user id message roots', () => { ); assert.equal( directMessageThreadReplyPath('U0123ABCDEF', '1711111111.000100', '1711111222.000200'), - '/slack/users/U0123ABCDEF/messages/1711111111_000100/replies/1711111222_000200.json', + '/slack/users/U0123ABCDEF/messages/1711111111_000100/replies/1711111222_000200/meta.json', ); assert.deepEqual( parseSlackDirectMessagePath(directMessagePath('U0123ABCDEF', '1711111111.000100')), @@ -135,6 +137,28 @@ test('direct message paths use bare user id message roots', () => { replyTs: '1711111222.000200', }, ); + // Legacy flat reply paths must still parse so routing works mid-migration. + assert.deepEqual( + parseSlackDirectMessageThreadReplyPath( + directMessageThreadReplyLegacyPath('U0123ABCDEF', '1711111111.000100', '1711111222.000200'), + ), + { + userId: 'U0123ABCDEF', + messageTs: '1711111111.000100', + replyTs: '1711111222.000200', + }, + ); + assert.deepEqual( + slackDirectMessageThreadReplyReadCandidatePaths( + 'U0123ABCDEF', + '1711111111.000100', + '1711111222.000200', + ), + [ + '/slack/users/U0123ABCDEF/messages/1711111111_000100/replies/1711111222_000200/meta.json', + '/slack/users/U0123ABCDEF/messages/1711111111_000100/replies/1711111222_000200.json', + ], + ); assert.equal(parseSlackDirectMessagePath('/slack/channels/D123/messages/1711111111_000100/meta.json'), null); }); diff --git a/packages/slack/src/index.ts b/packages/slack/src/index.ts index 25af4fb5..ff24adc9 100644 --- a/packages/slack/src/index.ts +++ b/packages/slack/src/index.ts @@ -9,6 +9,7 @@ export { createSlackThreadReplyObjectId, directMessageDirectory, directMessagePath, + directMessageThreadReplyLegacyPath, directMessageThreadReplyPath, fileCommentPath, fileMetadataPath, @@ -26,12 +27,15 @@ export { slackByNameChannelAliasPath, slackByNameUserAliasPath, slackChannelsIndexPath, + slackDirectMessageThreadReplyReadCandidatePaths, slackMessageReadCandidatePaths, slackNameWithId, slackRootIndexPath, + slackThreadReplyReadCandidatePaths, slackTimestampToPathToken, slackUsersIndexPath, threadPath, + threadReplyLegacyPath, threadReplyPath, userMetadataPath, } from './path-mapper.js'; diff --git a/packages/slack/src/layout-prompt.ts b/packages/slack/src/layout-prompt.ts index a2a9a064..8d510b40 100644 --- a/packages/slack/src/layout-prompt.ts +++ b/packages/slack/src/layout-prompt.ts @@ -14,7 +14,7 @@ Always run \`ls\` before constructing a path. v2 standardizes resource directory - \`messages//reactions/--.json\` — reaction records. \`/slack/users/__/meta.json\` — canonical user record. \`/slack/users//messages//meta.json\` — canonical 1:1 direct message records addressed by bare user id. Slack's internal \`D…\` IM channel id stays inside the JSON payload as source metadata; do not mount or write raw \`D…\` paths as the product contract. -\`/slack/users//messages//replies/.json\` — threaded replies in a 1:1 direct message. +\`/slack/users//messages//replies//meta.json\` — threaded replies in a 1:1 direct message (directory record). \`/slack/users/by-name/.json\` and \`/slack/channels/by-name/.json\` — name-keyed alias files pointing to canonical records. Collisions are disambiguated with a short id-derived hash suffix (e.g. \`sam-3b1a9f7c.json\`). \`/slack/users/bots/__.json\` — alias subtree of bot users only, for \`ls\`-style discovery. \`/discovery/slack/channels/_index.json\` and \`/discovery/slack/users/_index.json\` are history-independent lookup indexes for writeback context. They are populated from Slack channel/user discovery syncs and can be mounted even when historical message records under \`/slack/channels/**\` or \`/slack/users/**\` are not mounted. diff --git a/packages/slack/src/path-mapper.ts b/packages/slack/src/path-mapper.ts index 6d1f5c50..6836cf6d 100644 --- a/packages/slack/src/path-mapper.ts +++ b/packages/slack/src/path-mapper.ts @@ -351,10 +351,37 @@ export function directMessagePath(userId: string, messageTs: string): string { ); } +/** + * Canonical 1:1 direct-message thread-reply record path. Like + * {@link threadReplyPath}, the reply is a directory record + * (`replies//meta.json`) so its stem is a directory and can carry children + * without the file/dir name collision that wedges a POSIX mount. Pre-0.8.x + * emitted a flat `replies/.json` leaf — read it back via + * {@link slackDirectMessageThreadReplyReadCandidatePaths}. + */ export function directMessageThreadReplyPath( userId: string, threadTs: string, replyTs: string, +): string { + return joinPath( + directMessageDirectory(userId), + messageSegmentV2(threadTs), + 'replies', + messageSegmentV2(replyTs), + 'meta.json', + ); +} + +/** + * @deprecated Pre-0.8.x emitted a flat `.../replies/.json` leaf. Use + * {@link directMessageThreadReplyPath}. Retained for back-compat reads only — + * see {@link slackDirectMessageThreadReplyReadCandidatePaths}. + */ +export function directMessageThreadReplyLegacyPath( + userId: string, + threadTs: string, + replyTs: string, ): string { return joinPath( directMessageDirectory(userId), @@ -364,6 +391,22 @@ export function directMessageThreadReplyPath( ); } +/** + * Reader hint: candidate paths for a DM thread-reply canonical record, current + * (`/meta.json`) then legacy (`.json`), so a reply mirrored by either + * the current or a pre-0.8.x adapter still reads. + */ +export function slackDirectMessageThreadReplyReadCandidatePaths( + userId: string, + threadTs: string, + replyTs: string, +): string[] { + return [ + directMessageThreadReplyPath(userId, threadTs, replyTs), + directMessageThreadReplyLegacyPath(userId, threadTs, replyTs), + ]; +} + export function parseSlackDirectMessagePath(path: string): SlackDirectMessageReference | null { const match = /^\/slack\/users\/([^/]+)\/messages\/([^/]+)\/meta\.json$/.exec(path); if (!match?.[1] || !match[2]) { @@ -379,7 +422,11 @@ export function parseSlackDirectMessagePath(path: string): SlackDirectMessageRef export function parseSlackDirectMessageThreadReplyPath( path: string, ): SlackDirectMessageThreadReplyReference | null { - const match = /^\/slack\/users\/([^/]+)\/messages\/([^/]+)\/replies\/([^/]+)\.json$/.exec(path); + // Accept both the current reply record (`replies//meta.json`) and the + // legacy flat leaf (`replies/.json`) so routing works mid-migration. + const match = /^\/slack\/users\/([^/]+)\/messages\/([^/]+)\/replies\/([^/]+?)(?:\/meta)?\.json$/.exec( + path, + ); if (!match?.[1] || !match[2] || !match[3]) { return null; } From e3db7fee09bca668cd158a41de38f7b1a1cf4dd8 Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Mon, 8 Jun 2026 16:29:30 +0000 Subject: [PATCH 3/5] chore: apply pr-reviewer fixes for #162 --- packages/dropbox/src/resources.ts | 44 ++++++------------- packages/hubspot/src/resources.ts | 9 ++-- packages/linear/src/resources.ts | 8 ---- packages/slack/discovery/slack/.adapter.md | 14 +++--- .../users/{userId}/messages/.schema.json | 4 +- packages/slack/src/emit-auxiliary-files.ts | 8 ++-- scripts/writeback-discovery-data.mjs | 6 ++- 7 files changed, 33 insertions(+), 60 deletions(-) diff --git a/packages/dropbox/src/resources.ts b/packages/dropbox/src/resources.ts index 9d8908cc..1c0819af 100644 --- a/packages/dropbox/src/resources.ts +++ b/packages/dropbox/src/resources.ts @@ -5,44 +5,28 @@ export interface AdapterResourceConfig { readonly idPattern: RegExp; readonly schema: string; readonly createExample: string; - readonly sampleIndexPath?: string; } export const resources = [ { - name: 'files', - path: '/dropbox/files', - pathPattern: /^\/dropbox\/files\/(?!_index\.json$)[^/]+\.json$/, - idPattern: /^[A-Za-z0-9_.:@%+-][A-Za-z0-9_.:@%+-]*$/, - schema: 'discovery/dropbox/files/.schema.json', - createExample: 'discovery/dropbox/files/.create.example.json', + name: "files", + path: "/dropbox/files", + pathPattern: /^\/dropbox\/files(?:\/[^\/]+(?:\.json)?)?$/, + idPattern: /^[A-Za-z0-9_.:-]+$/, + schema: "discovery/dropbox/files/.schema.json", + createExample: "discovery/dropbox/files/.create.example.json", }, { - name: 'folders', - path: '/dropbox/folders', - pathPattern: /^\/dropbox\/folders\/(?!_index\.json$)[^/]+\.json$/, - idPattern: /^[A-Za-z0-9_.:@%+-][A-Za-z0-9_.:@%+-]*$/, - schema: 'discovery/dropbox/folders/.schema.json', - createExample: 'discovery/dropbox/folders/.create.example.json', - }, - { - name: 'shared-folders', - path: '/dropbox/shared-folders', - pathPattern: /^\/dropbox\/shared-folders\/(?:(?!_index\.json$)[^/]+|by-id\/(?!_index\.json$)[^/]+)\.json$/, - idPattern: /^[A-Za-z0-9_.:@-]+$/, - schema: 'discovery/dropbox/shared-folders/.schema.json', - createExample: 'discovery/dropbox/shared-folders/.create.example.json', - }, - { - name: 'shared-links', - path: '/dropbox/shared-links', - pathPattern: /^\/dropbox\/shared-links\/(?:(?!_index\.json$)[^/]+|by-id\/(?!_index\.json$)[^/]+)\.json$/, - idPattern: /^[A-Za-z0-9_.:@-]+$/, - schema: 'discovery/dropbox/shared-links/.schema.json', - createExample: 'discovery/dropbox/shared-links/.create.example.json', + name: "cursors", + path: "/dropbox/cursors", + pathPattern: /^\/dropbox\/cursors(?:\/[^\/]+(?:\.json)?)?$/, + idPattern: /^[A-Za-z0-9_.:-]+$/, + schema: "discovery/dropbox/cursors/.schema.json", + createExample: "discovery/dropbox/cursors/.create.example.json", }, ] as const satisfies readonly AdapterResourceConfig[]; export function findResourceByPath(path: string): AdapterResourceConfig | undefined { - return resources.find((resource) => resource.pathPattern.test(path)); + const normalizedPath = path.endsWith(".json") ? path : path.replace(/\/$/, ""); + return resources.find((resource) => resource.pathPattern.test(normalizedPath)); } diff --git a/packages/hubspot/src/resources.ts b/packages/hubspot/src/resources.ts index ece5641c..ba4cb129 100644 --- a/packages/hubspot/src/resources.ts +++ b/packages/hubspot/src/resources.ts @@ -7,13 +7,12 @@ export interface AdapterResourceConfig { readonly createExample: string; } -// HubSpot CRM object ids are numeric strings - no slug-prefix form (tightened from 0.2.x) export const resources = [ { name: "contacts", path: "/hubspot/contacts", pathPattern: /^\/hubspot\/contacts(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[0-9]+$/, + idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, schema: "discovery/hubspot/contacts/.schema.json", createExample: "discovery/hubspot/contacts/.create.example.json", }, @@ -21,7 +20,7 @@ export const resources = [ name: "companies", path: "/hubspot/companies", pathPattern: /^\/hubspot\/companies(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[0-9]+$/, + idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, schema: "discovery/hubspot/companies/.schema.json", createExample: "discovery/hubspot/companies/.create.example.json", }, @@ -29,7 +28,7 @@ export const resources = [ name: "deals", path: "/hubspot/deals", pathPattern: /^\/hubspot\/deals(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[0-9]+$/, + idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, schema: "discovery/hubspot/deals/.schema.json", createExample: "discovery/hubspot/deals/.create.example.json", }, @@ -37,7 +36,7 @@ export const resources = [ name: "tickets", path: "/hubspot/tickets", pathPattern: /^\/hubspot\/tickets(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[0-9]+$/, + idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, schema: "discovery/hubspot/tickets/.schema.json", createExample: "discovery/hubspot/tickets/.create.example.json", }, diff --git a/packages/linear/src/resources.ts b/packages/linear/src/resources.ts index 1303f9f0..fd1efd58 100644 --- a/packages/linear/src/resources.ts +++ b/packages/linear/src/resources.ts @@ -24,14 +24,6 @@ export const resources = [ schema: "discovery/linear/issues/{issueId}/comments/.schema.json", createExample: "discovery/linear/issues/{issueId}/comments/.create.example.json", }, - { - name: "agent-activities", - path: "/linear/agent-sessions/{sessionId}/activities", - pathPattern: /^\/linear\/agent-sessions\/[^\/]+\/activities(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^(?:activity_[A-Za-z0-9_-]+|[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i, - schema: "discovery/linear/agent-sessions/{sessionId}/activities/.schema.json", - createExample: "discovery/linear/agent-sessions/{sessionId}/activities/.create.example.json", - }, ] as const satisfies readonly AdapterResourceConfig[]; export function findResourceByPath(path: string): AdapterResourceConfig | undefined { diff --git a/packages/slack/discovery/slack/.adapter.md b/packages/slack/discovery/slack/.adapter.md index 8513b130..737ad791 100644 --- a/packages/slack/discovery/slack/.adapter.md +++ b/packages/slack/discovery/slack/.adapter.md @@ -1,25 +1,23 @@ # Slack adapter -The Slack adapter exposes channels, users, messages, threads, replies, files, and reactions under `/slack`, with writeback routes for posting channel messages, direct messages, replies, and reactions. It also advertises history-independent discovery lookup indexes for Slack channel and user ids under `/discovery/slack`. +The Slack adapter exposes channels, users, messages, threads, replies, files, and reactions under `/slack`, with writeback routes for posting channel messages, direct messages, replies, and reactions. Direct messages use `/slack/users//messages` as the product contract for both reads and writes; Slack internal `D...` IM channel ids stay in record payload metadata. It also advertises history-independent discovery lookup indexes for Slack channel and user ids under `/discovery/slack`. Read-only mounts: - `/slack/channels/.json` - Channel records. - `/slack/channels//messages//meta.json` - Message records. -- `/slack/channels//messages//replies/.json` - Thread reply records. +- `/slack/channels//threads//replies//meta.json` - Thread reply records (directory records; legacy flat `/messages//replies/.json` writeback paths still route for edits/deletes). - `/slack/users/.json` - User records. - `/slack/users//messages//meta.json` - 1:1 direct message records addressed by bare Slack user id. -- `/slack/users//messages//replies//meta.json` - Threaded replies in a 1:1 direct message (directory record; legacy flat `.json` still readable). +- `/slack/users//messages//replies//meta.json` - Threaded replies in a 1:1 direct message (directory records; legacy flat `.json` still readable). - `/discovery/slack/channels/_index.json` - History-independent channel id/name lookup rows for writeback context, materialized from Slack channel discovery syncs. - `/discovery/slack/users/_index.json` - History-independent user id/name lookup rows for direct-message writeback context, materialized from Slack user discovery syncs. -Direct messages use `/slack/users//messages` as the product contract for both reads and writes. Slack's internal `D...` IM channel id is preserved in record payload metadata as the source conversation id; raw `D...` channel paths are diagnostic/legacy only and are not the mount contract for DMs. - Resources: | Resource | Schema | Create example | ID pattern | What it does | |---|---|---|---|---| | `/slack/channels/{channelId}/messages/.json` | `/slack/channels/{channelId}/messages/.schema.json` | `/slack/channels/{channelId}/messages/.create.example.json` | `^(?:meta\|(?:[A-Za-z0-9_.:-]+--)?\d{10,}(?:_\d+)?)$` | Posts a top-level Slack message. | -| `/slack/users/{userId}/messages/.json` | `/slack/users/{userId}/messages/.schema.json` | `/slack/users/{userId}/messages/.create.example.json` | `^$` | Reads 1:1 direct message records and opens or reuses a direct message conversation when posting a new draft. | +| `/slack/users/{userId}/messages/.json` | `/slack/users/{userId}/messages/.schema.json` | `/slack/users/{userId}/messages/.create.example.json` | `^$` | Opens or reuses a direct message conversation and posts a Slack message. | | `/slack/channels/{channelId}/messages/{messageTs}/replies/.json` | `/slack/channels/{channelId}/messages/{messageTs}/replies/.schema.json` | `/slack/channels/{channelId}/messages/{messageTs}/replies/.create.example.json` | `^(?:[A-Za-z0-9_.:-]+--)?\d{10,}(?:_\d+)?$` | Posts a reply in a Slack thread. | | `/slack/channels/{channelId}/messages/{messageTs}/reactions/.json` | `/slack/channels/{channelId}/messages/{messageTs}/reactions/.schema.json` | `/slack/channels/{channelId}/messages/{messageTs}/reactions/.create.example.json` | `^[A-Za-z0-9_.:-]+(?:--[A-Za-z0-9_.:-]+)*$` | Adds an emoji reaction to a Slack message. | @@ -64,7 +62,7 @@ Fields: - `unfurl_media` (optional, boolean) - Whether Slack should unfurl media. - `mrkdwn` (optional, boolean) - Whether Slack should parse mrkdwn in text. -### Slack direct message +### Post Slack direct message Resource: `/slack/users/{userId}/messages/.json` Schema: `/slack/users/{userId}/messages/.schema.json` @@ -73,8 +71,6 @@ Required fields: none at the top level. Optional fields: `text`, `blocks`, `attachments`, `username`, `icon_emoji`, `icon_url`, `unfurl_links`, `unfurl_media`, `mrkdwn`. Validation: provide at least one of `text`, `blocks`, `attachments`. -Synced 1:1 DM records are materialized at `/slack/users//messages//meta.json` and keep Slack's raw `D...` conversation id in read-only fields such as `channel`, `channelId`, or `_webhook.raw_event.channel` when present. New draft writes in the same directory use `conversations.open` with the bare `` and then post the message. - Fields: - `text` (optional, string) - Message text. Required unless blocks or attachments are supplied. diff --git a/packages/slack/discovery/slack/users/{userId}/messages/.schema.json b/packages/slack/discovery/slack/users/{userId}/messages/.schema.json index 5d44f6cb..3135de3b 100644 --- a/packages/slack/discovery/slack/users/{userId}/messages/.schema.json +++ b/packages/slack/discovery/slack/users/{userId}/messages/.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Slack direct message", + "title": "Post Slack direct message", "type": "object", "required": [], "anyOf": [ @@ -135,5 +135,5 @@ } }, "additionalProperties": false, - "description": "Full direct message resource record schema. Synced records are read under /slack/users/{userId}/messages//meta.json; create drafts in the same directory post a Slack DM. Fields marked readOnly are synced from the provider and cannot be written by agents." + "description": "Full resource record schema. Fields marked readOnly are synced from the provider and cannot be written by agents." } diff --git a/packages/slack/src/emit-auxiliary-files.ts b/packages/slack/src/emit-auxiliary-files.ts index ed4a2bc5..3e58b25a 100644 --- a/packages/slack/src/emit-auxiliary-files.ts +++ b/packages/slack/src/emit-auxiliary-files.ts @@ -26,10 +26,10 @@ * Bot-flip (`is_bot: true → false`) deletes the stale `bots/` alias * while leaving `by-name` in place. * - * 3. **Message** / **Thread** / **Thread reply** records emit only the - * canonical `meta.json` (and per-reply `.json`) under their - * channel directory. No alias fan-out and no index file at this - * level. Because the message record itself doesn't carry the parent + * 3. **Message** / **Thread** / **Thread reply** records emit only their + * canonical `meta.json` under the channel directory. No alias fan-out + * and no index file at this level. Because the message record itself + * doesn't carry the parent * channel name, the path falls back to the bare `` segment * — readers join via the channel index to discover the human-readable * directory name. diff --git a/scripts/writeback-discovery-data.mjs b/scripts/writeback-discovery-data.mjs index 65f8a340..f07365cb 100644 --- a/scripts/writeback-discovery-data.mjs +++ b/scripts/writeback-discovery-data.mjs @@ -395,12 +395,14 @@ export const adapters = [ slug: 'slack', title: 'Slack adapter', overview: - 'The Slack adapter exposes channels, users, messages, threads, replies, files, and reactions under `/slack`, with writeback routes for posting channel messages, direct messages, replies, and reactions. It also advertises history-independent discovery lookup indexes for Slack channel and user ids under `/discovery/slack`.', + 'The Slack adapter exposes channels, users, messages, threads, replies, files, and reactions under `/slack`, with writeback routes for posting channel messages, direct messages, replies, and reactions. Direct messages use `/slack/users//messages` as the product contract for both reads and writes; Slack internal `D...` IM channel ids stay in record payload metadata. It also advertises history-independent discovery lookup indexes for Slack channel and user ids under `/discovery/slack`.', readPaths: [ ['/slack/channels/.json', 'Channel records.'], ['/slack/channels//messages//meta.json', 'Message records.'], - ['/slack/channels//messages//replies/.json', 'Thread reply records.'], + ['/slack/channels//threads//replies//meta.json', 'Thread reply records (directory records; legacy flat `/messages//replies/.json` writeback paths still route for edits/deletes).'], ['/slack/users/.json', 'User records.'], + ['/slack/users//messages//meta.json', '1:1 direct message records addressed by bare Slack user id.'], + ['/slack/users//messages//replies//meta.json', 'Threaded replies in a 1:1 direct message (directory records; legacy flat `.json` still readable).'], ['/discovery/slack/channels/_index.json', 'History-independent channel id/name lookup rows for writeback context, materialized from Slack channel discovery syncs.'], ['/discovery/slack/users/_index.json', 'History-independent user id/name lookup rows for direct-message writeback context, materialized from Slack user discovery syncs.'], ], From e3eb5b99977ed11e053404377cabc01d2ef64fe2 Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Tue, 9 Jun 2026 09:20:42 +0200 Subject: [PATCH 4/5] chore(core): regenerate writeback-path catalog to match adapter resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's "generated writeback-path catalog is in sync with adapter resources" check (adapter-core) was failing: a prior commit changed dropbox/linear/hubspot `resources.ts` (e.g. dropbox `shared-folders`/`shared-links`/`folders` → `cursors`/`files`; linear dropped `agent-activities`) without regenerating the checked-in catalog. The generator reads each adapter's BUILT `dist/resources.js`, so the drift only surfaces on a fresh CI build — locally a stale `dist` masked it. Rebuilt all adapters and re-ran `adapter-core writeback-paths generate`. Slack is unaffected — this PR's reply-path change is read-side only and does not touch slack's writeback `resources.ts`, so its catalog entry is unchanged. `--check` passes; adapter-core (105) and slack (104) suites green. Co-Authored-By: Claude Opus 4.8 --- .../writeback-paths/catalog.generated.json | 28 +++---------------- .../src/writeback-paths/catalog.generated.ts | 28 +++---------------- 2 files changed, 8 insertions(+), 48 deletions(-) diff --git a/packages/core/src/writeback-paths/catalog.generated.json b/packages/core/src/writeback-paths/catalog.generated.json index 75bb56a5..c6826d13 100644 --- a/packages/core/src/writeback-paths/catalog.generated.json +++ b/packages/core/src/writeback-paths/catalog.generated.json @@ -108,27 +108,15 @@ ] }, "dropbox": { - "files": [ - { - "path": "/dropbox/files", - "params": [] - } - ], - "folders": [ - { - "path": "/dropbox/folders", - "params": [] - } - ], - "shared-folders": [ + "cursors": [ { - "path": "/dropbox/shared-folders", + "path": "/dropbox/cursors", "params": [] } ], - "shared-links": [ + "files": [ { - "path": "/dropbox/shared-links", + "path": "/dropbox/files", "params": [] } ] @@ -345,14 +333,6 @@ ] }, "linear": { - "agent-activities": [ - { - "path": "/linear/agent-sessions/{sessionId}/activities", - "params": [ - "sessionId" - ] - } - ], "comments": [ { "path": "/linear/issues/{issueId}/comments", diff --git a/packages/core/src/writeback-paths/catalog.generated.ts b/packages/core/src/writeback-paths/catalog.generated.ts index 3578146c..e6f3e641 100644 --- a/packages/core/src/writeback-paths/catalog.generated.ts +++ b/packages/core/src/writeback-paths/catalog.generated.ts @@ -117,27 +117,15 @@ export const WRITEBACK_PATH_CATALOG = { ] }, "dropbox": { - "files": [ - { - "path": "/dropbox/files", - "params": [] - } - ], - "folders": [ - { - "path": "/dropbox/folders", - "params": [] - } - ], - "shared-folders": [ + "cursors": [ { - "path": "/dropbox/shared-folders", + "path": "/dropbox/cursors", "params": [] } ], - "shared-links": [ + "files": [ { - "path": "/dropbox/shared-links", + "path": "/dropbox/files", "params": [] } ] @@ -354,14 +342,6 @@ export const WRITEBACK_PATH_CATALOG = { ] }, "linear": { - "agent-activities": [ - { - "path": "/linear/agent-sessions/{sessionId}/activities", - "params": [ - "sessionId" - ] - } - ], "comments": [ { "path": "/linear/issues/{issueId}/comments", From 88c24d3e93b8df4661f04aaab6aaa4cd080edcec Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Tue, 9 Jun 2026 09:27:09 +0200 Subject: [PATCH 5/5] revert(out-of-scope): restore dropbox/hubspot/linear resources + catalog to main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pr-reviewer bot's earlier commit on this branch changed dropbox/hubspot/linear `resources.ts` — unrelated to this slack reply-path PR — and in particular removed linear's `agent-activities` writeback resource (added in #146). That broke CI two ways: the writeback-path catalog went out of sync (adapter-core test), and once regenerated, `relay-helpers/src/linear.ts` failed to typecheck because `ProviderClient<"linear">` no longer had `agent-activities` (`agentActivity`/`respond`/`acknowledge`). Revert those three resources.ts files to origin/main and regenerate the writeback-path catalog (restoring agent-activities etc.), so this PR contains ONLY the slack thread/DM reply directory-record change it's about. The bot's slack-related edits (discovery readPaths, DM schema wording, emit-auxiliary-files) are kept — they align with this fix. Verified: adapter-core (105) and slack (104) suites green, relay-helpers typecheck clean, writeback-discovery + digest-contracts checks pass, full `turbo build` succeeds. Co-Authored-By: Claude Opus 4.8 --- .../writeback-paths/catalog.generated.json | 28 ++++++++++-- .../src/writeback-paths/catalog.generated.ts | 28 ++++++++++-- packages/dropbox/src/resources.ts | 44 +++++++++++++------ packages/hubspot/src/resources.ts | 9 ++-- packages/linear/src/resources.ts | 8 ++++ 5 files changed, 91 insertions(+), 26 deletions(-) diff --git a/packages/core/src/writeback-paths/catalog.generated.json b/packages/core/src/writeback-paths/catalog.generated.json index c6826d13..75bb56a5 100644 --- a/packages/core/src/writeback-paths/catalog.generated.json +++ b/packages/core/src/writeback-paths/catalog.generated.json @@ -108,15 +108,27 @@ ] }, "dropbox": { - "cursors": [ + "files": [ { - "path": "/dropbox/cursors", + "path": "/dropbox/files", "params": [] } ], - "files": [ + "folders": [ { - "path": "/dropbox/files", + "path": "/dropbox/folders", + "params": [] + } + ], + "shared-folders": [ + { + "path": "/dropbox/shared-folders", + "params": [] + } + ], + "shared-links": [ + { + "path": "/dropbox/shared-links", "params": [] } ] @@ -333,6 +345,14 @@ ] }, "linear": { + "agent-activities": [ + { + "path": "/linear/agent-sessions/{sessionId}/activities", + "params": [ + "sessionId" + ] + } + ], "comments": [ { "path": "/linear/issues/{issueId}/comments", diff --git a/packages/core/src/writeback-paths/catalog.generated.ts b/packages/core/src/writeback-paths/catalog.generated.ts index e6f3e641..3578146c 100644 --- a/packages/core/src/writeback-paths/catalog.generated.ts +++ b/packages/core/src/writeback-paths/catalog.generated.ts @@ -117,15 +117,27 @@ export const WRITEBACK_PATH_CATALOG = { ] }, "dropbox": { - "cursors": [ + "files": [ { - "path": "/dropbox/cursors", + "path": "/dropbox/files", "params": [] } ], - "files": [ + "folders": [ { - "path": "/dropbox/files", + "path": "/dropbox/folders", + "params": [] + } + ], + "shared-folders": [ + { + "path": "/dropbox/shared-folders", + "params": [] + } + ], + "shared-links": [ + { + "path": "/dropbox/shared-links", "params": [] } ] @@ -342,6 +354,14 @@ export const WRITEBACK_PATH_CATALOG = { ] }, "linear": { + "agent-activities": [ + { + "path": "/linear/agent-sessions/{sessionId}/activities", + "params": [ + "sessionId" + ] + } + ], "comments": [ { "path": "/linear/issues/{issueId}/comments", diff --git a/packages/dropbox/src/resources.ts b/packages/dropbox/src/resources.ts index 1c0819af..9d8908cc 100644 --- a/packages/dropbox/src/resources.ts +++ b/packages/dropbox/src/resources.ts @@ -5,28 +5,44 @@ export interface AdapterResourceConfig { readonly idPattern: RegExp; readonly schema: string; readonly createExample: string; + readonly sampleIndexPath?: string; } export const resources = [ { - name: "files", - path: "/dropbox/files", - pathPattern: /^\/dropbox\/files(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[A-Za-z0-9_.:-]+$/, - schema: "discovery/dropbox/files/.schema.json", - createExample: "discovery/dropbox/files/.create.example.json", + name: 'files', + path: '/dropbox/files', + pathPattern: /^\/dropbox\/files\/(?!_index\.json$)[^/]+\.json$/, + idPattern: /^[A-Za-z0-9_.:@%+-][A-Za-z0-9_.:@%+-]*$/, + schema: 'discovery/dropbox/files/.schema.json', + createExample: 'discovery/dropbox/files/.create.example.json', }, { - name: "cursors", - path: "/dropbox/cursors", - pathPattern: /^\/dropbox\/cursors(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[A-Za-z0-9_.:-]+$/, - schema: "discovery/dropbox/cursors/.schema.json", - createExample: "discovery/dropbox/cursors/.create.example.json", + name: 'folders', + path: '/dropbox/folders', + pathPattern: /^\/dropbox\/folders\/(?!_index\.json$)[^/]+\.json$/, + idPattern: /^[A-Za-z0-9_.:@%+-][A-Za-z0-9_.:@%+-]*$/, + schema: 'discovery/dropbox/folders/.schema.json', + createExample: 'discovery/dropbox/folders/.create.example.json', + }, + { + name: 'shared-folders', + path: '/dropbox/shared-folders', + pathPattern: /^\/dropbox\/shared-folders\/(?:(?!_index\.json$)[^/]+|by-id\/(?!_index\.json$)[^/]+)\.json$/, + idPattern: /^[A-Za-z0-9_.:@-]+$/, + schema: 'discovery/dropbox/shared-folders/.schema.json', + createExample: 'discovery/dropbox/shared-folders/.create.example.json', + }, + { + name: 'shared-links', + path: '/dropbox/shared-links', + pathPattern: /^\/dropbox\/shared-links\/(?:(?!_index\.json$)[^/]+|by-id\/(?!_index\.json$)[^/]+)\.json$/, + idPattern: /^[A-Za-z0-9_.:@-]+$/, + schema: 'discovery/dropbox/shared-links/.schema.json', + createExample: 'discovery/dropbox/shared-links/.create.example.json', }, ] as const satisfies readonly AdapterResourceConfig[]; export function findResourceByPath(path: string): AdapterResourceConfig | undefined { - const normalizedPath = path.endsWith(".json") ? path : path.replace(/\/$/, ""); - return resources.find((resource) => resource.pathPattern.test(normalizedPath)); + return resources.find((resource) => resource.pathPattern.test(path)); } diff --git a/packages/hubspot/src/resources.ts b/packages/hubspot/src/resources.ts index ba4cb129..ece5641c 100644 --- a/packages/hubspot/src/resources.ts +++ b/packages/hubspot/src/resources.ts @@ -7,12 +7,13 @@ export interface AdapterResourceConfig { readonly createExample: string; } +// HubSpot CRM object ids are numeric strings - no slug-prefix form (tightened from 0.2.x) export const resources = [ { name: "contacts", path: "/hubspot/contacts", pathPattern: /^\/hubspot\/contacts(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, + idPattern: /^[0-9]+$/, schema: "discovery/hubspot/contacts/.schema.json", createExample: "discovery/hubspot/contacts/.create.example.json", }, @@ -20,7 +21,7 @@ export const resources = [ name: "companies", path: "/hubspot/companies", pathPattern: /^\/hubspot\/companies(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, + idPattern: /^[0-9]+$/, schema: "discovery/hubspot/companies/.schema.json", createExample: "discovery/hubspot/companies/.create.example.json", }, @@ -28,7 +29,7 @@ export const resources = [ name: "deals", path: "/hubspot/deals", pathPattern: /^\/hubspot\/deals(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, + idPattern: /^[0-9]+$/, schema: "discovery/hubspot/deals/.schema.json", createExample: "discovery/hubspot/deals/.create.example.json", }, @@ -36,7 +37,7 @@ export const resources = [ name: "tickets", path: "/hubspot/tickets", pathPattern: /^\/hubspot\/tickets(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, + idPattern: /^[0-9]+$/, schema: "discovery/hubspot/tickets/.schema.json", createExample: "discovery/hubspot/tickets/.create.example.json", }, diff --git a/packages/linear/src/resources.ts b/packages/linear/src/resources.ts index fd1efd58..1303f9f0 100644 --- a/packages/linear/src/resources.ts +++ b/packages/linear/src/resources.ts @@ -24,6 +24,14 @@ export const resources = [ schema: "discovery/linear/issues/{issueId}/comments/.schema.json", createExample: "discovery/linear/issues/{issueId}/comments/.create.example.json", }, + { + name: "agent-activities", + path: "/linear/agent-sessions/{sessionId}/activities", + pathPattern: /^\/linear\/agent-sessions\/[^\/]+\/activities(?:\/[^\/]+(?:\.json)?)?$/, + idPattern: /^(?:activity_[A-Za-z0-9_-]+|[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i, + schema: "discovery/linear/agent-sessions/{sessionId}/activities/.schema.json", + createExample: "discovery/linear/agent-sessions/{sessionId}/activities/.create.example.json", + }, ] as const satisfies readonly AdapterResourceConfig[]; export function findResourceByPath(path: string): AdapterResourceConfig | undefined {