← Space Hog
API

Drive Space Hog from your own code

The one metered task, space, is available over HTTP. It takes a storage listing as the page has read it - what the storage is and what the person is trying to do, how much space they want back, the totals by kind of file, the largest folders, and the candidates worth deciding on, each one numbered line (i1, i2 ...): a group of likely duplicate copies with the space its extra copies take, or a single big file with its size and kind - and returns a plan with one action per candidate (remove_copies, archive, ask, keep), a reason for each, a question for each it asks about, what the plan frees, a verdict (reaches or short) and a note.

The division of labour first: the page reads the listing, totals the space, finds the duplicates and adds up what the plan frees; the run decides each candidate, naming candidates only by id so the page can tie each action to real files with real byte sizes. The page reads up to 20000 lines and sends up to 30 candidates. After the reply it keeps one allowed action per candidate - the extra copies of a duplicate group may be removed, a single file may be archived, kept or asked about, and nothing else - adds up what the plan frees from its own byte counts, checks the verdict against the target and the reply's own total, and warns when a sentence talks about deleting a file. Nothing is moved or deleted from the page, and the plan never deletes a file's only copy. Your code can do the same with the engine files the page loads (see "Build the input yourself").

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"space-hog"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (task, goal, target, totals, folders, items) is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the space run is metered and needs a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field, the input and the rules

Every run sends task: space (the one task is space); a missing or different one is still answered as space. All fields are strings. What each holds, in the words of the system prompt, with the first sample's value (its first line where it has several):

FieldRequiredWhat it holds
taskyesAlways space. If it is missing or different, still answer this task and set "task": "space".
goalyesWhat the storage is and what the person is trying to do, in their words, or none. First sample: My MacBook says the disk is full; I finished editing my sister's wedding video last year.
targetyesHow much space the person wants to free. First sample: 60 GB.
totalsyesHow many files, how much space, and the space by kind of file. First sample: 15 files, 182 GB; by kind: video 86.3 GB (47%), disk image 66.7 GB (37%), other 17.3 GB (10%), archive 11.7 GB (6%), app or code 312 MB (0%), pdf 5.5 MB (0%).
foldersyesThe largest folders with their size and number of files. First sample: Movies - 86.3 GB, 4 files ...
itemsyesOne line per candidate: i1. copies: N files named X of S each, in folders | extra copies E (likely duplicates: the same size and the same name), or i2. file: path | size | kind. First sample: i1. copies: 2 files named family photos export.zip of 2.3 GB each, both in Downloads | extra copies 2.3 GB ...
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

How the page reads the listing (SizeKit.readListing, called by SizeKit.form): up to 20000 non-empty lines, each in one of these shapes. du (du -ah, du -k): a size, a tab, a path; a size with a unit (K, M, G, T) counts in powers of 1024, a bare number is a 1 KB block, and every path that has deeper paths listed under it is a folder total and is set aside. ls -l and ls -lR: the size column of each plain file (folders and links are skipped), with the folder taken from the folder: heading above it. find -printf "%s %p": bytes, a space, a path. A table (comma, tab or semicolon separated, such as a Google Drive storage list): a header row with a name column (name, file, file name, path, full path or title), a size column (any header holding size, bytes or quota) and, if there is one, a folder column (folder, directory, location or parent); sizes such as 4.2 GB or 310 MB count in powers of 1024. A path listed twice is read once; a line the page cannot read is counted and quoted in the warning. The page needs at least two files.

What the page works out from the files, free and before any run: the total; the space by kind of file from the extension (video, raw photo, image, audio, archive, disk image, pdf, document, spreadsheet, presentation, app or code and other; SizeKit.kinds); the space by folder, rolled up to every parent folder (SizeKit.folders; the input sends the ten largest below the top); and the likely duplicates (SizeKit.duplicates): files of exactly the same byte size whose names match, ignoring case, once copy marks - Copy of, (1), copy - are set aside; a group's extra copies are all but one. The candidates (SizeKit.items), ids i1, i2 ... in this order: the duplicate groups whose extra copies come to 1 MB or more (up to ten, the most extra space first), then the biggest single files not in a group that hold at least 0.5% of the total, up to 30 candidates in all. The target is read as a size (60 GB, 500M); when none is given, the page aims for a fifth of the total and says so in the input.

The model's rules

As the system prompt states them, in order:

  1. plan - one entry for each item, in id order: item (its id), action, and why (one sentence, at most 18 words, using only the item's own path, name, size and kind, and goal). Actions:
    • remove_copies - only for a copies item: keep one copy, remove the extra copies. It frees the item's extra copies.
    • archive - only for a file item whose path or name marks it as finished or past (an old year, raw footage after an edit, an export, a backup, an installer, an old version), or that goal says can leave the drive. It frees the file's full size, and the file is kept elsewhere.
    • ask - for a file whose path or name suggests it may be the only copy of something personal or still in use, or when you cannot tell; add question: one sentence, at most 20 words, asking the person what they need to know to decide.
    • keep - for a file the path or goal shows is in active use.
    Give question as "" for every action but ask.
  2. frees - the total the remove_copies and archive entries free, written like the sizes in the input (e.g. 12.4 GB), adding the sizes exactly as given.
  3. verdict - reaches when frees is at least target; short otherwise.
  4. note - one sentence, at most 30 words: what the plan frees against the target and, for short, which answer to an ask would close the gap.

And the house rules:

The reply

One JSON object, in the shape the system prompt asks for:

{
  "task": "space",
  "verdict": "reaches | short",
  "plan": [ { "item": "i1", "action": "remove_copies | archive | ask | keep", "why": "one sentence", "question": "one sentence or empty" } ],
  "frees": "the total, like 12.4 GB",
  "note": "one sentence"
}

The page reads the reply with ReplyKit.normalize (the verdict, the item ids and the actions matched in any case) and keeps what it trusts with ReplyKit.trusted(reply, built): an entry is set aside, with the reason, when it names an item the page did not send, an item that already has an action, an action the page does not know, removes the copies of a single file, or archives a group of copies instead of removing the extra ones; every sent candidate with no action is listed as missing, and the page keeps it. t.acts maps each id to its action, and t.freed is the page's own sum in bytes (SizeKit.freed): an archived file counts in full, a group whose extra copies are removed counts all but one copy, and asking or keeping frees nothing. The paths, the sizes and the copies always come from the page's own reading, never from the reply.

ReplyKit.check(reply, built) lists, under "The page's check": a missing verdict, or one that the page's own sum against the target contradicts; a total the page cannot read (it gives its own), or one more than a tenth (plus 1 MB) away from the page's sum, or a smaller gap the reply has rounded; every entry set aside, and every candidate with no action; an item asked about without a question, or a question on any other action (the page leaves it out); a reason or question that talks about deleting, erasing, wiping or trashing a file on any action but removing extra copies; a number in a sentence that is not in the input or a total; a sentence that names an item id or an internal field or uses backticks; and words such as "again", "already", "always", "never" or "every" that the input never used. Any warning makes the status flagged; otherwise it is kept. The verdict on the page reads "The plan frees enough" or "The plan falls short".

The exports: SizeKit.toCSV(form, t.acts, built.items) writes one row per file, biggest first - the path, the bytes, the size, the kind, the candidate id, its action and, in a duplicate group, whether the row is the kept copy or an extra copy - with CRLF line ends; ReplyKit.md and ReplyKit.json write the totals, the space by kind and every candidate with its action, reason and question; and the treemap saves as PNG or SVG.

Worked examples - every sample

These are the free samples on the page, one per listing shape: the listing as pasted, how the page reads it - the files, the total, the space by kind and by folder - the candidates it picks, the exact input it sends, the reply that replays for free, and what the page does with it - the plan it trusts, candidate by candidate, its own sum of what the plan frees against the target, the verdict against that sum and its own check. Together they cover the verdicts reaches and short.

A full laptop (du -ah)

The form: the storage is My MacBook says the disk is full; I finished editing my sister's wedding video last year; the space to free: 60 GB. The listing as pasted (the gaps in du lines are tabs):

48G	./Movies/2023 Wedding/wedding_raw_footage_cam_a.mov
31G	./Movies/2023 Wedding/wedding_raw_footage_cam_b.mov
6.2G	./Movies/2023 Wedding/wedding_final_edit.mp4
85G	./Movies/2023 Wedding
1.1G	./Movies/screen recording 2024-05-02.mov
86G	./Movies
62G	./Virtual Machines/Windows 11.vmdk
62G	./Virtual Machines
7.1G	./Downloads/Xcode_15.2.xip
4.7G	./Downloads/ubuntu-22.04.4-desktop-amd64.iso
312M	./Downloads/zoom_installer.pkg
2.3G	./Downloads/family photos export.zip
2.3G	./Downloads/family photos export (1).zip
17G	./Downloads
3.4M	./Pictures/Scans/passport.pdf
540M	./Pictures/2024 Lisbon
544M	./Pictures
2.1M	./Documents/thesis_final.pdf
12G	./Documents/Lightroom Catalog Previews.lrdata
12G	./Documents
4.8G	./Music/Library
4.8G	./Music
182G	.

The page reads 23 lines as du lines (size, tab, path): 15 files totalling 182 GB, with 8 folder totals set aside. The space to free is 60 GB.

The page warns: "du rounds its sizes, so totals are close rather than exact."

By kind (SizeKit.kinds):

KindSizeFilesShare
video86.3 GB447%
disk image66.7 GB237%
other17.3 GB310%
archive11.7 GB36%
app or code312 MB10%
pdf5.5 MB20%

The largest folders below the top, rolled up (SizeKit.folders, as the input sends them):

FolderSizeFiles
Movies86.3 GB4
Movies/2023 Wedding85.2 GB3
Virtual Machines62 GB1
Downloads16.7 GB5
Documents12 GB2
Music4.8 GB1
Pictures543 MB2
Pictures/Scans3.4 MB1

The candidates (SizeKit.items; for a duplicate group the size is its extra copies), each as its line in the input (SizeKit.itemLine):

IdTypeSizeLine
i1likely duplicates2.3 GBi1. copies: 2 files named family photos export.zip of 2.3 GB each, both in Downloads | extra copies 2.3 GB
i2single file62 GBi2. file: Virtual Machines/Windows 11.vmdk | 62 GB | disk image
i3single file48 GBi3. file: Movies/2023 Wedding/wedding_raw_footage_cam_a.mov | 48 GB | video
i4single file31 GBi4. file: Movies/2023 Wedding/wedding_raw_footage_cam_b.mov | 31 GB | video
i5single file12 GBi5. file: Documents/Lightroom Catalog Previews.lrdata | 12 GB | other
i6single file7.1 GBi6. file: Downloads/Xcode_15.2.xip | 7.1 GB | archive
i7single file6.2 GBi7. file: Movies/2023 Wedding/wedding_final_edit.mp4 | 6.2 GB | video
i8single file4.8 GBi8. file: Music/Library | 4.8 GB | other
i9single file4.7 GBi9. file: Downloads/ubuntu-22.04.4-desktop-amd64.iso | 4.7 GB | disk image
i10single file1.1 GBi10. file: Movies/screen recording 2024-05-02.mov | 1.1 GB | video

Input:

{
 "task": "space",
 "goal": "My MacBook says the disk is full; I finished editing my sister's wedding video last year",
 "target": "60 GB",
 "totals": "15 files, 182 GB; by kind: video 86.3 GB (47%), disk image 66.7 GB (37%), other 17.3 GB (10%), archive 11.7 GB (6%), app or code 312 MB (0%), pdf 5.5 MB (0%)",
 "folders": "Movies - 86.3 GB, 4 files\nMovies/2023 Wedding - 85.2 GB, 3 files\nVirtual Machines - 62 GB, 1 file\nDownloads - 16.7 GB, 5 files\nDocuments - 12 GB, 2 files\nMusic - 4.8 GB, 1 file\nPictures - 543 MB, 2 files\nPictures/Scans - 3.4 MB, 1 file",
 "items": "i1. copies: 2 files named family photos export.zip of 2.3 GB each, both in Downloads | extra copies 2.3 GB\ni2. file: Virtual Machines/Windows 11.vmdk | 62 GB | disk image\ni3. file: Movies/2023 Wedding/wedding_raw_footage_cam_a.mov | 48 GB | video\ni4. file: Movies/2023 Wedding/wedding_raw_footage_cam_b.mov | 31 GB | video\ni5. file: Documents/Lightroom Catalog Previews.lrdata | 12 GB | other\ni6. file: Downloads/Xcode_15.2.xip | 7.1 GB | archive\ni7. file: Movies/2023 Wedding/wedding_final_edit.mp4 | 6.2 GB | video\ni8. file: Music/Library | 4.8 GB | other\ni9. file: Downloads/ubuntu-22.04.4-desktop-amd64.iso | 4.7 GB | disk image\ni10. file: Movies/screen recording 2024-05-02.mov | 1.1 GB | video"
}

Reply:

{
 "task": "space",
 "verdict": "reaches",
 "plan": [
  {
   "item": "i1",
   "action": "remove_copies",
   "why": "family photos export.zip has 2 copies at 2.3 GB each, so the extra copy can be removed.",
   "question": ""
  },
  {
   "item": "i2",
   "action": "ask",
   "why": "Windows 11.vmdk is a 62 GB disk image, and whether it is still needed is unclear.",
   "question": "Is the Windows 11 virtual machine still in use, or can it be archived?"
  },
  {
   "item": "i3",
   "action": "archive",
   "why": "wedding_raw_footage_cam_a.mov is 48 GB of raw footage left over after the wedding edit was finished.",
   "question": ""
  },
  {
   "item": "i4",
   "action": "archive",
   "why": "wedding_raw_footage_cam_b.mov is 31 GB of raw footage left over after the wedding edit was finished.",
   "question": ""
  },
  {
   "item": "i5",
   "action": "ask",
   "why": "Lightroom Catalog Previews.lrdata is a 12 GB other file and whether it is still needed is unclear.",
   "question": "Is Lightroom Catalog Previews.lrdata still needed, or can it be archived?"
  },
  {
   "item": "i6",
   "action": "archive",
   "why": "Xcode_15.2.xip is a 7.1 GB installer, so it can move off the drive once used.",
   "question": ""
  },
  {
   "item": "i7",
   "action": "keep",
   "why": "wedding_final_edit.mp4 is the finished 6.2 GB video from editing the wedding, so it should stay.",
   "question": ""
  },
  {
   "item": "i8",
   "action": "keep",
   "why": "Music/Library is a 4.8 GB file in the Music folder, so it can stay.",
   "question": ""
  },
  {
   "item": "i9",
   "action": "archive",
   "why": "ubuntu-22.04.4-desktop-amd64.iso is a 4.7 GB installer image that can move off the drive once used.",
   "question": ""
  },
  {
   "item": "i10",
   "action": "ask",
   "why": "screen recording 2024-05-02.mov is a 1.1 GB video and whether it is still needed is unclear.",
   "question": "Is the screen recording from 2024-05-02 still needed, or can it be archived?"
  }
 ],
 "frees": "93.1 GB",
 "note": "The plan frees 93.1 GB, more than the 60 GB the person wants to free up."
}

The plan the page trusts (ReplyKit.trusted), candidate by candidate:

CandidateSizeActionFreesWhy / question
the copies of family photos export.zip (i1)2.3 GBremove_copies (remove extra copies)2.3 GBfamily photos export.zip has 2 copies at 2.3 GB each, so the extra copy can be removed.
Virtual Machines/Windows 11.vmdk (i2)62 GBask (ask first)-Windows 11.vmdk is a 62 GB disk image, and whether it is still needed is unclear. Ask: "Is the Windows 11 virtual machine still in use, or can it be archived?"
Movies/2023 Wedding/wedding_raw_footage_cam_a.mov (i3)48 GBarchive48 GBwedding_raw_footage_cam_a.mov is 48 GB of raw footage left over after the wedding edit was finished.
Movies/2023 Wedding/wedding_raw_footage_cam_b.mov (i4)31 GBarchive31 GBwedding_raw_footage_cam_b.mov is 31 GB of raw footage left over after the wedding edit was finished.
Documents/Lightroom Catalog Previews.lrdata (i5)12 GBask (ask first)-Lightroom Catalog Previews.lrdata is a 12 GB other file and whether it is still needed is unclear. Ask: "Is Lightroom Catalog Previews.lrdata still needed, or can it be archived?"
Downloads/Xcode_15.2.xip (i6)7.1 GBarchive7.1 GBXcode_15.2.xip is a 7.1 GB installer, so it can move off the drive once used.
Movies/2023 Wedding/wedding_final_edit.mp4 (i7)6.2 GBkeep-wedding_final_edit.mp4 is the finished 6.2 GB video from editing the wedding, so it should stay.
Music/Library (i8)4.8 GBkeep-Music/Library is a 4.8 GB file in the Music folder, so it can stay.
Downloads/ubuntu-22.04.4-desktop-amd64.iso (i9)4.7 GBarchive4.7 GBubuntu-22.04.4-desktop-amd64.iso is a 4.7 GB installer image that can move off the drive once used.
Movies/screen recording 2024-05-02.mov (i10)1.1 GBask (ask first)-screen recording 2024-05-02.mov is a 1.1 GB video and whether it is still needed is unclear. Ask: "Is the screen recording from 2024-05-02 still needed, or can it be archived?"

The page's own sum (t.freed): archived files 90.8 GB, extra copies removed 2.3 GB, in all 93.1 GB against a target of 60 GB. The reply says it frees 93.1 GB.

The verdict: reaches - "The plan frees enough"; it agrees with the page's sum, which reaches the target.

The note: "The plan frees 93.1 GB, more than the 60 GB the person wants to free up."

The page's check (ReplyKit.check): kept.

No notes.

A team's shared drive (CSV export)

The form: the storage is Our team's shared drive is at its 30 GB quota; we need about 20 GB free for this quarter's recordings; the space to free: 20 GB. The listing as pasted:

Name,Folder,Owner,File size
All-hands 2023-03 recording.mp4,Recordings,Priya,4.2 GB
All-hands 2023-06 recording.mp4,Recordings,Priya,3.9 GB
Customer interviews raw audio.zip,Research/2024,Tom,5.6 GB
Q3 board deck.pptx,Board,Ana,310 MB
Copy of Q3 board deck.pptx,Board/Old drafts,Ana,310 MB
Q3 board deck.pptx,Shared with me,Ana,310 MB
Brand video master.mov,Marketing/Launch,Leo,6.8 GB
Brand video web.mp4,Marketing/Launch,Leo,420 MB
Product photos 2022.zip,Marketing/Archive,Leo,2.4 GB
Budget 2025.xlsx,Finance,Ana,2 MB
Team handbook.pdf,People,Priya,18 MB
Onboarding course recording.mp4,People,Priya,2.9 GB

The page reads 13 lines as a table with a name and a size column: 12 files totalling 27.1 GB. The space to free is 20 GB.

By kind (SizeKit.kinds):

KindSizeFilesShare
video18.2 GB567%
archive8 GB229%
presentation930 MB33%
pdf18 MB10%
spreadsheet2 MB10%

The largest folders below the top, rolled up (SizeKit.folders, as the input sends them):

FolderSizeFiles
Marketing9.61 GB3
Recordings8.1 GB2
Marketing/Launch7.21 GB2
Research5.6 GB1
Research/20245.6 GB1
People2.92 GB2
Marketing/Archive2.4 GB1
Board620 MB2
Board/Old drafts310 MB1
Shared with me310 MB1

The candidates (SizeKit.items; for a duplicate group the size is its extra copies), each as its line in the input (SizeKit.itemLine):

IdTypeSizeLine
i1likely duplicates620 MBi1. copies: 3 files named Q3 board deck.pptx of 310 MB each, in Board, Board/Old drafts, Shared with me | extra copies 620 MB
i2single file6.8 GBi2. file: Marketing/Launch/Brand video master.mov | 6.8 GB | video
i3single file5.6 GBi3. file: Research/2024/Customer interviews raw audio.zip | 5.6 GB | archive
i4single file4.2 GBi4. file: Recordings/All-hands 2023-03 recording.mp4 | 4.2 GB | video
i5single file3.9 GBi5. file: Recordings/All-hands 2023-06 recording.mp4 | 3.9 GB | video
i6single file2.9 GBi6. file: People/Onboarding course recording.mp4 | 2.9 GB | video
i7single file2.4 GBi7. file: Marketing/Archive/Product photos 2022.zip | 2.4 GB | archive
i8single file420 MBi8. file: Marketing/Launch/Brand video web.mp4 | 420 MB | video

Input:

{
 "task": "space",
 "goal": "Our team's shared drive is at its 30 GB quota; we need about 20 GB free for this quarter's recordings",
 "target": "20 GB",
 "totals": "12 files, 27.1 GB; by kind: video 18.2 GB (67%), archive 8 GB (29%), presentation 930 MB (3%), pdf 18 MB (0%), spreadsheet 2 MB (0%)",
 "folders": "Marketing - 9.61 GB, 3 files\nRecordings - 8.1 GB, 2 files\nMarketing/Launch - 7.21 GB, 2 files\nResearch - 5.6 GB, 1 file\nResearch/2024 - 5.6 GB, 1 file\nPeople - 2.92 GB, 2 files\nMarketing/Archive - 2.4 GB, 1 file\nBoard - 620 MB, 2 files\nBoard/Old drafts - 310 MB, 1 file\nShared with me - 310 MB, 1 file",
 "items": "i1. copies: 3 files named Q3 board deck.pptx of 310 MB each, in Board, Board/Old drafts, Shared with me | extra copies 620 MB\ni2. file: Marketing/Launch/Brand video master.mov | 6.8 GB | video\ni3. file: Research/2024/Customer interviews raw audio.zip | 5.6 GB | archive\ni4. file: Recordings/All-hands 2023-03 recording.mp4 | 4.2 GB | video\ni5. file: Recordings/All-hands 2023-06 recording.mp4 | 3.9 GB | video\ni6. file: People/Onboarding course recording.mp4 | 2.9 GB | video\ni7. file: Marketing/Archive/Product photos 2022.zip | 2.4 GB | archive\ni8. file: Marketing/Launch/Brand video web.mp4 | 420 MB | video"
}

Reply:

{
 "task": "space",
 "verdict": "short",
 "plan": [
  {
   "item": "i1",
   "action": "remove_copies",
   "why": "Q3 board deck.pptx has 3 copies of 310 MB each; keeping one frees the 620 MB extra copies.",
   "question": ""
  },
  {
   "item": "i2",
   "action": "ask",
   "why": "Brand video master.mov is 6.8 GB of video in the Marketing Launch folder and may still be needed.",
   "question": "Do you still need Brand video master.mov for future edits, or can it be archived now?"
  },
  {
   "item": "i3",
   "action": "archive",
   "why": "Customer interviews raw audio.zip is 5.6 GB of raw archive data from the 2024 folder.",
   "question": ""
  },
  {
   "item": "i4",
   "action": "archive",
   "why": "All-hands 2023-03 recording.mp4 is 4.2 GB of video from a past meeting.",
   "question": ""
  },
  {
   "item": "i5",
   "action": "archive",
   "why": "All-hands 2023-06 recording.mp4 is 3.9 GB of video from a past meeting.",
   "question": ""
  },
  {
   "item": "i6",
   "action": "keep",
   "why": "Onboarding course recording.mp4 is 2.9 GB of video still used for onboarding.",
   "question": ""
  },
  {
   "item": "i7",
   "action": "archive",
   "why": "Product photos 2022.zip is 2.4 GB of archive data in the Marketing Archive folder.",
   "question": ""
  },
  {
   "item": "i8",
   "action": "keep",
   "why": "Brand video web.mp4 is a 420 MB web version still in active use.",
   "question": ""
  }
 ],
 "frees": "16.72 GB",
 "note": "This plan frees 16.72 GB of the 20 GB needed; confirming Brand video master.mov can be archived would close the remaining gap."
}

The plan the page trusts (ReplyKit.trusted), candidate by candidate:

CandidateSizeActionFreesWhy / question
the copies of Q3 board deck.pptx (i1)620 MBremove_copies (remove extra copies)620 MBQ3 board deck.pptx has 3 copies of 310 MB each; keeping one frees the 620 MB extra copies.
Marketing/Launch/Brand video master.mov (i2)6.8 GBask (ask first)-Brand video master.mov is 6.8 GB of video in the Marketing Launch folder and may still be needed. Ask: "Do you still need Brand video master.mov for future edits, or can it be archived now?"
Research/2024/Customer interviews raw audio.zip (i3)5.6 GBarchive5.6 GBCustomer interviews raw audio.zip is 5.6 GB of raw archive data from the 2024 folder.
Recordings/All-hands 2023-03 recording.mp4 (i4)4.2 GBarchive4.2 GBAll-hands 2023-03 recording.mp4 is 4.2 GB of video from a past meeting.
Recordings/All-hands 2023-06 recording.mp4 (i5)3.9 GBarchive3.9 GBAll-hands 2023-06 recording.mp4 is 3.9 GB of video from a past meeting.
People/Onboarding course recording.mp4 (i6)2.9 GBkeep-Onboarding course recording.mp4 is 2.9 GB of video still used for onboarding.
Marketing/Archive/Product photos 2022.zip (i7)2.4 GBarchive2.4 GBProduct photos 2022.zip is 2.4 GB of archive data in the Marketing Archive folder.
Marketing/Launch/Brand video web.mp4 (i8)420 MBkeep-Brand video web.mp4 is a 420 MB web version still in active use.

The page's own sum (t.freed): archived files 16.1 GB, extra copies removed 620 MB, in all 16.7 GB against a target of 20 GB. The reply says it frees 16.72 GB, read as 16.7 GB.

The verdict: short - "The plan falls short"; it agrees with the page's sum, which falls short of the target.

The note: "This plan frees 16.72 GB of the 20 GB needed; confirming Brand video master.mov can be archived would close the remaining gap."

The page's check (ReplyKit.check): kept.

No notes.

A home server (find)

The form: the storage is Home server for family photos and backups; the 2 TB data disk is 90% full; the space to free: 300 GB. The listing as pasted:

412316860416 ./backups/laptop-2022-01-full.tar.gz
398458880000 ./backups/laptop-2023-01-full.tar.gz
405874278400 ./backups/laptop-2024-01-full.tar.gz
86973087744 ./backups/phone-2024-08.tar.gz
53687091200 ./media/movies/home videos 1998-2005.mkv
214748364800 ./media/photos/library.photoslibrary.tar
21474836480 ./media/photos/2019/raw/DSC_0001-1200.zip
21474836480 ./media/photos/2019/raw-copy/DSC_0001-1200.zip
10737418240 ./downloads/debian-12-dvd.iso
4294967296 ./downloads/old-vm.qcow2
1073741824 ./docker/volumes/nextcloud.db

The page reads 11 lines as find -printf lines (bytes, path): 11 files totalling 1.48 TB. The space to free is 300 GB.

By kind (SizeKit.kinds):

KindSizeFilesShare
archive1.42 TB796%
video50 GB13%
disk image14 GB21%
app or code1 GB10%

The largest folders below the top, rolled up (SizeKit.folders, as the input sends them):

FolderSizeFiles
backups1.19 TB4
media290 GB4
media/photos240 GB3
media/movies50 GB1
media/photos/201940 GB2
media/photos/2019/raw20 GB1
media/photos/2019/raw-copy20 GB1
downloads14 GB2
docker1 GB1
docker/volumes1 GB1

The candidates (SizeKit.items; for a duplicate group the size is its extra copies), each as its line in the input (SizeKit.itemLine):

IdTypeSizeLine
i1likely duplicates20 GBi1. copies: 2 files named DSC_0001-1200.zip of 20 GB each, in media/photos/2019/raw, media/photos/2019/raw-copy | extra copies 20 GB
i2single file384 GBi2. file: backups/laptop-2022-01-full.tar.gz | 384 GB | archive
i3single file378 GBi3. file: backups/laptop-2024-01-full.tar.gz | 378 GB | archive
i4single file371 GBi4. file: backups/laptop-2023-01-full.tar.gz | 371 GB | archive
i5single file200 GBi5. file: media/photos/library.photoslibrary.tar | 200 GB | archive
i6single file81 GBi6. file: backups/phone-2024-08.tar.gz | 81 GB | archive
i7single file50 GBi7. file: media/movies/home videos 1998-2005.mkv | 50 GB | video
i8single file10 GBi8. file: downloads/debian-12-dvd.iso | 10 GB | disk image

Input:

{
 "task": "space",
 "goal": "Home server for family photos and backups; the 2 TB data disk is 90% full",
 "target": "300 GB",
 "totals": "11 files, 1.48 TB; by kind: archive 1.42 TB (96%), video 50 GB (3%), disk image 14 GB (1%), app or code 1 GB (0%)",
 "folders": "backups - 1.19 TB, 4 files\nmedia - 290 GB, 4 files\nmedia/photos - 240 GB, 3 files\nmedia/movies - 50 GB, 1 file\nmedia/photos/2019 - 40 GB, 2 files\nmedia/photos/2019/raw - 20 GB, 1 file\nmedia/photos/2019/raw-copy - 20 GB, 1 file\ndownloads - 14 GB, 2 files\ndocker - 1 GB, 1 file\ndocker/volumes - 1 GB, 1 file",
 "items": "i1. copies: 2 files named DSC_0001-1200.zip of 20 GB each, in media/photos/2019/raw, media/photos/2019/raw-copy | extra copies 20 GB\ni2. file: backups/laptop-2022-01-full.tar.gz | 384 GB | archive\ni3. file: backups/laptop-2024-01-full.tar.gz | 378 GB | archive\ni4. file: backups/laptop-2023-01-full.tar.gz | 371 GB | archive\ni5. file: media/photos/library.photoslibrary.tar | 200 GB | archive\ni6. file: backups/phone-2024-08.tar.gz | 81 GB | archive\ni7. file: media/movies/home videos 1998-2005.mkv | 50 GB | video\ni8. file: downloads/debian-12-dvd.iso | 10 GB | disk image"
}

Reply:

{
 "task": "space",
 "verdict": "reaches",
 "plan": [
  {
   "item": "i1",
   "action": "remove_copies",
   "why": "DSC_0001-1200.zip is duplicated in raw and raw-copy at 20 GB each, so the extra copy can be removed.",
   "question": ""
  },
  {
   "item": "i2",
   "action": "archive",
   "why": "laptop-2022-01-full.tar.gz is a 384 GB backup, so it can move off the drive.",
   "question": ""
  },
  {
   "item": "i3",
   "action": "archive",
   "why": "laptop-2024-01-full.tar.gz is a 378 GB backup, so it can move off the drive.",
   "question": ""
  },
  {
   "item": "i4",
   "action": "archive",
   "why": "laptop-2023-01-full.tar.gz is a 371 GB backup, so it can move off the drive.",
   "question": ""
  },
  {
   "item": "i5",
   "action": "ask",
   "why": "library.photoslibrary.tar is 200 GB under media/photos, and its current use isn't clear.",
   "question": "Is library.photoslibrary.tar still your live photo library, or can it move to other storage?"
  },
  {
   "item": "i6",
   "action": "archive",
   "why": "phone-2024-08.tar.gz is an 81 GB backup, so it can move off the drive.",
   "question": ""
  },
  {
   "item": "i7",
   "action": "archive",
   "why": "Home videos 1998-2005.mkv covers years long past, so it can move off the drive at 50 GB.",
   "question": ""
  },
  {
   "item": "i8",
   "action": "archive",
   "why": "debian-12-dvd.iso is a 10 GB installer image, so it can move off the drive.",
   "question": ""
  }
 ],
 "frees": "1294 GB",
 "note": "The plan frees 1294 GB, well past the 300 GB wanted to clear space on the family server."
}

The plan the page trusts (ReplyKit.trusted), candidate by candidate:

CandidateSizeActionFreesWhy / question
the copies of DSC_0001-1200.zip (i1)20 GBremove_copies (remove extra copies)20 GBDSC_0001-1200.zip is duplicated in raw and raw-copy at 20 GB each, so the extra copy can be removed.
backups/laptop-2022-01-full.tar.gz (i2)384 GBarchive384 GBlaptop-2022-01-full.tar.gz is a 384 GB backup, so it can move off the drive.
backups/laptop-2024-01-full.tar.gz (i3)378 GBarchive378 GBlaptop-2024-01-full.tar.gz is a 378 GB backup, so it can move off the drive.
backups/laptop-2023-01-full.tar.gz (i4)371 GBarchive371 GBlaptop-2023-01-full.tar.gz is a 371 GB backup, so it can move off the drive.
media/photos/library.photoslibrary.tar (i5)200 GBask (ask first)-library.photoslibrary.tar is 200 GB under media/photos, and its current use isn't clear. Ask: "Is library.photoslibrary.tar still your live photo library, or can it move to other storage?"
backups/phone-2024-08.tar.gz (i6)81 GBarchive81 GBphone-2024-08.tar.gz is an 81 GB backup, so it can move off the drive.
media/movies/home videos 1998-2005.mkv (i7)50 GBarchive50 GBHome videos 1998-2005.mkv covers years long past, so it can move off the drive at 50 GB.
downloads/debian-12-dvd.iso (i8)10 GBarchive10 GBdebian-12-dvd.iso is a 10 GB installer image, so it can move off the drive.

The page's own sum (t.freed): archived files 1.24 TB, extra copies removed 20 GB, in all 1.26 TB against a target of 300 GB. The reply says it frees 1294 GB, read as 1.26 TB.

The verdict: reaches - "The plan frees enough"; it agrees with the page's sum, which reaches the target.

The note: "The plan frees 1294 GB, well past the 300 GB wanted to clear space on the family server."

The page's check (ReplyKit.check): kept.

No notes.

The CSV for the first sample

What SizeKit.toCSV(form, t.acts, built.items) writes for "A full laptop (du -ah)" above: one row per file, biggest first, with the candidate it belongs to, the trusted action and, in a duplicate group, which row is the kept copy and which the extra copies (lines end in CRLF). A file that is not a candidate has no plan.

path,bytes,size,kind,item,plan,copy
Virtual Machines/Windows 11.vmdk,66571993088,62 GB,disk image,i2,ask,
Movies/2023 Wedding/wedding_raw_footage_cam_a.mov,51539607552,48 GB,video,i3,archive,
Movies/2023 Wedding/wedding_raw_footage_cam_b.mov,33285996544,31 GB,video,i4,archive,
Documents/Lightroom Catalog Previews.lrdata,12884901888,12 GB,other,i5,ask,
Downloads/Xcode_15.2.xip,7623566950,7.1 GB,archive,i6,archive,
Movies/2023 Wedding/wedding_final_edit.mp4,6657199309,6.2 GB,video,i7,keep,
Music/Library,5153960755,4.8 GB,other,i8,keep,
Downloads/ubuntu-22.04.4-desktop-amd64.iso,5046586573,4.7 GB,disk image,i9,archive,
Downloads/family photos export.zip,2469606195,2.3 GB,archive,i1,remove_copies,kept copy
Downloads/family photos export (1).zip,2469606195,2.3 GB,archive,i1,remove_copies,extra copy
Movies/screen recording 2024-05-02.mov,1181116006,1.1 GB,video,i10,ask,
Pictures/2024 Lisbon,566231040,540 MB,other,,,
Downloads/zoom_installer.pkg,327155712,312 MB,app or code,,,
Pictures/Scans/passport.pdf,3565158,3.4 MB,pdf,,,
Documents/thesis_final.pdf,2202010,2.1 MB,pdf,,,

Build the input yourself

Load the engine in node (global.window = global; require("./sizekit.js"); require("./replykit.js")), then:

const K = SizeKit, R = ReplyKit;
const form = K.form({ goal: "My laptop is nearly full", target: "3 GB",
  listing: "4.2G\t./Videos/holiday_raw.mov\n1.1G\t./Downloads/setup.dmg\n1.1G\t./Downloads/setup (1).dmg\n300M\t./Documents/taxes.zip" });
// form.files: every file as read [{path, dir, name, ext, kind, size}]; form.total and form.target in bytes; form.format
K.kinds(form); K.folders(form); K.duplicates(form)   // the totals and the likely duplicates, no run needed
const built = K.buildInput(form);     // built.ok is false, with built.note, until two files are read
// built.items: the candidates [{id, type: copies or file, size, files}]; built.warn: what the page warns about
const wire = K.toWire(built.input);   // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// const t = R.trusted(reply, built)       // {acts: {i1: action}, why, ask, dropped, missing, freed}
// R.check(reply, built)                    // {status: kept or flagged, notes}
// K.toCSV(form, t.acts, built.items)       // every file with its size, kind and plan

Form keys: listing (the pasted listing, any shape above), goal (what the storage is and what the person is trying to do, or empty) and target (the space to free, such as 60 GB or 500M; empty means a fifth of the total). K.freed(t.acts, built.items) gives the same sum for a plan you edit yourself.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://space-hog.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"space-hog"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "space", "goal": "My laptop is nearly full", "target": "3 GB", "totals": "4 files, 6.69 GB; by kind: video 4.2 GB (63%), disk image 2.2 GB (33%), archive 300 MB (4%)", "folders": "Videos - 4.2 GB, 1 file\nDownloads - 2.2 GB, 2 files\nDocuments - 300 MB, 1 file", "items": "i1. copies: 2 files named setup.dmg of 1.1 GB each, both in Downloads | extra copies 1.1 GB\ni2. file: Videos/holiday_raw.mov | 4.2 GB | video\ni3. file: Documents/taxes.zip | 300 MB | archive"}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses space-hog:<task>:<inputHash>:a<attempt> (the one task is space), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="space-hog:space:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r '.verdict, .frees, (.plan[] | "\(.item) \(.action)")' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: space-hog:space:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json