@@ -249,6 +249,52 @@ export interface ExternalDataSource {
249249 schemas ?: ExternalDataSourceSchema [ ] | string ;
250250}
251251
252+ export interface FolderInstructionsUser {
253+ id ?: number ;
254+ uuid ?: string ;
255+ first_name ?: string ;
256+ last_name ?: string | null ;
257+ email ?: string ;
258+ }
259+
260+ export interface FolderInstructions {
261+ id : string ;
262+ content : string ;
263+ version : number ;
264+ is_latest : boolean ;
265+ created_by : FolderInstructionsUser | null ;
266+ created_at : string ;
267+ updated_at : string ;
268+ }
269+
270+ export interface FolderInstructionsVersion {
271+ id : string ;
272+ version : number ;
273+ is_latest : boolean ;
274+ created_by : FolderInstructionsUser | null ;
275+ created_at : string ;
276+ }
277+
278+ interface PaginatedFolderInstructionsVersions {
279+ count : number ;
280+ next : string | null ;
281+ previous : string | null ;
282+ results : FolderInstructionsVersion [ ] ;
283+ }
284+
285+ // Thrown when PUT /instructions/ rejects a publish because the caller's
286+ // `base_version` is older than the current latest. Callers can re-fetch and
287+ // retry against the new latest.
288+ export class FolderInstructionsConflictError extends Error {
289+ status = 409 ;
290+ constructor (
291+ message = "Folder instructions changed since you started editing" ,
292+ ) {
293+ super ( message ) ;
294+ this . name = "FolderInstructionsConflictError" ;
295+ }
296+ }
297+
252298export interface TaskArtifactUploadRequest {
253299 name : string ;
254300 type : "user_attachment" ;
@@ -848,6 +894,113 @@ export class PostHogAPIClient {
848894 }
849895 }
850896
897+ // Per-folder, versioned markdown instructions for a desktop folder. The
898+ // endpoint is keyed on the FileSystem row id (must be `type === "folder"`).
899+ // Returns the current latest version or null when none has been published.
900+ async getDesktopFolderInstructions (
901+ folderId : string ,
902+ ) : Promise < FolderInstructions | null > {
903+ const teamId = await this . getTeamId ( ) ;
904+ const urlPath = `/api/projects/${ teamId } /desktop_file_system/${ encodeURIComponent ( folderId ) } /instructions/` ;
905+ const url = new URL ( `${ this . api . baseUrl } ${ urlPath } ` ) ;
906+ const response = await this . api . fetcher . fetch ( {
907+ method : "get" ,
908+ url,
909+ path : urlPath ,
910+ } ) ;
911+ if ( response . status === 404 ) return null ;
912+ if ( ! response . ok ) {
913+ throw new Error (
914+ `Failed to fetch folder instructions: ${ response . statusText } ` ,
915+ ) ;
916+ }
917+ return ( await response . json ( ) ) as FolderInstructions ;
918+ }
919+
920+ // Publish a new version of the folder's instructions. Pass `base_version`
921+ // (the latest version the editor was started from) for optimistic
922+ // concurrency; use 0 when no instructions exist yet. A 409 turns into a
923+ // typed `FolderInstructionsConflictError` so the UI can prompt to reload.
924+ async putDesktopFolderInstructions (
925+ folderId : string ,
926+ input : { content : string ; base_version ?: number } ,
927+ ) : Promise < FolderInstructions > {
928+ const teamId = await this . getTeamId ( ) ;
929+ const urlPath = `/api/projects/${ teamId } /desktop_file_system/${ encodeURIComponent ( folderId ) } /instructions/` ;
930+ const url = new URL ( `${ this . api . baseUrl } ${ urlPath } ` ) ;
931+ const response = await this . api . fetcher . fetch ( {
932+ method : "put" ,
933+ url,
934+ path : urlPath ,
935+ overrides : {
936+ body : JSON . stringify ( input ) ,
937+ } ,
938+ } ) ;
939+ if ( response . status === 409 ) {
940+ throw new FolderInstructionsConflictError ( ) ;
941+ }
942+ if ( ! response . ok ) {
943+ throw new Error (
944+ `Failed to publish folder instructions: ${ response . statusText } ` ,
945+ ) ;
946+ }
947+ return ( await response . json ( ) ) as FolderInstructions ;
948+ }
949+
950+ // Soft-delete all versions of this folder's instructions. The folder row
951+ // itself is not affected.
952+ async deleteDesktopFolderInstructions ( folderId : string ) : Promise < void > {
953+ const teamId = await this . getTeamId ( ) ;
954+ const urlPath = `/api/projects/${ teamId } /desktop_file_system/${ encodeURIComponent ( folderId ) } /instructions/` ;
955+ const url = new URL ( `${ this . api . baseUrl } ${ urlPath } ` ) ;
956+ const response = await this . api . fetcher . fetch ( {
957+ method : "delete" ,
958+ url,
959+ path : urlPath ,
960+ } ) ;
961+ if ( ! response . ok && response . status !== 404 ) {
962+ throw new Error (
963+ `Failed to delete folder instructions: ${ response . statusText } ` ,
964+ ) ;
965+ }
966+ }
967+
968+ // List version metadata (no content) newest-first. Single page is enough for
969+ // the typical UI; we cap follow-up pages to avoid runaway pagination on
970+ // pathological histories.
971+ async listDesktopFolderInstructionVersions (
972+ folderId : string ,
973+ ) : Promise < FolderInstructionsVersion [ ] > {
974+ const VERSIONS_MAX_PAGES = 20 ;
975+ const teamId = await this . getTeamId ( ) ;
976+ const all : FolderInstructionsVersion [ ] = [ ] ;
977+ let urlPath = `/api/projects/${ teamId } /desktop_file_system/${ encodeURIComponent ( folderId ) } /instructions/versions/` ;
978+ for ( let i = 0 ; i < VERSIONS_MAX_PAGES ; i ++ ) {
979+ const url = new URL ( `${ this . api . baseUrl } ${ urlPath } ` ) ;
980+ const response = await this . api . fetcher . fetch ( {
981+ method : "get" ,
982+ url,
983+ path : urlPath ,
984+ } ) ;
985+ if ( ! response . ok ) {
986+ throw new Error (
987+ `Failed to fetch folder instruction versions: ${ response . statusText } ` ,
988+ ) ;
989+ }
990+ const page =
991+ ( await response . json ( ) ) as PaginatedFolderInstructionsVersions ;
992+ all . push ( ...page . results ) ;
993+ if ( ! page . next ) return all ;
994+ const nextUrl = new URL ( page . next ) ;
995+ urlPath = `${ nextUrl . pathname } ${ nextUrl . search } ` ;
996+ }
997+ log . warn (
998+ `listDesktopFolderInstructionVersions hit MAX_PAGES (${ VERSIONS_MAX_PAGES } ); returning partial results` ,
999+ { folderId, returned : all . length } ,
1000+ ) ;
1001+ return all ;
1002+ }
1003+
8511004 async getGithubLogin ( ) : Promise < string | null > {
8521005 const data = ( await this . api . get ( "/api/users/{uuid}/github_login/" , {
8531006 path : { uuid : "@me" } ,
0 commit comments