@lingochunk/mcp 0.3.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -11
- package/dist/client.d.ts +207 -0
- package/dist/client.js +164 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +23 -0
- package/dist/config.js +41 -14
- package/dist/config.js.map +1 -1
- package/dist/generated/guides.d.ts +13 -0
- package/dist/generated/guides.js +50 -0
- package/dist/generated/guides.js.map +1 -0
- package/dist/http.d.ts +22 -0
- package/dist/http.js +174 -0
- package/dist/http.js.map +1 -0
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.js +26 -0
- package/dist/prompts.js.map +1 -0
- package/dist/tools.d.ts +7 -1
- package/dist/tools.js +807 -56
- package/dist/tools.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -14,13 +14,23 @@ contributions (see [CONTRIBUTING.md](CONTRIBUTING.md)).
|
|
|
14
14
|
> Install: `/plugin marketplace add lingochunk/lingochunk-mcp` in Claude Code (server
|
|
15
15
|
> plus lesson skills), or `npx -y @lingochunk/mcp` as a standalone MCP server.
|
|
16
16
|
|
|
17
|
+
**A creator rather than a coder?** If you make audio (a podcast, a YouTube
|
|
18
|
+
channel, classes) and want your episodes to become annotated transcripts,
|
|
19
|
+
lessons, courses and multi-language study guides your audience can use, start
|
|
20
|
+
with **[LingoChunk for creators](docs/creator-guide.md)** - no programming
|
|
21
|
+
required, everything happens by talking to an AI assistant.
|
|
22
|
+
|
|
17
23
|
## What it gives an agent
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
Thirty tools. Twelve read from your account; sixteen write to it; two serve
|
|
26
|
+
guidance: `whats_possible` answers "what can I do here?" with a short menu
|
|
27
|
+
and an example prompt per area, and `get_authoring_guide` serves the full
|
|
28
|
+
authoring craft guides so remote clients that never see the skills still
|
|
29
|
+
compose good lessons, cards and annotations.
|
|
21
30
|
|
|
22
31
|
| Tool | Scope | What it does |
|
|
23
32
|
|---|---|---|
|
|
33
|
+
| `whats_possible` | none | The quick tour: every area this connection covers (discuss, vocabulary, lessons, courses, cards, languages, creator notes, publishing), one example prompt each. The agent answers short and offers to go deeper per area. |
|
|
24
34
|
| `get_vocabulary` | `vocab:read` | Your vocabulary, aggregated per word with FSRS maturity (known/learning/new/due). Filterable; incremental sync via `since` + `cursor` (additive-only, so full-resync periodically). |
|
|
25
35
|
| `lookup_word` | `vocab:read` | One word: your own context plus a shared-lexicon gender/CEFR fallback. Grounds an LLM's guesses. |
|
|
26
36
|
| `list_library` | `content:read` | Your ready-to-study episodes (own + followed collections), cursor-paginated. |
|
|
@@ -28,33 +38,75 @@ the last five write to your account.
|
|
|
28
38
|
| `get_audio_url` | `content:read` | A short-lived presigned URL to the full native audio (Range-capable). |
|
|
29
39
|
| `search_examples` | `content:read` | Example sentences across your library, by word (`lemma`) or text (`q`). A capped sample, not exhaustive. |
|
|
30
40
|
| `get_audio_clip` | `content:read` | Cuts a short native-audio snippet, **saves it to a local file**, and returns `{path, media_type, size_bytes}` for embedding in lessons. |
|
|
41
|
+
| `get_authoring_guide` | none | Returns the full craft guide for `topic` (`overview`, `lesson`, `course`, `cards`, `annotations`, `add-language`, `discuss`) - the same content as the bundled skills, embedded in the package so remote clients get it too. Call it before composing. |
|
|
31
42
|
| `list_decks` | `cards:write` or `decks:export` | Your study decks with card counts, for picking a `deck_id` to add to or export. |
|
|
32
43
|
| `add_card` | `cards:write` | Adds a card to your review queue (FSRS, starts new). Preferred: the `card.v1` kinds (`word`, `phrase`, `collocation`, `idiom`, `chunk`, `grammar`, `cloze`, `contrast`, `qa`, `production`) anchored to a verbatim transcript sentence - the server derives the highlight/blur painting and native-audio clip, so the card matches the app's own. Legacy: `kind=vocab` from your vocabulary, or `kind=custom` front/back. Omit `deck_id` to use the deck for the card's own submission. |
|
|
33
44
|
| `export_anki_deck` | `decks:export` | Exports a deck to Anki `.apkg` (no LLM), polling internally; returns a download URL when ready. A deck with no linked episode can't be exported. |
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
| `validate_lesson` | `lessons:write` | Dry-run validates a `lesson.v1` document WITHOUT saving it, reporting EVERY problem at once (schema faults with a `loc`, reference faults with the same codes `save_lesson` raises) so you fix a document in one pass. Stores nothing; spends no lesson-cap budget. Call it before `save_lesson`. |
|
|
46
|
+
| `save_lesson` | `lessons:write` | Saves a lesson to your private library (100 max). Preferred: a structured `lesson.v1` document the app renders natively (Lessons tab on the episode, real audio, live word state, Ask AI); returns metadata + an `app_url`. Legacy: a self-contained HTML file (10 MB cap) opened via a short-lived view URL. Optional `course_id` (+ `sequence`) files it under a course. Creators: `visibility:'public'` publishes the lesson to everyone who can view the source episode (documents only, own episodes only). |
|
|
47
|
+
| `list_lessons` | `lessons:write` | Your saved lessons, newest first (id, title, language, format, source episode, and `course_id`/`sequence`/`course_title` when filed under a course), cursor-paginated - for finding ids and seeing what already exists. |
|
|
48
|
+
| `get_lesson` | `lessons:write` | Reads back a saved `lesson.v1` document by id, plus its `version` token. Opens the revision loop: `get_lesson` -> edit -> `update_lesson` revises in place. |
|
|
49
|
+
| `update_lesson` | `lessons:write` | Revises a saved `lesson.v1` document IN PLACE (same id, same `app_url`, same visibility/course): surgical block `ops` keyed to the app's 1-based § handles, or a full replacement `document`. `base_version` guards against overwriting a concurrent edit (409 `stale_document`). |
|
|
50
|
+
| `delete_lesson` | `lessons:write` | Permanently deletes one saved lesson by id (destructive; owner-scoped server-side). For lessons the user is done with - revision is `update_lesson`, not delete + re-save. |
|
|
51
|
+
| `create_course` | `lessons:write` | Creates a course: a named, ordered series to file lessons under. Returns its id, for `save_lesson`'s `course_id`. Authored via the API only (no in-app course editor). |
|
|
52
|
+
| `list_courses` | `lessons:write` | Your courses, newest first, each with its lesson count - for finding a `course_id` or seeing what series exist. |
|
|
53
|
+
| `delete_course` | `lessons:write` | Deletes one course by id (destructive to the grouping, idempotent). Its lessons SURVIVE - their `course_id` is set null, un-grouping them; authored content is never deleted. |
|
|
54
|
+
| `list_languages` | `content:read` | An episode's target languages and how to add more: the fan-out group so far (each with its own submission id + status), `available_targets` (ordinary Groq targets), `simplify_targets` (leveled same-language codes like `de-a2`) and in-progress drafts. |
|
|
55
|
+
| `get_translation_source` | `content:read` | Pages the primary's sentences to translate yourself: source text, the pivot-language gloss per sentence and per token (which fixes each word's sense). Feeds the draft flow. |
|
|
56
|
+
| `add_language` | `translations:write` | Fans an episode out into 1-10 extra **ordinary** target languages server-side (Groq, no tokens of yours); returns a job per language. Leveled same-language codes are rejected here - use the draft flow. |
|
|
57
|
+
| `put_language_translations` | `translations:write` | Uploads a batch (1-100) of agent-written draft sentences (whole-sentence translation + one meaning per token) for a target or leveled language; returns per-sentence rejections to repair. |
|
|
58
|
+
| `commit_language` | `translations:write` | Validates a complete draft and applies it, minting the sibling deck; polls the job and returns the new submission id when ready. |
|
|
59
|
+
| `discard_language_draft` | `translations:write` | Deletes the in-progress draft rows for a language (destructive; never a committed sibling). |
|
|
60
|
+
| `list_annotations` | `content:read` | An episode's creator annotations (each a markdown note on a transcript sentence span), plus `count` and `max_annotations` so you can budget and avoid duplicates. |
|
|
61
|
+
| `create_annotation` | `annotations:write` | Attaches a markdown creator note to a sentence span (Unicode code-point offsets into the sentence's `display`, or a whole-sentence note); the response echoes `selected_text` to verify the span. |
|
|
62
|
+
| `update_annotation` | `annotations:write` | Replaces one annotation's note in place (the anchor stays put). |
|
|
63
|
+
| `delete_annotation` | `annotations:write` | Deletes one annotation (destructive); also how you fix a mis-anchored span before re-creating it. |
|
|
64
|
+
|
|
65
|
+
Plus seven skills:
|
|
66
|
+
|
|
67
|
+
- **`lingochunk-overview`** - the "what can I do?" tour: a short menu of
|
|
68
|
+
every area (with an example prompt each) and instructions to answer
|
|
69
|
+
briefly, then expand whichever area the user picks.
|
|
39
70
|
- **`lingochunk-lesson`** - composes a coursebook-style `lesson.v1` document
|
|
40
71
|
(listen, text, vocabulary, one grammar point, graded exercises, review)
|
|
41
72
|
from the tools above, filtering out words you already know; the app
|
|
42
73
|
renders it natively and can export an offline HTML worksheet.
|
|
74
|
+
- **`lingochunk-course`** - plans a multi-lesson series: slices an episode (or
|
|
75
|
+
collection) into coherent parts, creates a course, then builds N lessons via
|
|
76
|
+
the lesson skill with a different grammar point and ramping difficulty per
|
|
77
|
+
lesson, each filed under the course in order.
|
|
43
78
|
- **`lingochunk-cards`** - builds native-grade flashcards with the `card.v1`
|
|
44
79
|
kinds: verbatim transcript anchors, per-kind guidance (grammar =
|
|
45
80
|
production cloze of the morpheme with a hint), and a quality rubric
|
|
46
81
|
distilled from the known failure modes of AI-generated cards.
|
|
47
82
|
- **`lingochunk-discuss`** - a lighter, conversational "talk me through this
|
|
48
83
|
episode" workflow.
|
|
84
|
+
- **`lingochunk-add-language`** - adds another language to one of your episodes
|
|
85
|
+
as a new sibling deck: either the server-side Groq fan-out for an ordinary
|
|
86
|
+
target, or an agent-supplied translation you write sentence by sentence and
|
|
87
|
+
commit - the only way to build a leveled same-language deck (e.g.
|
|
88
|
+
"German (A2)", German audio glossed in simpler A2 German).
|
|
89
|
+
- **`lingochunk-annotate`** - finds the genuinely useful expressions in one of
|
|
90
|
+
your episodes (idioms, phrasal verbs, collocations, discourse markers,
|
|
91
|
+
culture-bound references) and attaches a short markdown creator note to each
|
|
92
|
+
exact span: an iris tint + note sheet for you, a forward-only note card for
|
|
93
|
+
your followers.
|
|
94
|
+
|
|
95
|
+
The Claude Code plugin installs these as skills. Every client also gets the
|
|
96
|
+
same guidance two other ways that need no plugin: as MCP **prompts** (one per
|
|
97
|
+
skill, named `lingochunk-lesson`, `lingochunk-cards`, ...) in the client's
|
|
98
|
+
prompt/slash picker, and via the **`get_authoring_guide`** tool the model can
|
|
99
|
+
call itself before composing.
|
|
49
100
|
|
|
50
101
|
## Prerequisites
|
|
51
102
|
|
|
52
103
|
- Node.js >= 18.
|
|
53
104
|
- A LingoChunk **personal access token**: in LingoChunk, open Settings -> API
|
|
54
105
|
access, create a token, and grant the scopes you need (`vocab:read` +
|
|
55
|
-
`content:read` cover the read tools; add `cards:write`, `decks:export
|
|
56
|
-
`lessons:write` for the write
|
|
57
|
-
|
|
106
|
+
`content:read` cover the read tools; add `cards:write`, `decks:export`,
|
|
107
|
+
`lessons:write`, `translations:write` and `annotations:write` for the write
|
|
108
|
+
tools). The token is shown once and starts with `lcp_`. The 403 errors from
|
|
109
|
+
the tools name the exact scope you are missing.
|
|
58
110
|
|
|
59
111
|
## Use it
|
|
60
112
|
|
|
@@ -92,6 +144,36 @@ npm install # installs deps and builds dist/ via the prepare script
|
|
|
92
144
|
claude mcp add lingochunk --env LINGOCHUNK_TOKEN=lcp_... -- node /absolute/path/to/lingochunk-mcp/dist/index.js
|
|
93
145
|
```
|
|
94
146
|
|
|
147
|
+
### Option C - hosted server (no install: claude.ai, ChatGPT, Le Chat, ...)
|
|
148
|
+
|
|
149
|
+
The same server also runs hosted at `https://lingochunk.com/mcp` as a
|
|
150
|
+
standard **remote MCP server** (Streamable HTTP). Nothing to install - paste
|
|
151
|
+
the URL into any client that supports custom remote MCP servers and **sign in
|
|
152
|
+
with your LingoChunk account when the client prompts you** (OAuth 2.1 with a
|
|
153
|
+
consent screen; the grant appears in Settings -> API tokens, revocable like
|
|
154
|
+
any token). Clients with a token field can instead send a personal access
|
|
155
|
+
token as a Bearer credential:
|
|
156
|
+
|
|
157
|
+
| Client | Where |
|
|
158
|
+
|---|---|
|
|
159
|
+
| claude.ai (web/desktop/mobile, incl. Free) | Settings -> Connectors -> *Add custom connector* -> URL `https://lingochunk.com/mcp` -> a LingoChunk sign-in/consent screen opens; approve and you are connected |
|
|
160
|
+
| Claude Code | `claude mcp add --transport http lingochunk https://lingochunk.com/mcp` (OAuth on first use), or pass `--header "Authorization: Bearer lcp_..."` to use a token instead |
|
|
161
|
+
| ChatGPT (paid plans) | Settings -> enable *Developer mode* -> Apps -> "+" -> URL (OAuth), or URL + your token |
|
|
162
|
+
| Mistral Le Chat | *+ Add Connector* -> Custom MCP Connector -> URL (auth auto-detected) |
|
|
163
|
+
| Perplexity (Pro/Max), Grok, Manus | add a custom connector/MCP server by URL, sign in or paste an API key |
|
|
164
|
+
|
|
165
|
+
Differences from the local server: `get_audio_clip` is unavailable (it writes
|
|
166
|
+
files, which only makes sense on your own machine - use `get_audio_url`), and
|
|
167
|
+
skills don't auto-load (the server exposes each skill as an MCP **prompt**,
|
|
168
|
+
and agents can pull the same guidance through the `get_authoring_guide` tool).
|
|
169
|
+
|
|
170
|
+
**If your client offers neither OAuth nor a token field**, embed the token in
|
|
171
|
+
the URL: `https://lingochunk.com/mcp/t/lcp_...`. That URL then IS a
|
|
172
|
+
credential - treat it like a password, prefer a token with only the scopes you
|
|
173
|
+
need, and revoke it in Settings if it ever leaks. OAuth sign-in or a header,
|
|
174
|
+
when your client supports one, is always preferred (URLs end up in access
|
|
175
|
+
logs).
|
|
176
|
+
|
|
95
177
|
## Use with other agents
|
|
96
178
|
|
|
97
179
|
Nothing in the server is Claude-specific: it is a standard stdio MCP server,
|
|
@@ -201,6 +283,22 @@ when you want the update.
|
|
|
201
283
|
The token is only ever sent as an `Authorization: Bearer` header to the
|
|
202
284
|
configured origin; it is never written to disk or logged.
|
|
203
285
|
|
|
286
|
+
### Hosted (remote) mode
|
|
287
|
+
|
|
288
|
+
`node dist/index.js --http` serves the same tools over Streamable HTTP for
|
|
289
|
+
remote MCP clients (see Option C). The process is stateless and multi-user:
|
|
290
|
+
`LINGOCHUNK_TOKEN` is not read - every request must carry its own
|
|
291
|
+
`Authorization: Bearer lcp_...` header, which is forwarded verbatim to the
|
|
292
|
+
API (the API stays the sole authority; the server stores nothing). A
|
|
293
|
+
`Dockerfile` is included.
|
|
294
|
+
|
|
295
|
+
| Variable | Required | Default | Meaning |
|
|
296
|
+
|---|---|---|---|
|
|
297
|
+
| `LINGOCHUNK_MCP_PORT` | no | `8100` | Listen port (`PORT` honoured as fallback). |
|
|
298
|
+
| `LINGOCHUNK_BASE_URL` | no | `https://lingochunk.com` | API origin; co-located deployments should point at the loopback origin. |
|
|
299
|
+
|
|
300
|
+
Endpoints: `POST /mcp` (the MCP wire), `GET /health` (liveness).
|
|
301
|
+
|
|
204
302
|
## Building a lesson
|
|
205
303
|
|
|
206
304
|
Ask your agent something like "build me a lesson from yesterday's German episode"
|
|
@@ -217,10 +315,15 @@ lessons side by side under the episode's Lessons tab. See
|
|
|
217
315
|
|
|
218
316
|
```
|
|
219
317
|
src/ the MCP server (TypeScript, stdio)
|
|
318
|
+
skills/lingochunk-overview/ the "what can I do?" tour skill
|
|
220
319
|
skills/lingochunk-lesson/ the coursebook lesson skill
|
|
320
|
+
skills/lingochunk-course/ the multi-lesson course planner skill
|
|
221
321
|
skills/lingochunk-cards/ the flashcard (card.v1) skill
|
|
222
322
|
skills/lingochunk-discuss/ the "discuss an episode" skill
|
|
323
|
+
skills/lingochunk-add-language/ the add-language / draft-translation skill
|
|
324
|
+
skills/lingochunk-annotate/ the useful-expression annotation skill
|
|
223
325
|
skills/*/examples/ example lesson.v1 documents (CI-validated)
|
|
326
|
+
docs/creator-guide.md the guide for content creators (start here if you make audio)
|
|
224
327
|
docs/skill-authoring.md how to write a new skill
|
|
225
328
|
docs/skill-template.md SKILL.md starting point
|
|
226
329
|
docs/integrations/fluent.md how to plug this into the fluent tutor plugin
|
|
@@ -253,7 +356,7 @@ npm run validate:lesson -- <doc.json> # validate a lesson.v1 document (Node 22
|
|
|
253
356
|
|
|
254
357
|
`spec/openapi-public-v1.json` is the source contract; it is exported from the
|
|
255
358
|
LingoChunk repo (`make generate-openapi-public`) and refreshed here on each API
|
|
256
|
-
release. This copy was taken from LingoChunk commit `
|
|
359
|
+
release. This copy was taken from LingoChunk commit `31c47289`.
|
|
257
360
|
|
|
258
361
|
### Live smoke test
|
|
259
362
|
|
package/dist/client.d.ts
CHANGED
|
@@ -24,6 +24,139 @@ export interface DeckExportStatus {
|
|
|
24
24
|
status: "ready" | "pending" | "failed" | "none";
|
|
25
25
|
download_url?: string;
|
|
26
26
|
}
|
|
27
|
+
/** One target language present on a submission's fan-out group. */
|
|
28
|
+
export interface SubmissionLanguage {
|
|
29
|
+
language: string;
|
|
30
|
+
submission_id: string;
|
|
31
|
+
status: string;
|
|
32
|
+
is_primary: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** An in-progress agent-supplied translation draft for one language. */
|
|
35
|
+
export interface DraftSummary {
|
|
36
|
+
language: string;
|
|
37
|
+
sentences_drafted: number;
|
|
38
|
+
sentence_count: number;
|
|
39
|
+
}
|
|
40
|
+
/** Result of GET /submissions/{id}/languages. */
|
|
41
|
+
export interface SubmissionLanguages {
|
|
42
|
+
source_language: string;
|
|
43
|
+
languages: SubmissionLanguage[];
|
|
44
|
+
/** Ordinary Groq targets addable via add_language (source + existing removed). */
|
|
45
|
+
available_targets: string[];
|
|
46
|
+
/** Leveled same-language codes (e.g. de-a2) for the draft flow only. */
|
|
47
|
+
simplify_targets: string[];
|
|
48
|
+
drafts: DraftSummary[];
|
|
49
|
+
}
|
|
50
|
+
/** One source token as the Groq translator would see it. */
|
|
51
|
+
export interface SourceToken {
|
|
52
|
+
surface: string;
|
|
53
|
+
lemma: string;
|
|
54
|
+
pos: string;
|
|
55
|
+
pivot_meaning: string;
|
|
56
|
+
}
|
|
57
|
+
/** One source sentence to translate, with its pivot-language gloss. */
|
|
58
|
+
export interface SourceSentence {
|
|
59
|
+
position: number;
|
|
60
|
+
text: string;
|
|
61
|
+
pivot_translation: string;
|
|
62
|
+
tokens: SourceToken[];
|
|
63
|
+
}
|
|
64
|
+
/** Result of GET /submissions/{id}/translation-source (one page). */
|
|
65
|
+
export interface TranslationSource {
|
|
66
|
+
source_language: string;
|
|
67
|
+
pivot_language: string;
|
|
68
|
+
sentence_count: number;
|
|
69
|
+
sentences: SourceSentence[];
|
|
70
|
+
/** null when the page exhausts the submission. */
|
|
71
|
+
next_from_position: number | null;
|
|
72
|
+
}
|
|
73
|
+
/** Result of POST /submissions/{id}/languages (Groq fan-out trigger). */
|
|
74
|
+
export interface AddLanguagesResult {
|
|
75
|
+
jobs: {
|
|
76
|
+
language: string;
|
|
77
|
+
job_id: string;
|
|
78
|
+
}[];
|
|
79
|
+
skipped: {
|
|
80
|
+
language: string;
|
|
81
|
+
reason: string;
|
|
82
|
+
}[];
|
|
83
|
+
}
|
|
84
|
+
/** One draft sentence in a PUT batch: whole-sentence target text (null = no
|
|
85
|
+
* sentence back) plus one meaning per source token, in order. */
|
|
86
|
+
export interface DraftSentence {
|
|
87
|
+
position: number;
|
|
88
|
+
translation: string | null;
|
|
89
|
+
meanings: string[];
|
|
90
|
+
}
|
|
91
|
+
/** Result of PUT /submissions/{id}/translations/{language}. */
|
|
92
|
+
export interface PutTranslationsResult {
|
|
93
|
+
accepted: number;
|
|
94
|
+
rejected: {
|
|
95
|
+
position: number;
|
|
96
|
+
reason: string;
|
|
97
|
+
expected?: number;
|
|
98
|
+
got?: number;
|
|
99
|
+
}[];
|
|
100
|
+
sentences_drafted: number;
|
|
101
|
+
sentence_count: number;
|
|
102
|
+
}
|
|
103
|
+
/** Result of POST /submissions/{id}/translations/{language}/commit. */
|
|
104
|
+
export interface CommitDraftResult {
|
|
105
|
+
job_id: string;
|
|
106
|
+
language: string;
|
|
107
|
+
}
|
|
108
|
+
/** Result of GET /jobs/{id}: status of a fan-out or draft-apply job. */
|
|
109
|
+
export interface JobStatus {
|
|
110
|
+
status: string;
|
|
111
|
+
progress?: number;
|
|
112
|
+
step?: string | null;
|
|
113
|
+
error?: string | null;
|
|
114
|
+
}
|
|
115
|
+
/** Result of DELETE /submissions/{id}/translations/{language}. */
|
|
116
|
+
export interface DeleteDraftResult {
|
|
117
|
+
deleted_sentences: number;
|
|
118
|
+
}
|
|
119
|
+
/** One creator annotation: a markdown note anchored to a transcript sentence,
|
|
120
|
+
* tinted onto a char span or the whole sentence. */
|
|
121
|
+
export interface AnnotationV1 {
|
|
122
|
+
id: number;
|
|
123
|
+
/** The sentence's stable id (survives split/merge), from get_transcript. */
|
|
124
|
+
sentence_id: number;
|
|
125
|
+
/** null (with char_end) for a whole-sentence note; else a code-point offset. */
|
|
126
|
+
char_start: number | null;
|
|
127
|
+
char_end: number | null;
|
|
128
|
+
/** Server snapshot of display[char_start:char_end]; verify it after create. */
|
|
129
|
+
selected_text: string | null;
|
|
130
|
+
note: string;
|
|
131
|
+
/** The sentence was edited since; the app hides the tint until re-anchored. */
|
|
132
|
+
stale: boolean;
|
|
133
|
+
start_time: number | null;
|
|
134
|
+
end_time: number | null;
|
|
135
|
+
}
|
|
136
|
+
/** Result of GET /submissions/{id}/annotations. */
|
|
137
|
+
export interface AnnotationList {
|
|
138
|
+
/** Ordered by sentence, then char_start. */
|
|
139
|
+
annotations: AnnotationV1[];
|
|
140
|
+
count: number;
|
|
141
|
+
/** The per-submission cap, so an agent can budget how many more to add. */
|
|
142
|
+
max_annotations: number;
|
|
143
|
+
}
|
|
144
|
+
/** Body of POST /submissions/{id}/annotations. char_start/char_end are
|
|
145
|
+
* both-or-neither (neither = a whole-sentence note); offsets are Unicode
|
|
146
|
+
* code points into the sentence's display. */
|
|
147
|
+
export interface CreateAnnotationBody {
|
|
148
|
+
sentence_id: number;
|
|
149
|
+
char_start?: number | null;
|
|
150
|
+
char_end?: number | null;
|
|
151
|
+
note: string;
|
|
152
|
+
start_time?: number | null;
|
|
153
|
+
end_time?: number | null;
|
|
154
|
+
}
|
|
155
|
+
/** Result of DELETE /submissions/{id}/annotations/{annotation_id}. */
|
|
156
|
+
export interface DeleteAnnotationResult {
|
|
157
|
+
deleted: boolean;
|
|
158
|
+
annotation_id: number;
|
|
159
|
+
}
|
|
27
160
|
export type QueryValue = string | number | boolean | undefined | null;
|
|
28
161
|
/** How long any single request may take before we abort it. */
|
|
29
162
|
export declare const REQUEST_TIMEOUT_MS = 30000;
|
|
@@ -49,10 +182,21 @@ export declare class LingoChunkClient {
|
|
|
49
182
|
private raiseForStatus;
|
|
50
183
|
/** GET a JSON endpoint. */
|
|
51
184
|
private getJson;
|
|
185
|
+
/** GET a JSON endpoint, also capturing one response header (e.g. the
|
|
186
|
+
* lesson optimistic-concurrency token in X-Lesson-Version). */
|
|
187
|
+
private getJsonWithHeader;
|
|
188
|
+
/** PUT a JSON body, also capturing one response header. */
|
|
189
|
+
private putJsonWithHeader;
|
|
52
190
|
/** DELETE an endpoint; success is 204 with no body to parse. */
|
|
53
191
|
private deleteNoContent;
|
|
54
192
|
/** POST a JSON body to an endpoint and parse the JSON response. */
|
|
55
193
|
private postJson;
|
|
194
|
+
/** PUT a JSON body to an endpoint and parse the JSON response. */
|
|
195
|
+
private putJson;
|
|
196
|
+
/** PATCH a JSON body to an endpoint and parse the JSON response. */
|
|
197
|
+
private patchJson;
|
|
198
|
+
/** DELETE an endpoint that returns a JSON body (unlike deleteNoContent). */
|
|
199
|
+
private deleteJson;
|
|
56
200
|
getVocabulary(params: Record<string, QueryValue>): Promise<unknown>;
|
|
57
201
|
lookupWord(params: Record<string, QueryValue>): Promise<unknown>;
|
|
58
202
|
listLibrary(params: Record<string, QueryValue>): Promise<unknown>;
|
|
@@ -62,6 +206,35 @@ export declare class LingoChunkClient {
|
|
|
62
206
|
listDecks(): Promise<unknown>;
|
|
63
207
|
addCard(body: object): Promise<unknown>;
|
|
64
208
|
createLesson(body: object): Promise<unknown>;
|
|
209
|
+
/** Dry-run validate a lesson.v1 document without saving it: reports every
|
|
210
|
+
* failing category at once (schema + reference), so a document can be fixed
|
|
211
|
+
* in one pass instead of save -> 400 -> fix cycles. Stores nothing. */
|
|
212
|
+
validateLesson(body: object): Promise<unknown>;
|
|
213
|
+
listLessons(params: Record<string, QueryValue>): Promise<unknown>;
|
|
214
|
+
/** Create a course: a named, ordered series to file lessons under. */
|
|
215
|
+
createCourse(body: object): Promise<unknown>;
|
|
216
|
+
/** The caller's courses, newest first, each with its lesson_count. */
|
|
217
|
+
listCourses(): Promise<unknown>;
|
|
218
|
+
/** Delete a course (owner-scoped). Its lessons survive - the DB sets their
|
|
219
|
+
* course_id NULL, un-grouping them. 404 for a foreign or unknown id. */
|
|
220
|
+
deleteCourse(courseId: string): Promise<void>;
|
|
221
|
+
/** The stored lesson.v1 document plus its optimistic-concurrency token
|
|
222
|
+
* (the X-Lesson-Version response header; a caller echoes it verbatim as
|
|
223
|
+
* an update's base_version). Owner-scoped server-side; 404 for HTML
|
|
224
|
+
* lessons (they have no document) and for foreign/unknown ids. */
|
|
225
|
+
getLessonDocument(lessonId: string): Promise<{
|
|
226
|
+
version: string | null;
|
|
227
|
+
document: unknown;
|
|
228
|
+
}>;
|
|
229
|
+
/** Replace a lesson.v1 document in place (owner-scoped; same id, same
|
|
230
|
+
* app_url). base_version is the X-Lesson-Version token the edit is based
|
|
231
|
+
* on - if the lesson changed since, the server answers 409 stale_document
|
|
232
|
+
* instead of overwriting the other writer. Returns the refreshed lesson
|
|
233
|
+
* metadata plus the NEW token for chained edits. */
|
|
234
|
+
updateLesson(lessonId: string, document: unknown, baseVersion: string): Promise<{
|
|
235
|
+
version: string | null;
|
|
236
|
+
lesson: unknown;
|
|
237
|
+
}>;
|
|
65
238
|
/** Owner-scoped server-side: a foreign or unknown id is a 404, never a leak. */
|
|
66
239
|
deleteLesson(lessonId: string): Promise<void>;
|
|
67
240
|
/** Start an Anki export (no body). 400 for a deck with no linked submission. */
|
|
@@ -69,4 +242,38 @@ export declare class LingoChunkClient {
|
|
|
69
242
|
exportDeckStatus(deckId: number): Promise<DeckExportStatus>;
|
|
70
243
|
/** GET a clip as raw audio bytes (the endpoint streams audio, not JSON). */
|
|
71
244
|
getAudioClip(submissionId: string, start: number, end: number): Promise<AudioClip>;
|
|
245
|
+
/** (a) The submission's fan-out group languages, addable targets, leveled
|
|
246
|
+
* simplify targets, and in-progress drafts. */
|
|
247
|
+
listSubmissionLanguages(submissionId: string): Promise<SubmissionLanguages>;
|
|
248
|
+
/** (b) A page of the primary's sentences (source text + pivot glosses) as the
|
|
249
|
+
* Groq translator would see them, for the agent to translate. */
|
|
250
|
+
getTranslationSource(submissionId: string, params: Record<string, QueryValue>): Promise<TranslationSource>;
|
|
251
|
+
/** (c) Trigger the server-side Groq fan-out into extra ordinary target
|
|
252
|
+
* languages; returns a job per accepted language plus per-code skips. */
|
|
253
|
+
addLanguages(submissionId: string, languages: string[]): Promise<AddLanguagesResult>;
|
|
254
|
+
/** (d) Upsert a batch of agent-supplied draft sentences for one language. */
|
|
255
|
+
putTranslations(submissionId: string, language: string, body: {
|
|
256
|
+
generator?: string;
|
|
257
|
+
sentences: DraftSentence[];
|
|
258
|
+
}): Promise<PutTranslationsResult>;
|
|
259
|
+
/** (e) Validate a complete draft and enqueue the apply job that mints the
|
|
260
|
+
* sibling submission. 409 when the draft misses sentence positions. */
|
|
261
|
+
commitTranslationDraft(submissionId: string, language: string): Promise<CommitDraftResult>;
|
|
262
|
+
/** (f) Owner-scoped job status, for polling a fan-out (c) or draft-apply (e)
|
|
263
|
+
* job to completion. */
|
|
264
|
+
getJob(jobId: string): Promise<JobStatus>;
|
|
265
|
+
/** (g) Delete the draft rows for one language (never a committed sibling). */
|
|
266
|
+
deleteTranslationDraft(submissionId: string, language: string): Promise<DeleteDraftResult>;
|
|
267
|
+
/** List a submission's creator annotations (ordered), with the per-submission
|
|
268
|
+
* cap so an agent can budget how many more to add. */
|
|
269
|
+
listAnnotations(submissionId: string): Promise<AnnotationList>;
|
|
270
|
+
/** Create one annotation on a sentence span (or the whole sentence). The
|
|
271
|
+
* response echoes the server's selected_text snapshot so the caller can
|
|
272
|
+
* verify the span it anchored. */
|
|
273
|
+
createAnnotation(submissionId: string, body: CreateAnnotationBody): Promise<AnnotationV1>;
|
|
274
|
+
/** Replace one annotation's markdown note (anchor unchanged; staleness is
|
|
275
|
+
* recomputed against the current sentence). */
|
|
276
|
+
updateAnnotation(submissionId: string, annotationId: number, note: string): Promise<AnnotationV1>;
|
|
277
|
+
/** Delete one annotation. Returns {deleted, annotation_id}. */
|
|
278
|
+
deleteAnnotation(submissionId: string, annotationId: number): Promise<DeleteAnnotationResult>;
|
|
72
279
|
}
|
package/dist/client.js
CHANGED
|
@@ -134,6 +134,34 @@ export class LingoChunkClient {
|
|
|
134
134
|
}
|
|
135
135
|
return (await res.json());
|
|
136
136
|
}
|
|
137
|
+
/** GET a JSON endpoint, also capturing one response header (e.g. the
|
|
138
|
+
* lesson optimistic-concurrency token in X-Lesson-Version). */
|
|
139
|
+
async getJsonWithHeader(path, header) {
|
|
140
|
+
const res = await fetch(this.buildUrl(path), {
|
|
141
|
+
method: "GET",
|
|
142
|
+
headers: this.authHeaders("application/json"),
|
|
143
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
await this.raiseForStatus(res);
|
|
147
|
+
}
|
|
148
|
+
return { data: (await res.json()), header: res.headers.get(header) };
|
|
149
|
+
}
|
|
150
|
+
/** PUT a JSON body, also capturing one response header. */
|
|
151
|
+
async putJsonWithHeader(path, body, header) {
|
|
152
|
+
const headers = this.authHeaders("application/json");
|
|
153
|
+
headers["Content-Type"] = "application/json";
|
|
154
|
+
const res = await fetch(this.buildUrl(path), {
|
|
155
|
+
method: "PUT",
|
|
156
|
+
headers,
|
|
157
|
+
body: JSON.stringify(body),
|
|
158
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
159
|
+
});
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
await this.raiseForStatus(res);
|
|
162
|
+
}
|
|
163
|
+
return { data: (await res.json()), header: res.headers.get(header) };
|
|
164
|
+
}
|
|
137
165
|
/** DELETE an endpoint; success is 204 with no body to parse. */
|
|
138
166
|
async deleteNoContent(path) {
|
|
139
167
|
const res = await fetch(this.buildUrl(path), {
|
|
@@ -161,6 +189,48 @@ export class LingoChunkClient {
|
|
|
161
189
|
}
|
|
162
190
|
return (await res.json());
|
|
163
191
|
}
|
|
192
|
+
/** PUT a JSON body to an endpoint and parse the JSON response. */
|
|
193
|
+
async putJson(path, body) {
|
|
194
|
+
const headers = this.authHeaders("application/json");
|
|
195
|
+
headers["Content-Type"] = "application/json";
|
|
196
|
+
const res = await fetch(this.buildUrl(path), {
|
|
197
|
+
method: "PUT",
|
|
198
|
+
headers,
|
|
199
|
+
body: JSON.stringify(body),
|
|
200
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
201
|
+
});
|
|
202
|
+
if (!res.ok) {
|
|
203
|
+
await this.raiseForStatus(res);
|
|
204
|
+
}
|
|
205
|
+
return (await res.json());
|
|
206
|
+
}
|
|
207
|
+
/** PATCH a JSON body to an endpoint and parse the JSON response. */
|
|
208
|
+
async patchJson(path, body) {
|
|
209
|
+
const headers = this.authHeaders("application/json");
|
|
210
|
+
headers["Content-Type"] = "application/json";
|
|
211
|
+
const res = await fetch(this.buildUrl(path), {
|
|
212
|
+
method: "PATCH",
|
|
213
|
+
headers,
|
|
214
|
+
body: JSON.stringify(body),
|
|
215
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
216
|
+
});
|
|
217
|
+
if (!res.ok) {
|
|
218
|
+
await this.raiseForStatus(res);
|
|
219
|
+
}
|
|
220
|
+
return (await res.json());
|
|
221
|
+
}
|
|
222
|
+
/** DELETE an endpoint that returns a JSON body (unlike deleteNoContent). */
|
|
223
|
+
async deleteJson(path) {
|
|
224
|
+
const res = await fetch(this.buildUrl(path), {
|
|
225
|
+
method: "DELETE",
|
|
226
|
+
headers: this.authHeaders("application/json"),
|
|
227
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
228
|
+
});
|
|
229
|
+
if (!res.ok) {
|
|
230
|
+
await this.raiseForStatus(res);
|
|
231
|
+
}
|
|
232
|
+
return (await res.json());
|
|
233
|
+
}
|
|
164
234
|
// --- Read endpoints (thin pass-throughs; the tools shape the arguments) ---
|
|
165
235
|
getVocabulary(params) {
|
|
166
236
|
return this.getJson("/vocab", params);
|
|
@@ -190,6 +260,45 @@ export class LingoChunkClient {
|
|
|
190
260
|
createLesson(body) {
|
|
191
261
|
return this.postJson("/lessons", body);
|
|
192
262
|
}
|
|
263
|
+
/** Dry-run validate a lesson.v1 document without saving it: reports every
|
|
264
|
+
* failing category at once (schema + reference), so a document can be fixed
|
|
265
|
+
* in one pass instead of save -> 400 -> fix cycles. Stores nothing. */
|
|
266
|
+
validateLesson(body) {
|
|
267
|
+
return this.postJson("/lessons/validate", body);
|
|
268
|
+
}
|
|
269
|
+
listLessons(params) {
|
|
270
|
+
return this.getJson("/lessons", params);
|
|
271
|
+
}
|
|
272
|
+
/** Create a course: a named, ordered series to file lessons under. */
|
|
273
|
+
createCourse(body) {
|
|
274
|
+
return this.postJson("/courses", body);
|
|
275
|
+
}
|
|
276
|
+
/** The caller's courses, newest first, each with its lesson_count. */
|
|
277
|
+
listCourses() {
|
|
278
|
+
return this.getJson("/courses");
|
|
279
|
+
}
|
|
280
|
+
/** Delete a course (owner-scoped). Its lessons survive - the DB sets their
|
|
281
|
+
* course_id NULL, un-grouping them. 404 for a foreign or unknown id. */
|
|
282
|
+
deleteCourse(courseId) {
|
|
283
|
+
return this.deleteNoContent(`/courses/${encodeURIComponent(courseId)}`);
|
|
284
|
+
}
|
|
285
|
+
/** The stored lesson.v1 document plus its optimistic-concurrency token
|
|
286
|
+
* (the X-Lesson-Version response header; a caller echoes it verbatim as
|
|
287
|
+
* an update's base_version). Owner-scoped server-side; 404 for HTML
|
|
288
|
+
* lessons (they have no document) and for foreign/unknown ids. */
|
|
289
|
+
async getLessonDocument(lessonId) {
|
|
290
|
+
const { data, header } = await this.getJsonWithHeader(`/lessons/${encodeURIComponent(lessonId)}/document`, "x-lesson-version");
|
|
291
|
+
return { version: header, document: data };
|
|
292
|
+
}
|
|
293
|
+
/** Replace a lesson.v1 document in place (owner-scoped; same id, same
|
|
294
|
+
* app_url). base_version is the X-Lesson-Version token the edit is based
|
|
295
|
+
* on - if the lesson changed since, the server answers 409 stale_document
|
|
296
|
+
* instead of overwriting the other writer. Returns the refreshed lesson
|
|
297
|
+
* metadata plus the NEW token for chained edits. */
|
|
298
|
+
async updateLesson(lessonId, document, baseVersion) {
|
|
299
|
+
const { data, header } = await this.putJsonWithHeader(`/lessons/${encodeURIComponent(lessonId)}/document`, { document, base_version: baseVersion }, "x-lesson-version");
|
|
300
|
+
return { version: header, lesson: data };
|
|
301
|
+
}
|
|
193
302
|
/** Owner-scoped server-side: a foreign or unknown id is a 404, never a leak. */
|
|
194
303
|
deleteLesson(lessonId) {
|
|
195
304
|
return this.deleteNoContent(`/lessons/${encodeURIComponent(lessonId)}`);
|
|
@@ -226,5 +335,60 @@ export class LingoChunkClient {
|
|
|
226
335
|
const contentType = res.headers.get("content-type") ?? "audio/mpeg";
|
|
227
336
|
return { data, contentType };
|
|
228
337
|
}
|
|
338
|
+
// --- Language / translation endpoints (phase 4) -------------------------
|
|
339
|
+
/** (a) The submission's fan-out group languages, addable targets, leveled
|
|
340
|
+
* simplify targets, and in-progress drafts. */
|
|
341
|
+
listSubmissionLanguages(submissionId) {
|
|
342
|
+
return this.getJson(`/submissions/${encodeURIComponent(submissionId)}/languages`);
|
|
343
|
+
}
|
|
344
|
+
/** (b) A page of the primary's sentences (source text + pivot glosses) as the
|
|
345
|
+
* Groq translator would see them, for the agent to translate. */
|
|
346
|
+
getTranslationSource(submissionId, params) {
|
|
347
|
+
return this.getJson(`/submissions/${encodeURIComponent(submissionId)}/translation-source`, params);
|
|
348
|
+
}
|
|
349
|
+
/** (c) Trigger the server-side Groq fan-out into extra ordinary target
|
|
350
|
+
* languages; returns a job per accepted language plus per-code skips. */
|
|
351
|
+
addLanguages(submissionId, languages) {
|
|
352
|
+
return this.postJson(`/submissions/${encodeURIComponent(submissionId)}/languages`, { languages });
|
|
353
|
+
}
|
|
354
|
+
/** (d) Upsert a batch of agent-supplied draft sentences for one language. */
|
|
355
|
+
putTranslations(submissionId, language, body) {
|
|
356
|
+
return this.putJson(`/submissions/${encodeURIComponent(submissionId)}/translations/${encodeURIComponent(language)}`, body);
|
|
357
|
+
}
|
|
358
|
+
/** (e) Validate a complete draft and enqueue the apply job that mints the
|
|
359
|
+
* sibling submission. 409 when the draft misses sentence positions. */
|
|
360
|
+
commitTranslationDraft(submissionId, language) {
|
|
361
|
+
return this.postJson(`/submissions/${encodeURIComponent(submissionId)}/translations/${encodeURIComponent(language)}/commit`);
|
|
362
|
+
}
|
|
363
|
+
/** (f) Owner-scoped job status, for polling a fan-out (c) or draft-apply (e)
|
|
364
|
+
* job to completion. */
|
|
365
|
+
getJob(jobId) {
|
|
366
|
+
return this.getJson(`/jobs/${encodeURIComponent(jobId)}`);
|
|
367
|
+
}
|
|
368
|
+
/** (g) Delete the draft rows for one language (never a committed sibling). */
|
|
369
|
+
deleteTranslationDraft(submissionId, language) {
|
|
370
|
+
return this.deleteJson(`/submissions/${encodeURIComponent(submissionId)}/translations/${encodeURIComponent(language)}`);
|
|
371
|
+
}
|
|
372
|
+
// --- Creator annotation endpoints (phase 5) -----------------------------
|
|
373
|
+
/** List a submission's creator annotations (ordered), with the per-submission
|
|
374
|
+
* cap so an agent can budget how many more to add. */
|
|
375
|
+
listAnnotations(submissionId) {
|
|
376
|
+
return this.getJson(`/submissions/${encodeURIComponent(submissionId)}/annotations`);
|
|
377
|
+
}
|
|
378
|
+
/** Create one annotation on a sentence span (or the whole sentence). The
|
|
379
|
+
* response echoes the server's selected_text snapshot so the caller can
|
|
380
|
+
* verify the span it anchored. */
|
|
381
|
+
createAnnotation(submissionId, body) {
|
|
382
|
+
return this.postJson(`/submissions/${encodeURIComponent(submissionId)}/annotations`, body);
|
|
383
|
+
}
|
|
384
|
+
/** Replace one annotation's markdown note (anchor unchanged; staleness is
|
|
385
|
+
* recomputed against the current sentence). */
|
|
386
|
+
updateAnnotation(submissionId, annotationId, note) {
|
|
387
|
+
return this.patchJson(`/submissions/${encodeURIComponent(submissionId)}/annotations/${annotationId}`, { note });
|
|
388
|
+
}
|
|
389
|
+
/** Delete one annotation. Returns {deleted, annotation_id}. */
|
|
390
|
+
deleteAnnotation(submissionId, annotationId) {
|
|
391
|
+
return this.deleteJson(`/submissions/${encodeURIComponent(submissionId)}/annotations/${annotationId}`);
|
|
392
|
+
}
|
|
229
393
|
}
|
|
230
394
|
//# sourceMappingURL=client.js.map
|