Drive Deep Hours from your own code
The one metered task, focus, is available over HTTP. It takes a working week as the page reads it - what the focus time is for, the weekly target in hours, the shortest block worth having, the time of day to favour, the working days and hours, every recurring meeting as one numbered line (m1, m2 ...), every free stretch long enough for focus as one numbered line (g1, g2 ...), the free total and the meetings whose move would open the most focus time - and returns the focus blocks, each placed inside one free stretch with a title and a reason, a verdict (met or short), the meetings worth moving when the week cannot hold the target, a status line for each calendar block and a note.
The division of labour first: the page reads the meetings and finds every free stretch exactly; the model chooses which stretches to protect and how much of each, names the blocks and says which meeting is worth moving, referring to stretches and meetings only by id so the page can check every time. The page reads up to 30 meeting lines, keeps 10 minutes clear before and after each meeting, keeps only stretches at least the shortest block long inside the working hours, and lists the three meetings whose move would add the most focus-length free time. After the reply it keeps only blocks that sit inside a stretch it sent, on the quarter hour, between the shortest block and 180 minutes long, one per stretch, re-checks the verdict against the blocks' own total and every number the reply quotes, and builds the calendar file. Your code can do the same with the engine files the page loads (see "Build the input yourself").
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"deep-hours"}), 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, target_hours, min_block, prefer, workdays, hours, meetings, gaps, free_total, move_options) 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 focus 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: focus (the one task is focus); a missing or different one is still answered as focus. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always focus. |
goal | yes | What the focus time is for, in the person's words (at most 140 characters): Writing the quarterly report. |
target_hours | yes | The focus hours wanted a week, 1 to 30, rounded to the quarter hour, as a string (10, 7.5). |
min_block | yes | The shortest block worth having, in minutes: 60, 90 or 120. |
prefer | yes | morning (earliest stretches first), afternoon (latest first) or any (longest first). |
workdays | yes | The working days in week order: Mon, Tue, Wed, Thu, Fri (also Sun-Thu or Mon-Sat). |
hours | yes | The working hours as HH:MM-HH:MM: 09:00-17:30. |
meetings | yes | One line per recurring meeting, at most 30: m2. Tue, Thu 14:00-15:00 client calls - id, days, time and title - or none. |
gaps | yes | One line per free stretch, in day then time order: g1. Mon 09:55-12:20 (145 min). |
free_total | yes | The free time in those stretches as the page counts it: 22.5 hours in 8 stretches of 90+ minutes. |
move_options | yes | Up to three meetings, most gain first, and what moving each would add: m4. retro (Fri 15:00-16:00): moving it adds 245 min of focus-length free time, or none. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
How the page reads the week (WeekKit): each meeting line holds its days anywhere (Mon, Tue-Thu, Mon & Wed, daily, weekdays), one time range (9:30-10, 2-3pm, 14:00-15:30) and a title, which is whatever is left. A range like 2-3pm puts both ends in the afternoon, and a range with no am or pm that starts before 7 is read as the afternoon. A line that names no day is read as every working day, and a line with no readable range is skipped; the page warns about both. On each working day the page blocks out every meeting plus 10 minutes either side and keeps each free stretch at least the shortest block long inside the working hours. For every meeting it also measures how much more free time in stretches of that length the week would have without it, and sends the best three. The page needs a goal of a few words and at least one free stretch before it will send.
The free fill (WeekKit.freeFill) needs no run: it takes the stretches in the preferred order, one day at a time before a second stretch on any day, and places a block on the quarter hour, up to 180 minutes, until the target is reached; the last block is trimmed but never below the shortest block. The run does the same choosing with a title and a reason for each block, and the advice on what to move.
The model's rules: at most one block per stretch, in day then time order, each on the quarter hour (minutes 00, 15, 30 or 45) inside its stretch, at least the shortest block and at most 180 minutes long. Stretches are chosen by the preference - earliest, latest or longest first - on different days before a second on the same day, and blocks are added until their total reaches the target, going over by no more than 30 minutes, or as far as the stretches allow. A title has at most 6 words from the goal's own words; a reason is one sentence of at most 16 words using only the stretch's day, time and length. The verdict is met when the blocks' total reaches the target, otherwise short; only then does the reply suggest at most two meetings from the move options, preferring a once-a-week meeting over a daily routine or a meal break, and always moving, never cancelling, declining or skipping. Sentences refer to stretches and meetings by day, time and title, never by id, and quote only numbers from the input or the blocks' own total.
The reply
One JSON object:
{"task": "focus", "verdict": "short", "blocks": [{"gap": "g1", "start": "13:30", "end": "16:30", "title": "...", "why": "..."}], "move": [{"meeting": "m5", "why": "..."}], "status_line": "...", "note": "..."}
| Field | What it holds |
|---|---|
verdict | met (the blocks reach the target) or short (the week cannot hold it). |
blocks[].gap | The id of the free stretch the block sits in; at most one block per stretch. |
blocks[].start, blocks[].end | HH:MM on the quarter hour, inside the stretch. |
blocks[].title | At most 6 words, from the goal's own words; it becomes the calendar event's title. |
blocks[].why | One sentence: why this stretch suits, using only its day, time and length. |
move[] | {meeting, why}, for short only: at most two meetings from the move options, each with one sentence naming the meeting and the minutes moving it adds. [] for met. |
status_line | One sentence for the description of every calendar block: protected focus time and what it is for, with no times or names. |
note | One sentence: the hours the blocks give against the target and, for short, what the move would open. |
The page reads the reply with ReplyKit.normalize (ids matched in any case, times read as HH:MM) and keeps what it trusts with ReplyKit.trusted(reply, built): a block is dropped, with the reason, when it names a stretch the page did not find, when its times are not HH:MM or not on the quarter hour, when it runs outside its stretch, when it is shorter than the shortest block or longer than three hours, or when its stretch already has a block. The rest are sorted by day and time, and their total is what the page checks the verdict against. Move suggestions are shown only for short, only for meetings among the page's own options, and at most two.
ReplyKit.check(reply, built) lists, under "The page's check": a missing verdict, or one the blocks' own total contradicts; every dropped block, or no usable block at all; blocks more than half an hour over the target; a short week that still has unused stretches; a day with two blocks while another day has a free stretch with none; a move suggested when the target is met, a meeting not among the options, or more than two moves; any number in a title, a reason, the status line or the note that is not in the input or the blocks; a sentence that names an id or an internal field; any word about cancelling, declining or skipping; words such as "still" or "always" the goal and meetings never used; and a status line that gives a time. Any warning makes the status flagged; otherwise it is kept. The verdict on the page reads "Target met" or "The week is too full".
The downloads: the calendar file (WeekKit.toICS(blocks, {from, note, uid})) holds one weekly repeating event per trusted block, in floating local time so the calendar's own time zone applies, starting on the first matching weekday on or after from (a YYYY-MM-DD date, or today) and carrying the status line as its description. ReplyKit.plain copies the blocks as text, ReplyKit.md and ReplyKit.json write the plan with the meetings worth moving, and the week timetable saves as PNG or SVG.
Worked examples - every sample
These are the free samples on the page: the week as typed, the meetings and free stretches the page reads, the exact input it sends, the reply that replays for free, and what the page does with it - the blocks it keeps, the meetings worth moving and its own check. Together they cover the verdicts met and short.
Writing a quarterly report
The form: Writing the quarterly report; 10 hours a week in blocks of at least 90 minutes, earliest stretches first; working hours 09:00-17:30, Mon-Fri. The meetings as typed:
daily 9:30-9:45 standup
Tue, Thu 2-3pm client calls
Wed 11:00-12:00 team review
Fri 15:00-16:00 retro
weekdays 12:30-13:15 lunch
The meetings as the page reads them, the free stretches it finds (22.5 hours in 8 stretches of 90+ minutes) and the move options (WeekKit.buildInput):
m1. Mon, Tue, Wed, Thu, Fri 09:30-09:45 standup
m2. Tue, Thu 14:00-15:00 client calls
m3. Wed 11:00-12:00 team review
m4. Fri 15:00-16:00 retro
m5. Mon, Tue, Wed, Thu, Fri 12:30-13:15 lunch
g1. Mon 09:55-12:20 (145 min)
g2. Mon 13:25-17:30 (245 min)
g3. Tue 09:55-12:20 (145 min)
g4. Tue 15:10-17:30 (140 min)
g5. Wed 13:25-17:30 (245 min)
g6. Thu 09:55-12:20 (145 min)
g7. Thu 15:10-17:30 (140 min)
g8. Fri 09:55-12:20 (145 min)
m5. lunch (Mon, Tue, Wed, Thu, Fri 12:30-13:15): moving it adds 470 min of focus-length free time
m1. standup (Mon, Tue, Wed, Thu, Fri 09:30-09:45): moving it adds 330 min of focus-length free time
m4. retro (Fri 15:00-16:00): moving it adds 245 min of focus-length free time
The free fill, placed with no run (WeekKit.freeFill):
Mon 10:00-12:15 Focus time
Tue 10:00-12:15 Focus time
Wed 13:30-15:00 Focus time
Thu 10:00-12:15 Focus time
Fri 10:00-12:15 Focus time
Input:
{
"task": "focus",
"goal": "Writing the quarterly report",
"target_hours": "10",
"min_block": "90",
"prefer": "morning",
"workdays": "Mon, Tue, Wed, Thu, Fri",
"hours": "09:00-17:30",
"meetings": "m1. Mon, Tue, Wed, Thu, Fri 09:30-09:45 standup\nm2. Tue, Thu 14:00-15:00 client calls\nm3. Wed 11:00-12:00 team review\nm4. Fri 15:00-16:00 retro\nm5. Mon, Tue, Wed, Thu, Fri 12:30-13:15 lunch",
"gaps": "g1. Mon 09:55-12:20 (145 min)\ng2. Mon 13:25-17:30 (245 min)\ng3. Tue 09:55-12:20 (145 min)\ng4. Tue 15:10-17:30 (140 min)\ng5. Wed 13:25-17:30 (245 min)\ng6. Thu 09:55-12:20 (145 min)\ng7. Thu 15:10-17:30 (140 min)\ng8. Fri 09:55-12:20 (145 min)",
"free_total": "22.5 hours in 8 stretches of 90+ minutes",
"move_options": "m5. lunch (Mon, Tue, Wed, Thu, Fri 12:30-13:15): moving it adds 470 min of focus-length free time\nm1. standup (Mon, Tue, Wed, Thu, Fri 09:30-09:45): moving it adds 330 min of focus-length free time\nm4. retro (Fri 15:00-16:00): moving it adds 245 min of focus-length free time"
}
Reply:
{
"task": "focus",
"verdict": "met",
"blocks": [
{
"gap": "g1",
"start": "10:00",
"end": "12:15",
"title": "Writing the quarterly report",
"why": "Monday's 09:55 to 12:20 stretch is 145 minutes, long enough for a focus block."
},
{
"gap": "g3",
"start": "10:00",
"end": "12:15",
"title": "Writing the quarterly report",
"why": "Tuesday's 09:55 to 12:20 stretch is 145 minutes, long enough for a focus block."
},
{
"gap": "g5",
"start": "13:30",
"end": "15:00",
"title": "Writing the quarterly report",
"why": "Wednesday's 13:25 to 17:30 stretch is 245 minutes, plenty for an afternoon focus block."
},
{
"gap": "g6",
"start": "10:00",
"end": "12:15",
"title": "Writing the quarterly report",
"why": "Thursday's 09:55 to 12:20 stretch is 145 minutes, long enough for a focus block."
},
{
"gap": "g8",
"start": "10:00",
"end": "12:15",
"title": "Writing the quarterly report",
"why": "Friday's 09:55 to 12:20 stretch is 145 minutes, long enough for a focus block."
}
],
"move": [],
"status_line": "This is protected focus time for writing the quarterly report.",
"note": "These blocks give 10.5 hours of focus time against a target of 10 hours."
}
The blocks the page keeps (ReplyKit.trusted), 10.5 hours against a target of 10:
- Mon 10:00-12:15 - Writing the quarterly report - Monday's 09:55 to 12:20 stretch is 145 minutes, long enough for a focus block.
- Tue 10:00-12:15 - Writing the quarterly report - Tuesday's 09:55 to 12:20 stretch is 145 minutes, long enough for a focus block.
- Wed 13:30-15:00 - Writing the quarterly report - Wednesday's 13:25 to 17:30 stretch is 245 minutes, plenty for an afternoon focus block.
- Thu 10:00-12:15 - Writing the quarterly report - Thursday's 09:55 to 12:20 stretch is 145 minutes, long enough for a focus block.
- Fri 10:00-12:15 - Writing the quarterly report - Friday's 09:55 to 12:20 stretch is 145 minutes, long enough for a focus block.
The verdict: met - "Target met"; it follows the blocks' total (10.5 hours against 10).
Worth moving: nothing - the week holds the target.
On each block: "This is protected focus time for writing the quarterly report."
The page's check (ReplyKit.check): kept.
No notes.
A meeting-heavy week
The form: Learning the new data platform; 12 hours a week in blocks of at least 120 minutes, longest stretches first; working hours 09:00-17:30, Mon-Fri. The meetings as typed:
daily 9:30-9:45 standup
weekdays 12:30-13:15 lunch
Mon 11:00-12:00 planning
Tue 10:30-11:30 vendor call
Tue 15:00-16:00 design review
Wed 11:00-12:00 hiring panel
Thu 10:30-11:30 1:1 with manager
Thu 14:30-15:30 architecture forum
Fri 11:00-12:00 demo prep
The meetings as the page reads them, the free stretches it finds (12.25 hours in 3 stretches of 120+ minutes) and the move options (WeekKit.buildInput):
m1. Mon, Tue, Wed, Thu, Fri 09:30-09:45 standup
m2. Mon, Tue, Wed, Thu, Fri 12:30-13:15 lunch
m3. Mon 11:00-12:00 planning
m4. Tue 10:30-11:30 vendor call
m5. Tue 15:00-16:00 design review
m6. Wed 11:00-12:00 hiring panel
m7. Thu 10:30-11:30 1:1 with manager
m8. Thu 14:30-15:30 architecture forum
m9. Fri 11:00-12:00 demo prep
g1. Mon 13:25-17:30 (245 min)
g2. Wed 13:25-17:30 (245 min)
g3. Fri 13:25-17:30 (245 min)
m2. lunch (Mon, Tue, Wed, Thu, Fri 12:30-13:15): moving it adds 575 min of focus-length free time
m5. design review (Tue 15:00-16:00): moving it adds 245 min of focus-length free time
m8. architecture forum (Thu 14:30-15:30): moving it adds 245 min of focus-length free time
The free fill, placed with no run (WeekKit.freeFill):
Mon 13:30-16:30 Focus time
Wed 13:30-16:30 Focus time
Fri 13:30-16:30 Focus time
Input:
{
"task": "focus",
"goal": "Learning the new data platform",
"target_hours": "12",
"min_block": "120",
"prefer": "any",
"workdays": "Mon, Tue, Wed, Thu, Fri",
"hours": "09:00-17:30",
"meetings": "m1. Mon, Tue, Wed, Thu, Fri 09:30-09:45 standup\nm2. Mon, Tue, Wed, Thu, Fri 12:30-13:15 lunch\nm3. Mon 11:00-12:00 planning\nm4. Tue 10:30-11:30 vendor call\nm5. Tue 15:00-16:00 design review\nm6. Wed 11:00-12:00 hiring panel\nm7. Thu 10:30-11:30 1:1 with manager\nm8. Thu 14:30-15:30 architecture forum\nm9. Fri 11:00-12:00 demo prep",
"gaps": "g1. Mon 13:25-17:30 (245 min)\ng2. Wed 13:25-17:30 (245 min)\ng3. Fri 13:25-17:30 (245 min)",
"free_total": "12.25 hours in 3 stretches of 120+ minutes",
"move_options": "m2. lunch (Mon, Tue, Wed, Thu, Fri 12:30-13:15): moving it adds 575 min of focus-length free time\nm5. design review (Tue 15:00-16:00): moving it adds 245 min of focus-length free time\nm8. architecture forum (Thu 14:30-15:30): moving it adds 245 min of focus-length free time"
}
Reply:
{
"task": "focus",
"verdict": "short",
"blocks": [
{
"gap": "g1",
"start": "13:30",
"end": "16:30",
"title": "Learning the new data platform",
"why": "The Mon 13:25 to 17:30 stretch is 245 minutes, enough for a dedicated focus block."
},
{
"gap": "g2",
"start": "13:30",
"end": "16:30",
"title": "Learning the new data platform",
"why": "The Wed 13:25 to 17:30 stretch is 245 minutes, enough for a dedicated focus block."
},
{
"gap": "g3",
"start": "13:30",
"end": "16:30",
"title": "Learning the new data platform",
"why": "The Fri 13:25 to 17:30 stretch is 245 minutes, enough for a dedicated focus block."
}
],
"move": [
{
"meeting": "m5",
"why": "Moving the Tue 15:00 to 16:00 design review would add 245 minutes of focus-length free time."
},
{
"meeting": "m8",
"why": "Moving the Thu 14:30 to 15:30 architecture forum would add 245 minutes of focus-length free time."
}
],
"status_line": "Protected focus time for learning the new data platform.",
"note": "The blocks give 9 hours against the 12-hour target, and moving the design review and the architecture forum would each open 245 more minutes."
}
The blocks the page keeps (ReplyKit.trusted), 9 hours against a target of 12:
- Mon 13:30-16:30 - Learning the new data platform - The Mon 13:25 to 17:30 stretch is 245 minutes, enough for a dedicated focus block.
- Wed 13:30-16:30 - Learning the new data platform - The Wed 13:25 to 17:30 stretch is 245 minutes, enough for a dedicated focus block.
- Fri 13:30-16:30 - Learning the new data platform - The Fri 13:25 to 17:30 stretch is 245 minutes, enough for a dedicated focus block.
The verdict: short - "The week is too full"; it follows the blocks' total (9 hours against 12).
Worth moving (the trusted suggestions):
- design review (Tue 15:00-16:00) - Moving the Tue 15:00 to 16:00 design review would add 245 minutes of focus-length free time.
- architecture forum (Thu 14:30-15:30) - Moving the Thu 14:30 to 15:30 architecture forum would add 245 minutes of focus-length free time.
On each block: "Protected focus time for learning the new data platform."
The page's check (ReplyKit.check): kept.
No notes.
Afternoons in the studio
The form: Drawing the illustrations for the picture book; 6 hours a week in blocks of at least 60 minutes, latest stretches first; working hours 10:00-18:00, Mon-Fri. The meetings as typed:
Mon, Wed 10:00-11:30 client check-ins
daily 13:00-14:00 lunch
Fri 15:00-17:00 invoicing and admin
The meetings as the page reads them, the free stretches it finds (26.25 hours in 9 stretches of 60+ minutes) and the move options (WeekKit.buildInput):
m1. Mon, Wed 10:00-11:30 client check-ins
m2. Mon, Tue, Wed, Thu, Fri 13:00-14:00 lunch
m3. Fri 15:00-17:00 invoicing and admin
g1. Mon 11:40-12:50 (70 min)
g2. Mon 14:10-18:00 (230 min)
g3. Tue 10:00-12:50 (170 min)
g4. Tue 14:10-18:00 (230 min)
g5. Wed 11:40-12:50 (70 min)
g6. Wed 14:10-18:00 (230 min)
g7. Thu 10:00-12:50 (170 min)
g8. Thu 14:10-18:00 (230 min)
g9. Fri 10:00-12:50 (170 min)
m2. lunch (Mon, Tue, Wed, Thu, Fri 13:00-14:00): moving it adds 440 min of focus-length free time
m3. invoicing and admin (Fri 15:00-17:00): moving it adds 230 min of focus-length free time
m1. client check-ins (Mon, Wed 10:00-11:30): moving it adds 200 min of focus-length free time
The free fill, placed with no run (WeekKit.freeFill):
Mon 15:00-18:00 Focus time
Tue 15:00-18:00 Focus time
Input:
{
"task": "focus",
"goal": "Drawing the illustrations for the picture book",
"target_hours": "6",
"min_block": "60",
"prefer": "afternoon",
"workdays": "Mon, Tue, Wed, Thu, Fri",
"hours": "10:00-18:00",
"meetings": "m1. Mon, Wed 10:00-11:30 client check-ins\nm2. Mon, Tue, Wed, Thu, Fri 13:00-14:00 lunch\nm3. Fri 15:00-17:00 invoicing and admin",
"gaps": "g1. Mon 11:40-12:50 (70 min)\ng2. Mon 14:10-18:00 (230 min)\ng3. Tue 10:00-12:50 (170 min)\ng4. Tue 14:10-18:00 (230 min)\ng5. Wed 11:40-12:50 (70 min)\ng6. Wed 14:10-18:00 (230 min)\ng7. Thu 10:00-12:50 (170 min)\ng8. Thu 14:10-18:00 (230 min)\ng9. Fri 10:00-12:50 (170 min)",
"free_total": "26.25 hours in 9 stretches of 60+ minutes",
"move_options": "m2. lunch (Mon, Tue, Wed, Thu, Fri 13:00-14:00): moving it adds 440 min of focus-length free time\nm3. invoicing and admin (Fri 15:00-17:00): moving it adds 230 min of focus-length free time\nm1. client check-ins (Mon, Wed 10:00-11:30): moving it adds 200 min of focus-length free time"
}
Reply:
{
"task": "focus",
"verdict": "met",
"blocks": [
{
"gap": "g2",
"start": "14:15",
"end": "17:15",
"title": "Drawing the picture book illustrations",
"why": "Monday's 14:10 to 18:00 stretch is 230 minutes long, enough for a solid block."
},
{
"gap": "g4",
"start": "14:15",
"end": "17:15",
"title": "Illustrations for the picture book",
"why": "Tuesday's 14:10 to 18:00 stretch is 230 minutes long, plenty for another block."
}
],
"move": [],
"status_line": "Protected focus time for drawing the illustrations for the picture book.",
"note": "These blocks give 6 hours of focus time, matching the target."
}
The blocks the page keeps (ReplyKit.trusted), 6 hours against a target of 6:
- Mon 14:15-17:15 - Drawing the picture book illustrations - Monday's 14:10 to 18:00 stretch is 230 minutes long, enough for a solid block.
- Tue 14:15-17:15 - Illustrations for the picture book - Tuesday's 14:10 to 18:00 stretch is 230 minutes long, plenty for another block.
The verdict: met - "Target met"; it follows the blocks' total (6 hours against 6).
Worth moving: nothing - the week holds the target.
On each block: "Protected focus time for drawing the illustrations for the picture book."
The page's check (ReplyKit.check): kept.
No notes.
The calendar file for the first sample
The opening lines of what WeekKit.toICS(blocks, {from: "", note: status_line, uid: "example"}) writes for "Writing a quarterly report" above, made on Monday 2 November 2026 with no start date: the calendar header and the first of its 5 weekly events (lines end in CRLF; long lines fold at 75 octets).
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//SkillSafe//Deep Hours//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
UID:deep-hours-example-0@skillsafe.ai
DTSTAMP:20261102T090000Z
DTSTART:20261102T100000
DTEND:20261102T121500
RRULE:FREQ=WEEKLY;BYDAY=MO
SUMMARY:Writing the quarterly report
DESCRIPTION:This is protected focus time for writing the quarterly report.
TRANSP:OPAQUE
CLASS:PUBLIC
END:VEVENT
Build the input yourself
Load the engine in node (global.window = global; require("./weekkit.js"); require("./replykit.js")), then:
const K = WeekKit, R = ReplyKit;
const form = K.form({ goal: "Writing the quarterly report", meetings: "daily 9:30-9:45 standup\nWed 11:00-12:00 team review",
start: "09:00", end: "12:30", week: "mon-fri", target: "6", minBlock: "90", prefer: "morning", from: "" });
// form.meetings is the week as read: [{id, days, start, end, title}], times in minutes from midnight
const built = K.buildInput(form); // built.ok is false, with built.note, until the goal is named and a stretch fits
// built.gaps: the free stretches [{id, day, start, end, len}]; built.moves: the move options
K.freeFill(form, built.gaps) // the free fill, no run needed
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) // {blocks, dropped, total}: only blocks inside a stretch the page sent
// R.plain(t.blocks) // "Mon 10:00-12:30 Writing the quarterly report" ...
// R.check(reply, built) // {status: kept or flagged, notes}
// K.toICS(t.blocks, { from: "2026-11-02", note: reply.status_line }) // the calendar file
Form keys: goal (what the focus time is for), meetings (one recurring meeting per line), start and end (the working hours), week (mon-fri, sun-thu or mon-sat), target (hours a week, 1 to 30), minBlock (60, 90 or 120), prefer (morning, afternoon or any) and from (the calendar file's first date as YYYY-MM-DD, or empty for today).
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. 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://deep-hours.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://deep-hours.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"]
// deep-hours.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://deep-hours.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://deep-hours.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
}
// DeepHours.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 DeepHours {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://deep-hours.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://deep-hours.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://deep-hours.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://deep-hours.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":"deep-hours"}' | 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": "deep-hours"})["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: "deep-hours" })).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": "deep-hours"}, 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", "deep-hours"), 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: "deep-hours" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "deep-hours"])["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 = "deep-hours" }))!["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": "focus", "goal": "Writing the quarterly report", "target_hours": "6", "min_block": "90", "prefer": "morning", "workdays": "Mon, Tue, Wed, Thu, Fri", "hours": "09:00-12:30", "meetings": "m1. Mon, Tue, Wed, Thu, Fri 09:30-09:45 standup\nm2. Wed 11:00-12:00 team review", "gaps": "g1. Mon 09:55-12:30 (155 min)\ng2. Tue 09:55-12:30 (155 min)\ng3. Thu 09:55-12:30 (155 min)\ng4. Fri 09:55-12:30 (155 min)", "free_total": "10.25 hours in 4 stretches of 90+ minutes", "move_options": "m1. standup (Mon, Tue, Wed, Thu, Fri 09:30-09:45): moving it adds 330 min of focus-length free time\nm2. team review (Wed 11:00-12:00): moving it adds 155 min of focus-length free time"}
# - 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": "focus", "goal": "Writing the quarterly report", "target_hours": "6", "min_block": "90", "prefer": "morning", "workdays": "Mon, Tue, Wed, Thu, Fri", "hours": "09:00-12:30", "meetings": "m1. Mon, Tue, Wed, Thu, Fri 09:30-09:45 standup\nm2. Wed 11:00-12:00 team review", "gaps": "g1. Mon 09:55-12:30 (155 min)\ng2. Tue 09:55-12:30 (155 min)\ng3. Thu 09:55-12:30 (155 min)\ng4. Fri 09:55-12:30 (155 min)", "free_total": "10.25 hours in 4 stretches of 90+ minutes", "move_options": "m1. standup (Mon, Tue, Wed, Thu, Fri 09:30-09:45): moving it adds 330 min of focus-length free time\nm2. team review (Wed 11:00-12:00): moving it adds 155 min of focus-length free time"}
# - 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: "focus", goal, target_hours, min_block, prefer, workdays, hours, meetings, gaps, free_total, move_options} (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 deep-hours:<task>:<inputHash>:a<attempt> (the one task is focus), 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="deep-hours:focus:$(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) \(.blocks | length)", (.blocks[] | "\(.start)-\(.end) \(.title)")' reply.json
import hashlib, time
key = "deep-hours:" + 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"), len(reply.get("blocks") or []))
for b in reply.get("blocks") or []:
print(b["start"] + "-" + b["end"], b["title"])
import { createHash } from "node:crypto";
const key = `deep-hours:${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, (reply.blocks || []).length);
for (const b of reply.blocks || []) console.log(`${b.start}-${b.end}`, b.title);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "deep-hours:" + 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)
blocks, _ := reply["blocks"].([]any)
fmt.Println(j["charged_credits"], j["truncated"], reply["verdict"], len(blocks))
for _, x := range blocks {
b := x.(map[string]any)
fmt.Println(fmt.Sprint(b["start"]) + "-" + fmt.Sprint(b["end"]), b["title"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "deep-hours:" + 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") + " " + reply.path("blocks").size());
for (JsonNode b : reply.path("blocks")) System.out.println(b.path("start").asText() + "-" + b.path("end").asText() + " " + b.path("title").asText());
require "digest"
key = "deep-hours:#{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"], (reply["blocks"] || []).size].inspect
(reply["blocks"] || []).each { |b| puts "#{b["start"]}-#{b["end"]} #{b["title"]}" }
$key = "deep-hours:" . $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"] ?? ""), " ", count($reply["blocks"] ?? []), "\n";
foreach ($reply["blocks"] ?? [] as $b) echo $b["start"], "-", $b["end"], " ", $b["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"deep-hours:{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"]} {(reply["blocks"] as JsonArray)?.Count ?? 0}");
foreach (var b in reply["blocks"] as JsonArray ?? new JsonArray()) Console.WriteLine($"{b!["start"]}-{b["end"]} {b["title"]}");
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: deep-hours:focus:$(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": "focus", "goal": "Writing the quarterly report", "target_hours": "6", "min_block": "90", "prefer": "morning", "workdays": "Mon, Tue, Wed, Thu, Fri", "hours": "09:00-12:30", "meetings": "m1. Mon, Tue, Wed, Thu, Fri 09:30-09:45 standup\nm2. Wed 11:00-12:00 team review", "gaps": "g1. Mon 09:55-12:30 (155 min)\ng2. Tue 09:55-12:30 (155 min)\ng3. Thu 09:55-12:30 (155 min)\ng4. Fri 09:55-12:30 (155 min)", "free_total": "10.25 hours in 4 stretches of 90+ minutes", "move_options": "m1. standup (Mon, Tue, Wed, Thu, Fri 09:30-09:45): moving it adds 330 min of focus-length free time\nm2. team review (Wed 11:00-12:00): moving it adds 155 min of focus-length free time"}
# - 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": "deep-hours:focus:" + 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": `deep-hours:focus:${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", "deep-hours:focus:"+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", "deep-hours:focus:" + 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" => "deep-hours:focus:#{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: deep-hours:focus:" . 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", $"deep-hours:focus:{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);