types.ts
  1  /**
  2   * Team Memory Sync Types
  3   *
  4   * Zod schemas and types for the repo-scoped team memory sync API.
  5   * Based on the backend API contract from anthropic/anthropic#250711.
  6   */
  7  
  8  import { z } from 'zod/v4'
  9  import { lazySchema } from '../../utils/lazySchema.js'
 10  
 11  /**
 12   * Content portion of team memory data - flat key-value storage.
 13   * Keys are file paths relative to the team memory directory (e.g. "MEMORY.md", "patterns.md").
 14   * Values are UTF-8 string content (typically Markdown).
 15   */
 16  export const TeamMemoryContentSchema = lazySchema(() =>
 17    z.object({
 18      entries: z.record(z.string(), z.string()),
 19      // Per-key SHA-256 of entry content (`sha256:<hex>`). Added in
 20      // anthropic/anthropic#283027. Optional for forward-compat with older
 21      // server deployments; empty map when entries is empty.
 22      entryChecksums: z.record(z.string(), z.string()).optional(),
 23    }),
 24  )
 25  
 26  /**
 27   * Full response from GET /api/claude_code/team_memory
 28   */
 29  export const TeamMemoryDataSchema = lazySchema(() =>
 30    z.object({
 31      organizationId: z.string(),
 32      repo: z.string(),
 33      version: z.number(),
 34      lastModified: z.string(), // ISO 8601 timestamp
 35      checksum: z.string(), // SHA256 with 'sha256:' prefix
 36      content: TeamMemoryContentSchema(),
 37    }),
 38  )
 39  
 40  /**
 41   * Structured 413 error body from the server (anthropic/anthropic#293258).
 42   * The server's RequestTooLargeException serializes error_code and the
 43   * extra_details dict flattened into error.details. We only model the
 44   * too-many-entries case; entry-too-large is handled via MAX_FILE_SIZE_BYTES
 45   * pre-check on the client side and would need a separate schema.
 46   */
 47  export const TeamMemoryTooManyEntriesSchema = lazySchema(() =>
 48    z.object({
 49      error: z.object({
 50        details: z.object({
 51          error_code: z.literal('team_memory_too_many_entries'),
 52          max_entries: z.number().int().positive(),
 53          received_entries: z.number().int().positive(),
 54        }),
 55      }),
 56    }),
 57  )
 58  
 59  export type TeamMemoryData = z.infer<ReturnType<typeof TeamMemoryDataSchema>>
 60  
 61  /**
 62   * A file skipped during push because it contains a detected secret.
 63   * The path is relative to the team memory directory. Only the matched
 64   * gitleaks rule ID is recorded — never the secret value itself.
 65   */
 66  export type SkippedSecretFile = {
 67    path: string
 68    /** Gitleaks rule ID (e.g., "github-pat", "aws-access-token") */
 69    ruleId: string
 70    /** Human-readable label derived from rule ID */
 71    label: string
 72  }
 73  
 74  /**
 75   * Result from fetching team memory
 76   */
 77  export type TeamMemorySyncFetchResult = {
 78    success: boolean
 79    data?: TeamMemoryData
 80    isEmpty?: boolean // true if 404 (no data exists)
 81    notModified?: boolean // true if 304 (ETag matched, no changes)
 82    checksum?: string // ETag from response header
 83    error?: string
 84    skipRetry?: boolean
 85    errorType?: 'auth' | 'timeout' | 'network' | 'parse' | 'unknown'
 86    httpStatus?: number
 87  }
 88  
 89  /**
 90   * Lightweight metadata-only probe result (GET ?view=hashes).
 91   * Contains per-key checksums without entry bodies. Used to refresh
 92   * serverChecksums cheaply during 412 conflict resolution.
 93   */
 94  export type TeamMemoryHashesResult = {
 95    success: boolean
 96    version?: number
 97    checksum?: string
 98    entryChecksums?: Record<string, string>
 99    error?: string
100    errorType?: 'auth' | 'timeout' | 'network' | 'parse' | 'unknown'
101    httpStatus?: number
102  }
103  
104  /**
105   * Result from uploading team memory with conflict info
106   */
107  export type TeamMemorySyncPushResult = {
108    success: boolean
109    filesUploaded: number
110    checksum?: string
111    conflict?: boolean // true if 412 Precondition Failed
112    error?: string
113    /** Files skipped because they contain detected secrets (PSR M22174). */
114    skippedSecrets?: SkippedSecretFile[]
115    errorType?:
116      | 'auth'
117      | 'timeout'
118      | 'network'
119      | 'conflict'
120      | 'unknown'
121      | 'no_oauth'
122      | 'no_repo'
123    httpStatus?: number
124  }
125  
126  /**
127   * Result from uploading team memory
128   */
129  export type TeamMemorySyncUploadResult = {
130    success: boolean
131    checksum?: string
132    lastModified?: string
133    conflict?: boolean // true if 412 Precondition Failed
134    error?: string
135    errorType?: 'auth' | 'timeout' | 'network' | 'unknown'
136    httpStatus?: number
137    /**
138     * Structured error_code from a parsed 413 body (anthropic/anthropic#293258).
139     * Currently only 'team_memory_too_many_entries' is modelled; if the server
140     * adds more (entry_too_large, total_bytes_exceeded) they'd extend this
141     * union.  Passed straight through to the tengu_team_mem_sync_push event
142     * as a Datadog-filterable facet.
143     */
144    serverErrorCode?: 'team_memory_too_many_entries'
145    /**
146     * Server-enforced max_entries, populated when serverErrorCode is
147     * team_memory_too_many_entries. Lets the caller cache the effective
148     * (possibly per-org) limit for subsequent pushes.
149     */
150    serverMaxEntries?: number
151    /**
152     * How many entries the rejected push would have produced after merge.
153     * Populated alongside serverMaxEntries.
154     */
155    serverReceivedEntries?: number
156  }