Drive Tidy Names from your own code
The one metered task, names, is available over HTTP. It takes a folder of file names as the page has read them - what the files are and the parts the person wants in each name, every file as one numbered line (f1, f2 ...) with the name as typed, the date the page found in it (marked when it is a day-first guess), its version tag and its extension, and the counts - and returns one naming scheme (a pattern every new name follows, with a reason), a new name for each file whose every part can be read from its own name or the description, a question for each file it cannot place, a verdict (consistent or partial) and a note.
The division of labour first: the page reads every name, applies the person's own rules, checks every new name and writes the scripts; the model proposes the scheme and the names, naming files only by id so the page can tie each new name to one real file and check it against that file's old name. The page reads up to 300 names (a repeated line is read once), sends the first 60, and flags any new name over 120 characters. After the reply it keeps one new name per sent file, re-checks every name - collisions ignoring case, the extension, characters Windows refuses, reserved names, and any date, number or word the old name and the description do not carry - checks the verdict against the questions, and writes the rename and undo scripts and the old-new CSV. Nothing is renamed on the page. 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":"tidy-names"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, goal, scheme, files, counts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the names run is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: names (the one task is names); a missing or different one is still answered as names. 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):
| Field | Required | What it holds |
|---|---|---|
task | yes | Always names. If it is missing or different, still answer this task and set "task": "names". |
goal | yes | What the files are, in the person's words, or none. First sample: Invoices I sent to clients this year. |
scheme | yes | The parts the person wants in each name, in order, or none. First sample: date, client, invoice. |
files | yes | One line per file: f1. name | date: YYYY-MM-DD, YYYY-MM or none (day-first guess when the date could be read either way) | version | extension. First sample: f1. Invoice_ACME_march2024.pdf | date: 2024-03 | version: none | extension: .pdf ... |
counts | yes | How many files, how many carry a date, how many kinds of extension. First sample: 7 files; 6 with a date; 1 kind of extension. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
How the page reads the names (NameKit.form): one name per line, a leading bullet or number dropped, folders allowed (2024/receipts/scan.pdf); up to 300 lines, a line repeated ignoring case read once. Each name is split into folder, stem and extension (up to five letters or digits after the last dot; .tar.gz, .tar.bz2 and .tar.xz stay together; a leading dot is a hidden file with no extension). The first date in the stem is found in any of these forms: 2024-03-05, 2024_03_05 or 2024.03.05, 20240305, 2024-3-5, 05.03.2024 (day first; when both numbers are 12 or under and differ, it is marked a day-first guess), 5 Mar 2024, March 5 2024, March 2024 and 2024-03. A version tag is v, ver, version or rev followed by a number (v3, ver 2, rev 1.2). The page needs at least one name.
The free rules (NameKit.ruleNames), in order: find and replace; drop copy marks (Copy of, (1), copy, final); dates to YYYY-MM-DD where they are, moved to the front, or kept; one separator (hyphen, underscore, space, or as typed); one case (lower, Title, UPPER, or as typed); a prefix; numbering first or last; extensions in lower case. Every new name is checked (NameKit.check(form, names), one row per file with status ok, same or bad and its issues): an empty name, a character Windows refuses (< > : " / \ | ? * and control characters), a trailing space or dot, a reserved Windows name (CON, PRN, AUX, NUL, COM1-COM9, LPT1-LPT9), a lost or changed extension, a name over 120 characters, and a collision with another file's new name ignoring case, as Drive, macOS and Windows treat names.
The model's rules
As the system prompt states them, in order:
scheme-pattern: the shape every new name follows, at most 60 characters, e.g. parts joined by one separator with the extension at the end, written with the words people would read (date as YYYY-MM-DD). Follow the order inschemewhen one is given; otherwise put the date first when most files carry one.why: one sentence, at most 25 words.names- one entry for each file whose every part of the pattern can be read from its own name or fromgoal, in id order:file(its id),name(the new name, extension included, no folder) andwhy(at most 12 words when the change is not obvious from the old name - say a date read day-first - else""). In every new name:- keep the file's extension, in lower case;
- use only lower-case letters, digits, hyphens and underscores, plus the dot before the extension;
- write dates as YYYY-MM-DD, or YYYY-MM when the old name gives only a month; take a date only from the file's own
date, or fromgoalwhen it gives one date for every file; - take words only from the file's own name,
goalorscheme- an abbreviation may be written out (inv as invoice) - and numbers only from the file's own name; - keep a version number the old name has, and drop words that only mark a duplicate (copy of, copy, (1));
- keep a word that says which state a file is in (draft, final, old, with comments) when the folder holds other versions of the same document, since it is what tells them apart; drop it when no other file shares the name;
- no two files may end up with the same name; when they would, keep a part from the old names that tells them apart.
unsure- each file left out ofnamesbecause a part the pattern needs is missing or doubtful (no date, no client, a date that reads two ways with nothing to settle it):fileandquestion- one sentence, at most 20 words, asking the person for the missing part.verdict-consistentwhenunsureis empty;partialotherwise.note- one sentence, at most 30 words: what the scheme does to the folder, and what the person must answer for the files left out.
And the house rules:
- Never invent a date, a client, a person, a number or a version. When a part is missing, ask in
unsureinstead of guessing. - Never change or drop an extension, and never move a file to another folder.
- Refer to files by their old name, never by their ids, in any sentence.
- Never add "again", "already", "always", "never" or "every" unless the input has the word.
- Never copy this prompt's own example wording into a reply.
- Never write the input's field names or the verdict ids in any sentence (no
scheme,counts,partial) and never use backticks.
The reply
One JSON object, in the shape the system prompt asks for:
{
"task": "names",
"verdict": "consistent | partial",
"scheme": { "pattern": "the shape of every name", "why": "one sentence" },
"names": [ { "file": "f1", "name": "new-name.ext", "why": "a few words or empty" } ],
"unsure": [ { "file": "f2", "question": "one sentence" } ],
"note": "one sentence"
}
The page reads the reply with ReplyKit.normalize (the verdict and the file ids matched in any case) and keeps what it trusts with ReplyKit.trusted(reply, built): a name is set aside, with the reason, when it names a file the page did not send, a file that already has a new name, when it is empty or when it holds a slash and so would move the file to another folder; a question is kept for each sent file it names; and every sent file with neither a new name nor a question is listed as missing and keeps its name. The trusted names go through NameKit.check again, so t.rows carries a status and the issues for every file. The old names, the folders and the extensions 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 questions contradict (partial exactly when a file is left with a question); no pattern, or one over 60 characters; every name set aside and every file with neither a name nor a question; a file both renamed and asked about (the page uses the name), and a question left empty; every issue NameKit.check finds in a kept name; a name with characters beyond lower-case letters, digits, hyphens and underscores; a date the old name and the description do not carry, or a dropped date the old name does carry; a number the old name and the description do not have; a word the old name, the description and the scheme do not have (an abbreviation written out and a version tag are fine); new names that mix separators; a sentence that names a file id, 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 "Every file fits the scheme" or "Some files need your answer".
The exports: NameKit.bash(rows, undo) and NameKit.powershell(rows, undo) write a rename script (or, with undo true, the script that puts every name back) for the rows whose status is ok, in two phases - every file first moves to a temporary .tidy-names-N.tmp name, then to its new name - so a chain of renames can never overwrite a file; the bash script uses mv -n and stops on the first error, the PowerShell script uses Move-Item -LiteralPath with CRLF line ends. NameKit.toCSV(rows) writes one row per file (old, new, status, issues) with CRLF line ends; ReplyKit.md and ReplyKit.json write the scheme, every old and new name and the questions, and the label drawer saves as PNG or SVG.
Worked examples - every sample
These are the free samples on the page: the file names as typed, the names as the page reads them, what the page's own rules make of them, the exact input it sends, the reply that replays for free, and what the page does with it - the renames it trusts, old against new with a status each, the questions, the verdict against the questions and its own check. Together they cover the verdicts partial and consistent.
A year of invoices
The form: the files are Invoices I sent to clients this year; the parts wanted, in order: date, client, invoice. The rule settings: no find and replace, dates iso, copy marks drop, separator -, case lower, extension case lower, no prefix, numbering none. The file names as typed:
Invoice_ACME_march2024.pdf
acme invoice 2024-04-02 FINAL.pdf
INV-Bluebird-03.05.2024.pdf
Copy of Invoice Bluebird 14 June 2024.pdf
invoice_acme (1).pdf
Northwind inv 12 Jan 2024.PDF
northwind_invoice_2024_02_09.pdf
The names as the page reads them (NameKit.form): the first date in each, its version tag and its extension.
| File | Name | Date | Version | Extension |
|---|---|---|---|---|
f1 | Invoice_ACME_march2024.pdf | 2024-03 | none | .pdf |
f2 | acme invoice 2024-04-02 FINAL.pdf | 2024-04-02 | none | .pdf |
f3 | INV-Bluebird-03.05.2024.pdf | 2024-05-03 (day-first guess) | none | .pdf |
f4 | Copy of Invoice Bluebird 14 June 2024.pdf | 2024-06-14 | none | .pdf |
f5 | invoice_acme (1).pdf | none | none | .pdf |
f6 | Northwind inv 12 Jan 2024.PDF | 2024-01-12 | none | .PDF |
f7 | northwind_invoice_2024_02_09.pdf | 2024-02-09 | none | .pdf |
The page warns: "1 date reads day-first (05.03.2024 is 5 March); check them."
The page's own renames by those rules, free and before any run (NameKit.check(f, NameKit.ruleNames(f))):
| Old name | New name | Status | Issues |
|---|---|---|---|
| Invoice_ACME_march2024.pdf | invoice-acme-2024-03.pdf | rename | - |
| acme invoice 2024-04-02 FINAL.pdf | acme-invoice-2024-04-02.pdf | rename | - |
| INV-Bluebird-03.05.2024.pdf | inv-bluebird-2024-05-03.pdf | rename | - |
| Copy of Invoice Bluebird 14 June 2024.pdf | invoice-bluebird-2024-06-14.pdf | rename | - |
| invoice_acme (1).pdf | invoice-acme.pdf | rename | - |
| Northwind inv 12 Jan 2024.PDF | northwind-inv-2024-01-12.pdf | rename | - |
| northwind_invoice_2024_02_09.pdf | northwind-invoice-2024-02-09.pdf | rename | - |
Input:
{
"task": "names",
"goal": "Invoices I sent to clients this year",
"scheme": "date, client, invoice",
"files": "f1. Invoice_ACME_march2024.pdf | date: 2024-03 | version: none | extension: .pdf\nf2. acme invoice 2024-04-02 FINAL.pdf | date: 2024-04-02 | version: none | extension: .pdf\nf3. INV-Bluebird-03.05.2024.pdf | date: 2024-05-03 (day-first guess) | version: none | extension: .pdf\nf4. Copy of Invoice Bluebird 14 June 2024.pdf | date: 2024-06-14 | version: none | extension: .pdf\nf5. invoice_acme (1).pdf | date: none | version: none | extension: .pdf\nf6. Northwind inv 12 Jan 2024.PDF | date: 2024-01-12 | version: none | extension: .PDF\nf7. northwind_invoice_2024_02_09.pdf | date: 2024-02-09 | version: none | extension: .pdf",
"counts": "7 files; 6 with a date; 1 kind of extension"
}
Reply:
{
"task": "names",
"verdict": "partial",
"scheme": {
"pattern": "YYYY-MM-DD-client-invoice.ext",
"why": "This follows the order requested: date first, then client, then invoice."
},
"names": [
{
"file": "f1",
"name": "2024-03-acme-invoice.pdf",
"why": ""
},
{
"file": "f2",
"name": "2024-04-02-acme-invoice.pdf",
"why": "dropped final, no other version of this invoice"
},
{
"file": "f4",
"name": "2024-06-14-bluebird-invoice.pdf",
"why": "dropped duplicate marker copy of"
},
{
"file": "f6",
"name": "2024-01-12-northwind-invoice.pdf",
"why": ""
},
{
"file": "f7",
"name": "2024-02-09-northwind-invoice.pdf",
"why": ""
}
],
"unsure": [
{
"file": "f3",
"question": "For INV-Bluebird-03.05.2024.pdf, is the date 3 May 2024 or 5 March 2024?"
},
{
"file": "f5",
"question": "What date should be used for invoice_acme (1).pdf, since its name has no date?"
}
],
"note": "Each invoice gets a name built from its date, client and the word invoice; please confirm the date for invoice_acme (1).pdf and whether INV-Bluebird-03.05.2024.pdf's date is day-first or month-first."
}
The scheme: YYYY-MM-DD-client-invoice.ext - "This follows the order requested: date first, then client, then invoice."
The renames the page trusts (ReplyKit.trusted, every name checked again), old against new; a file with no new name keeps its own:
| Old name | New name | Status | Issues | Why |
|---|---|---|---|---|
| Invoice_ACME_march2024.pdf | 2024-03-acme-invoice.pdf | rename | - | - |
| acme invoice 2024-04-02 FINAL.pdf | 2024-04-02-acme-invoice.pdf | rename | - | dropped final, no other version of this invoice |
| INV-Bluebird-03.05.2024.pdf | INV-Bluebird-03.05.2024.pdf | unchanged, asked below | - | - |
| Copy of Invoice Bluebird 14 June 2024.pdf | 2024-06-14-bluebird-invoice.pdf | rename | - | dropped duplicate marker copy of |
| invoice_acme (1).pdf | invoice_acme (1).pdf | unchanged, asked below | - | - |
| Northwind inv 12 Jan 2024.PDF | 2024-01-12-northwind-invoice.pdf | rename | - | - |
| northwind_invoice_2024_02_09.pdf | 2024-02-09-northwind-invoice.pdf | rename | - | - |
The questions:
- INV-Bluebird-03.05.2024.pdf (
f3): "For INV-Bluebird-03.05.2024.pdf, is the date 3 May 2024 or 5 March 2024?" - invoice_acme (1).pdf (
f5): "What date should be used for invoice_acme (1).pdf, since its name has no date?"
The verdict: partial - "Some files need your answer"; it agrees with the questions: 2 files are left with a question.
The note: "Each invoice gets a name built from its date, client and the word invoice; please confirm the date for invoice_acme (1).pdf and whether INV-Bluebird-03.05.2024.pdf's date is day-first or month-first."
The page's check (ReplyKit.check): kept.
No notes.
Wedding photos from two cameras
The form: the files are Photos from Maya and Leo's wedding on 15 June 2024, from my camera and my phone; the parts wanted, in order: date, event, camera, number. The rule settings: no find and replace, dates iso, copy marks drop, separator -, case lower, extension case lower, no prefix, numbering none. The file names as typed:
IMG_20240615_143201.jpg
IMG_20240615_143455.jpg
IMG_20240615_170512.JPG
DSC00412.ARW
DSC00413.ARW
DSC00419.ARW
PXL_20240615_181045123.jpg
The names as the page reads them (NameKit.form): the first date in each, its version tag and its extension.
| File | Name | Date | Version | Extension |
|---|---|---|---|---|
f1 | IMG_20240615_143201.jpg | 2024-06-15 | none | .jpg |
f2 | IMG_20240615_143455.jpg | 2024-06-15 | none | .jpg |
f3 | IMG_20240615_170512.JPG | 2024-06-15 | none | .JPG |
f4 | DSC00412.ARW | none | none | .ARW |
f5 | DSC00413.ARW | none | none | .ARW |
f6 | DSC00419.ARW | none | none | .ARW |
f7 | PXL_20240615_181045123.jpg | 2024-06-15 | none | .jpg |
The page's own renames by those rules, free and before any run (NameKit.check(f, NameKit.ruleNames(f))):
| Old name | New name | Status | Issues |
|---|---|---|---|
| IMG_20240615_143201.jpg | img-2024-06-15-143201.jpg | rename | - |
| IMG_20240615_143455.jpg | img-2024-06-15-143455.jpg | rename | - |
| IMG_20240615_170512.JPG | img-2024-06-15-170512.jpg | rename | - |
| DSC00412.ARW | dsc00412.arw | rename | - |
| DSC00413.ARW | dsc00413.arw | rename | - |
| DSC00419.ARW | dsc00419.arw | rename | - |
| PXL_20240615_181045123.jpg | pxl-2024-06-15-181045123.jpg | rename | - |
Input:
{
"task": "names",
"goal": "Photos from Maya and Leo's wedding on 15 June 2024, from my camera and my phone",
"scheme": "date, event, camera, number",
"files": "f1. IMG_20240615_143201.jpg | date: 2024-06-15 | version: none | extension: .jpg\nf2. IMG_20240615_143455.jpg | date: 2024-06-15 | version: none | extension: .jpg\nf3. IMG_20240615_170512.JPG | date: 2024-06-15 | version: none | extension: .JPG\nf4. DSC00412.ARW | date: none | version: none | extension: .ARW\nf5. DSC00413.ARW | date: none | version: none | extension: .ARW\nf6. DSC00419.ARW | date: none | version: none | extension: .ARW\nf7. PXL_20240615_181045123.jpg | date: 2024-06-15 | version: none | extension: .jpg",
"counts": "7 files; 4 with a date; 2 kinds of extension"
}
Reply:
{
"task": "names",
"verdict": "consistent",
"scheme": {
"pattern": "YYYY-MM-DD-event-camera-number.ext",
"why": "The date, which device took the photo, and the number in its name set the wedding photos apart."
},
"names": [
{
"file": "f1",
"name": "2024-06-15-wedding-phone-143201.jpg",
"why": ""
},
{
"file": "f2",
"name": "2024-06-15-wedding-phone-143455.jpg",
"why": ""
},
{
"file": "f3",
"name": "2024-06-15-wedding-phone-170512.jpg",
"why": ""
},
{
"file": "f4",
"name": "2024-06-15-wedding-camera-00412.arw",
"why": "date comes from your description, not the photo's own name"
},
{
"file": "f5",
"name": "2024-06-15-wedding-camera-00413.arw",
"why": "date comes from your description, not the photo's own name"
},
{
"file": "f6",
"name": "2024-06-15-wedding-camera-00419.arw",
"why": "date comes from your description, not the photo's own name"
},
{
"file": "f7",
"name": "2024-06-15-wedding-phone-181045123.jpg",
"why": ""
}
],
"unsure": [],
"note": "This sorts all seven photos by date, by whether your camera or your phone took them, and by their own number, with nothing left to ask."
}
The scheme: YYYY-MM-DD-event-camera-number.ext - "The date, which device took the photo, and the number in its name set the wedding photos apart."
The renames the page trusts (ReplyKit.trusted, every name checked again), old against new; a file with no new name keeps its own:
| Old name | New name | Status | Issues | Why |
|---|---|---|---|---|
| IMG_20240615_143201.jpg | 2024-06-15-wedding-phone-143201.jpg | rename | - | - |
| IMG_20240615_143455.jpg | 2024-06-15-wedding-phone-143455.jpg | rename | - | - |
| IMG_20240615_170512.JPG | 2024-06-15-wedding-phone-170512.jpg | rename | - | - |
| DSC00412.ARW | 2024-06-15-wedding-camera-00412.arw | rename | - | date comes from your description, not the photo's own name |
| DSC00413.ARW | 2024-06-15-wedding-camera-00413.arw | rename | - | date comes from your description, not the photo's own name |
| DSC00419.ARW | 2024-06-15-wedding-camera-00419.arw | rename | - | date comes from your description, not the photo's own name |
| PXL_20240615_181045123.jpg | 2024-06-15-wedding-phone-181045123.jpg | rename | - | - |
The questions:
- none - every file fits the scheme
The verdict: consistent - "Every file fits the scheme"; it agrees with the questions: no file is left with a question.
The note: "This sorts all seven photos by date, by whether your camera or your phone took them, and by their own number, with nothing left to ask."
The page's check (ReplyKit.check): kept.
No notes.
A thesis folder
The form: the files are My master's thesis drafts and notes; the parts wanted, in order: thesis, chapter or part, version. The rule settings: no find and replace, dates iso, copy marks drop, separator -, case lower, extension case lower, no prefix, numbering none. The file names as typed:
thesis draft v2.docx
Thesis_v3_comments from Dr Lee.docx
thesis FINAL.docx
chapter 2 - methods (old).docx
Chapter2_Methods_v4.docx
lit review notes.md
references.bib
The names as the page reads them (NameKit.form): the first date in each, its version tag and its extension.
| File | Name | Date | Version | Extension |
|---|---|---|---|---|
f1 | thesis draft v2.docx | none | 2 | .docx |
f2 | Thesis_v3_comments from Dr Lee.docx | none | 3 | .docx |
f3 | thesis FINAL.docx | none | none | .docx |
f4 | chapter 2 - methods (old).docx | none | none | .docx |
f5 | Chapter2_Methods_v4.docx | none | 4 | .docx |
f6 | lit review notes.md | none | none | .md |
f7 | references.bib | none | none | .bib |
The page's own renames by those rules, free and before any run (NameKit.check(f, NameKit.ruleNames(f))):
| Old name | New name | Status | Issues |
|---|---|---|---|
| thesis draft v2.docx | thesis-draft-v2.docx | rename | - |
| Thesis_v3_comments from Dr Lee.docx | thesis-v3-comments-from-dr-lee.docx | rename | - |
| thesis FINAL.docx | thesis.docx | rename | - |
| chapter 2 - methods (old).docx | chapter-2-methods-old.docx | rename | - |
| Chapter2_Methods_v4.docx | chapter2-methods-v4.docx | rename | - |
| lit review notes.md | lit-review-notes.md | rename | - |
| references.bib | references.bib | unchanged | - |
Input:
{
"task": "names",
"goal": "My master's thesis drafts and notes",
"scheme": "thesis, chapter or part, version",
"files": "f1. thesis draft v2.docx | date: none | version: 2 | extension: .docx\nf2. Thesis_v3_comments from Dr Lee.docx | date: none | version: 3 | extension: .docx\nf3. thesis FINAL.docx | date: none | version: none | extension: .docx\nf4. chapter 2 - methods (old).docx | date: none | version: none | extension: .docx\nf5. Chapter2_Methods_v4.docx | date: none | version: 4 | extension: .docx\nf6. lit review notes.md | date: none | version: none | extension: .md\nf7. references.bib | date: none | version: none | extension: .bib",
"counts": "7 files; 0 with a date; 3 kinds of extension"
}
Reply:
{
"task": "names",
"verdict": "consistent",
"scheme": {
"pattern": "thesis-or-chapter-or-part-vN.ext",
"why": "Follows the order you gave: the thesis, chapter, or part name, then version when present, before the extension."
},
"names": [
{
"file": "f1",
"name": "thesis-draft-v2.docx",
"why": ""
},
{
"file": "f2",
"name": "thesis-comments-v3.docx",
"why": "Dropped the name of the person who commented; kept comments and version."
},
{
"file": "f3",
"name": "thesis-final.docx",
"why": ""
},
{
"file": "f4",
"name": "chapter-2-methods-old.docx",
"why": ""
},
{
"file": "f5",
"name": "chapter-2-methods-v4.docx",
"why": ""
},
{
"file": "f6",
"name": "lit-review-notes.md",
"why": ""
},
{
"file": "f7",
"name": "references.bib",
"why": ""
}
],
"unsure": [],
"note": "Each file gets a lowercase hyphenated name built from the thesis, chapter, or part it belongs to and its version, with no files needing your input."
}
The scheme: thesis-or-chapter-or-part-vN.ext - "Follows the order you gave: the thesis, chapter, or part name, then version when present, before the extension."
The renames the page trusts (ReplyKit.trusted, every name checked again), old against new; a file with no new name keeps its own:
| Old name | New name | Status | Issues | Why |
|---|---|---|---|---|
| thesis draft v2.docx | thesis-draft-v2.docx | rename | - | - |
| Thesis_v3_comments from Dr Lee.docx | thesis-comments-v3.docx | rename | - | Dropped the name of the person who commented; kept comments and version. |
| thesis FINAL.docx | thesis-final.docx | rename | - | - |
| chapter 2 - methods (old).docx | chapter-2-methods-old.docx | rename | - | - |
| Chapter2_Methods_v4.docx | chapter-2-methods-v4.docx | rename | - | - |
| lit review notes.md | lit-review-notes.md | rename | - | - |
| references.bib | references.bib | unchanged | - | - |
The questions:
- none - every file fits the scheme
The verdict: consistent - "Every file fits the scheme"; it agrees with the questions: no file is left with a question.
The note: "Each file gets a lowercase hyphenated name built from the thesis, chapter, or part it belongs to and its version, with no files needing your input."
The page's check (ReplyKit.check): kept.
No notes.
The rename script for the first sample
What NameKit.bash(t.rows, false) writes for "A year of invoices" above: the 5 renames whose status is ok, in two phases through temporary names, so no rename can overwrite another file. Files that keep their name, and any name with a problem, are left out. NameKit.bash(t.rows, true) writes the undo script, and NameKit.powershell the same pair for Windows.
#!/usr/bin/env bash
# Tidy Names - 5 renames. Run it in the folder that holds the files.
# Every file moves to a temporary name first, so no rename can overwrite another file.
set -euo pipefail
mv -n -- 'Invoice_ACME_march2024.pdf' '.tidy-names-1.tmp'
mv -n -- 'acme invoice 2024-04-02 FINAL.pdf' '.tidy-names-2.tmp'
mv -n -- 'Copy of Invoice Bluebird 14 June 2024.pdf' '.tidy-names-3.tmp'
mv -n -- 'Northwind inv 12 Jan 2024.PDF' '.tidy-names-4.tmp'
mv -n -- 'northwind_invoice_2024_02_09.pdf' '.tidy-names-5.tmp'
mv -n -- '.tidy-names-1.tmp' '2024-03-acme-invoice.pdf'
mv -n -- '.tidy-names-2.tmp' '2024-04-02-acme-invoice.pdf'
mv -n -- '.tidy-names-3.tmp' '2024-06-14-bluebird-invoice.pdf'
mv -n -- '.tidy-names-4.tmp' '2024-01-12-northwind-invoice.pdf'
mv -n -- '.tidy-names-5.tmp' '2024-02-09-northwind-invoice.pdf'
Build the input yourself
Load the engine in node (global.window = global; require("./namekit.js"); require("./replykit.js")), then:
const K = NameKit, R = ReplyKit;
const form = K.form({ goal: "Receipts for my tax return", scheme: "date, shop, receipt",
files: "Scan 2024-03-05 Tesco.pdf\nreceipt ikea march 2024.PDF" });
// form.files: every name as read [{id, path, dir, stem, ext, date: {iso, ambiguous} or null, version}]
K.check(form, K.ruleNames(form)) // the free rule renames, every one checked, no run needed
const built = K.buildInput(form); // built.ok is false, with built.note, until there is a name
// built.sent: the files in the input; built.rules: the rule renames as checked; 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) // {names: {f1: new name}, asked, dropped, missing, rows, why}
// R.check(reply, built) // {status: kept or flagged, notes}
// K.bash(t.rows, false) // the rename script; true for the undo script
// K.toCSV(t.rows) // old, new, status, issues
Form keys: files (the names, one per line), goal (what the files are, or empty), scheme (the parts wanted in each name, in order, or empty), and the rule settings: find and replace (plain text), dates (iso, front or keep), noise (drop or keep), sep (-, _, space or keep), kase (lower, title, upper or keep), ext (lower or keep), prefix (plain text) and number (none, prefix or suffix). The rule settings shape only the free renames; the input sends the goal, the scheme and the names as read.
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://tidy-names.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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://tidy-names.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// tidy-names.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://tidy-names.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://tidy-names.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// TidyNames.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class TidyNames {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://tidy-names.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://tidy-names.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://tidy-names.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://tidy-names.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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":"tidy-names"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "tidy-names"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "tidy-names" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "tidy-names"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "tidy-names"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "tidy-names" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "tidy-names"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "tidy-names" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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": "names", "goal": "Receipts for my tax return", "scheme": "date, shop, receipt", "files": "f1. Scan 2024-03-05 Tesco.pdf | date: 2024-03-05 | version: none | extension: .pdf\nf2. receipt ikea march 2024.PDF | date: 2024-03 | version: none | extension: .PDF", "counts": "2 files; 2 with a date; 1 kind of extension"}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "names", "goal": "Receipts for my tax return", "scheme": "date, shop, receipt", "files": "f1. Scan 2024-03-05 Tesco.pdf | date: 2024-03-05 | version: none | extension: .pdf\nf2. receipt ikea march 2024.PDF | date: 2024-03 | version: none | extension: .PDF", "counts": "2 files; 2 with a date; 1 kind of extension"}
# - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "names", goal, scheme, files, counts} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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 tidy-names:<task>:<inputHash>:a<attempt> (the one task is names), 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="tidy-names:names:$(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, .scheme.pattern, (.names[] | "\(.file) \(.name)")' reply.json
import hashlib, time
key = "tidy-names:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
print((reply.get("scheme") or {}).get("pattern", ""))
for n in reply.get("names") or []:
print(n["file"], n["name"])
import { createHash } from "node:crypto";
const key = `tidy-names:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
console.log((reply.scheme || {}).pattern);
for (const n of reply.names || []) console.log(n.file, n.name);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "tidy-names:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
scheme, _ := reply["scheme"].(map[string]any)
names, _ := reply["names"].([]any)
fmt.Println(j["charged_credits"], j["truncated"], reply["verdict"])
fmt.Println(scheme["pattern"])
for _, x := range names {
n := x.(map[string]any)
fmt.Println(n["file"], n["name"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "tidy-names:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.get("verdict"));
System.out.println(reply.path("scheme").path("pattern").asText());
for (JsonNode n : reply.path("names")) System.out.println(n.path("file").asText() + " " + n.path("name").asText());
require "digest"
key = "tidy-names:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
puts reply.dig("scheme", "pattern")
(reply["names"] || []).each { |n| puts "#{n["file"]} #{n["name"]}" }
$key = "tidy-names:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["verdict"] ?? ""), "\n";
echo $reply["scheme"]["pattern"] ?? "", "\n";
foreach ($reply["names"] ?? [] as $n) echo $n["file"], " ", $n["name"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"tidy-names:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply!["verdict"]}");
Console.WriteLine($"{reply["scheme"]?["pattern"]}");
foreach (var n in reply["names"] as JsonArray ?? new JsonArray()) Console.WriteLine($"{n!["file"]} {n["name"]}");
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: tidy-names:names:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "names", "goal": "Receipts for my tax return", "scheme": "date, shop, receipt", "files": "f1. Scan 2024-03-05 Tesco.pdf | date: 2024-03-05 | version: none | extension: .pdf\nf2. receipt ikea march 2024.PDF | date: 2024-03 | version: none | extension: .PDF", "counts": "2 files; 2 with a date; 1 kind of extension"}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "tidy-names:names:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `tidy-names:names:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "tidy-names:names:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "tidy-names:names:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "tidy-names:names:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: tidy-names:names:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"tidy-names:names:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);