@kolbo/mcp 1.73.0 → 1.74.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.73.0",
3
+ "version": "1.74.0",
4
4
  "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  # AUTO-GENERATED — do not edit
2
2
 
3
- This tree is mirrored from kolbo-code@6d0fcfe, the single source of truth.
3
+ This tree is mirrored from kolbo-code@d0d959b, the single source of truth.
4
4
  Canonical source: packages/opencode/skills/kolbo/
5
5
  Distribution: .github/workflows/sync-skill-to-plugin.yml
6
6
 
package/skill/SKILL.md CHANGED
@@ -222,6 +222,7 @@ Full tables + formulas in `references/workflows/cost-and-validation.md`. Quick r
222
222
  - **Otherwise confirm** via the labeled-question card: the parameters + the credit cost, suggest a cheaper alternative if one fits, wait for the user's pick. Never fire on defaults the user didn't choose.
223
223
  - **Batch totalling 100+ credits**: run `check_credits` first.
224
224
  - **Quote real cost**: after firing, log `credits_used` (from the tool result) to `.kolbo/production.md` — never `base × count`.
225
+ - **Video/lipsync `credit` is per-SECOND, not per-clip**: `total = credit × duration`. This is the universal rule for video/firstlast/elements/motion_graphic/cast types, not a per-model exception — `list_models` states it inline now. The one carve-out is a model with `flat_credit_by_resolution` set.
225
226
  - **Never state "credits remaining" from arithmetic** (opening balance − generation costs). Coding/chat usage deducts credits too, so the math is always wrong. Report cost only; if the user asks for their balance, call `check_credits` fresh at that moment.
226
227
 
227
228
  ## Rate Limiting & Batch Generation
@@ -112,7 +112,6 @@ The match is **literal and case-insensitive**, so:
112
112
  - The `@name` must equal the stored `name` field (e.g. if `name: "esther_model"` → write `@esther_model`, not `@Esther`, not `@אסתר`, not `@the model`).
113
113
  - Any-language characters are supported — if the DNA was created with `name: "אסתר"` you write `@אסתר`. Use the EXACT stored string.
114
114
  - Mentions terminate at punctuation (`.,!?`), double-spaces, another `@`, or end of string. So `@maya, wearing...` matches `maya`.
115
- - **Never glue a possessive onto the tag.** Write `the face of @maya stays stable`, not `@maya's face stays stable` — the tag must end at the stored name. kolbo-api does strip a trailing `'s` / `’s` before the lookup, so a possessive no longer loses the DNA, but the apostrophe is left sitting in the prompt text the model sees.
116
115
 
117
116
  This composes with `@image1` / `@image2` positional tags for plain reference/source images — see "Reference Tagging" below.
118
117
 
@@ -70,7 +70,8 @@ var TOOL_TITLES = {
70
70
  generate_first_last_frame: 'First–Last Frame', generate_lipsync: 'Lipsync',
71
71
  generate_music: 'Music Generation', generate_speech: 'Text to Speech',
72
72
  generate_sound: 'Sound Effect', generate_3d: '3D Generation',
73
- generate_creative_director: 'Creative Director', edit_image: 'Image Edit', edit_video: 'Video Edit'
73
+ generate_creative_director: 'Creative Director', edit_image: 'Image Edit', edit_video: 'Video Edit',
74
+ get_generation_status: 'Generations'
74
75
  };
75
76
 
76
77
  // Long text is clamped by CSS (.k-prompt 2 lines / .k-caption 1 line). When it
@@ -591,6 +592,7 @@ function renderResult(sc) {
591
592
  // voice that actually ran, so the finished card must not keep the guess.
592
593
  renderChips(sc);
593
594
  setPhaseChip('', false);
595
+ if (sc.kind === 'status' && Array.isArray(sc.items)) return renderStatusGrid(sc);
594
596
  if (sc.batch && sc.scenes && sc.scenes.length) return renderBatchGrid(sc);
595
597
  if (sc.kind === 'scenes' && sc.scenes && sc.scenes.length) return renderScenes(sc);
596
598
  var urls = sc.urls || [];
@@ -773,6 +775,49 @@ function renderBatchGrid(sc) {
773
775
  window.kolbo.notifySize();
774
776
  }
775
777
 
778
+ // get_generation_status checking SEVERAL ids in one call (the "run these N
779
+ // generations in parallel, then check on all of them" pattern). Each item is
780
+ // independently image/video/audio and independently pending/completed/failed
781
+ // — a single generation-in-progress tile grid can't express that. Completed
782
+ // items get a real thumbnail (same tile markup as renderBatchGrid, so the two
783
+ // grids read as one visual language); pending/failed items get the same
784
+ // spinner/error badge language renderGenerating/renderError already use, so
785
+ // nothing here is a new visual pattern, only a new combination of them.
786
+ function renderStatusGrid(sc) {
787
+ var items = sc.items;
788
+ if (!items.length) return renderError('No results');
789
+ el('stage').innerHTML = '<div class="k-gen-grid n' + Math.min(items.length, 4) + '">' +
790
+ items.map(function (it, i) {
791
+ var cap = it.title ? '<span class="k-skel-cap" title="' + esc(it.title) + '">' + esc(it.title) + '</span>' : '';
792
+ if (it.state === 'completed' && it.url) {
793
+ // Classify by extension, same as every other reference/thumb in this
794
+ // file — the tool only knows a url came back, not what kind it is.
795
+ it.kind = refKind(it.url, 'image');
796
+ var shape = it.kind === 'video' ? 'video' : 'square';
797
+ return '<div class="k-skel done ' + shape + '" data-focus="' + i + '">' +
798
+ (it.kind === 'video'
799
+ ? '<video class="k-cell-fill" src="' + esc(it.url) + '" controls playsinline preload="metadata"></video>'
800
+ : it.kind === 'audio'
801
+ ? '<div class="k-cell-fill" style="display:flex;align-items:center;justify-content:center">' + ICONS.sound + '</div>'
802
+ : '<img class="k-cell-fill" src="' + esc(it.url) + '" alt="" loading="lazy" style="cursor:zoom-in">') +
803
+ cap + dlBtnHTML(it.url) + '</div>';
804
+ }
805
+ var failed = it.state === 'failed' || it.state === 'cancelled';
806
+ var badge = failed
807
+ ? '<span class="k-gen-badge" style="background:var(--error,#e5484d)"><span aria-hidden="true">✕</span>' + esc(it.state) + '</span>'
808
+ : '<span class="k-gen-badge"><span class="k-spin"></span>' + esc(it.state || 'processing') + '</span>';
809
+ return '<div class="k-skel square">' + badge + cap + '</div>';
810
+ }).join('') + '</div>';
811
+ wireDlButtons(el('stage'));
812
+ Array.prototype.forEach.call(el('stage').querySelectorAll('[data-focus]'), function (cell) {
813
+ var it = items[+cell.getAttribute('data-focus')];
814
+ if (it.kind !== 'image') return; // <video controls> owns its own clicks
815
+ cell.onclick = function () { focusMedia(it.url); };
816
+ });
817
+ renderActions(sc);
818
+ window.kolbo.notifySize();
819
+ }
820
+
776
821
  function renderScenes(sc) {
777
822
  var items = sceneItems(sc);
778
823
  if (!items.length) return renderError('No completed scenes received');
@@ -650,6 +650,11 @@ async function uiCompleted(p, textPayload, extraContent) {
650
650
  title: p.title,
651
651
  duration: p.duration,
652
652
  scenes: p.scenes,
653
+ // Independent per-item results (get_generation_status checking several ids
654
+ // at once) — each one carries its OWN state/media, unlike `scenes`/`urls`
655
+ // above which assume everything finished together. Only set when the
656
+ // caller actually has this shape; every existing caller is unaffected.
657
+ ...(Array.isArray(p.items) ? { items: p.items } : {}),
653
658
  credits_used: p.credits_used,
654
659
  open_url: buildOpenUrl(p.tool, p.gen),
655
660
  };
@@ -978,18 +978,45 @@ function registerGenerateTools(server, client, options = {}) {
978
978
  };
979
979
  }
980
980
 
981
+ const text = JSON.stringify({
982
+ all_done: pending.length === 0,
983
+ completed: results.filter(r => r.state === 'completed').length,
984
+ failed: results.filter(r => r.state === 'failed' || r.state === 'cancelled').length,
985
+ still_processing: pending.map(r => r.generation_id),
986
+ _hint: pending.length === 0 ? doneHint : pendingHint,
987
+ generations: results
988
+ }, null, 2);
989
+
990
+ // Checking SEVERAL ids in one call — the "run N generations in parallel,
991
+ // then check on all of them" pattern — is the one shape hasGeneratedOutput
992
+ // (the host's card-selection check) cannot see into: media here sits one
993
+ // level under EACH ARRAY ITEM's own `result`, and nothing walks arrays
994
+ // looking for it. Without structuredContent the card degrades to a plain
995
+ // status list — ids and state, no thumbnails, even for completed items
996
+ // whose media is sitting right there. uiCompleted gives it the same rich
997
+ // per-item render every generate_* tool already gets; kind:'status' picks
998
+ // renderStatusGrid, the one path here that mixes independent
999
+ // pending/completed/failed items in one grid instead of assuming they all
1000
+ // finished together.
1001
+ if (ui()) {
1002
+ return uiCompleted({
1003
+ tool: 'get_generation_status', kind: 'status', client,
1004
+ model: 'Generations', gen: { generation_id: ids[0] },
1005
+ settings: {},
1006
+ items: results.map(r => {
1007
+ const res = r.result || {};
1008
+ return {
1009
+ id: r.generation_id,
1010
+ state: r.state,
1011
+ title: res.prompt_used || res.prompt || undefined,
1012
+ url: Array.isArray(res.urls) ? res.urls[0] : undefined,
1013
+ };
1014
+ }),
1015
+ }, text);
1016
+ }
1017
+
981
1018
  return {
982
- content: [{
983
- type: 'text',
984
- text: JSON.stringify({
985
- all_done: pending.length === 0,
986
- completed: results.filter(r => r.state === 'completed').length,
987
- failed: results.filter(r => r.state === 'failed' || r.state === 'cancelled').length,
988
- still_processing: pending.map(r => r.generation_id),
989
- _hint: pending.length === 0 ? doneHint : pendingHint,
990
- generations: results
991
- }, null, 2)
992
- }]
1019
+ content: [{ type: 'text', text }]
993
1020
  };
994
1021
  }
995
1022
  );
@@ -103,7 +103,7 @@ function registerModelTools(server, client, options = {}) {
103
103
  // ─── list_models ───────────────────────────────────────────
104
104
  server.tool(
105
105
  'list_models',
106
- 'List available AI models on Kolbo. Filter by `type` to narrow to a generation type, and pass `format: "json"` to enumerate the catalog with exact identifiers — `format: "json"` + `type` returns the full raw model documents (every constraint field, for programmatic comparison / cap validation before submitting a generation); `format: "json"` alone returns a compact index of EVERY model and its identifier. Default `format: "text"` returns the human-readable summary. NEVER guess a model identifier: call this tool.',
106
+ 'List available AI models on Kolbo. Filter by `type` to narrow to a generation type, and pass `format: "json"` to enumerate the catalog with exact identifiers — `format: "json"` + `type` returns the full raw model documents (every constraint field, for programmatic comparison / cap validation before submitting a generation); `format: "json"` alone returns a compact index of EVERY model and its identifier. Default `format: "text"` returns the human-readable summary. NEVER guess a model identifier: call this tool. ⚠️ COST: for any model whose type is video / firstlast / elements / motion_graphic / cast, `credit` is a PER-SECOND rate, not a per-clip price — multiply by the requested `duration` before quoting cost to the user (e.g. `credit: 9` at `duration: 8` is 72 credits, not 9). This is the universal rule, not a per-model exception. The one carve-out is a model with `flat_credit_by_resolution` set — those charge the flat rate regardless of duration. Every other model type (image, audio, 3D, per-token text) already bills flat per generation as `credit` states.',
107
107
  {
108
108
  type: z.string().optional().describe('Filter by DB type name: "text_to_img", "image_editing", "text_to_video", "img_to_video", "draw_to_video", "video_to_video", "elements", "firstlastgenerations", "lipsync-image", "lipsync-video", "music_gen", "text_to_speech", "text_to_sound", "stt", "text". Legacy aliases also accepted: "image", "image_edit", "video", "video_from_image", "video_from_video", "music", "speech", "sound", "chat", "lipsync" (both lipsync types), "three_d" (all 3D types), "first_last_frame", "transcription". Omit for all models.'),
109
109
  format: z.enum(['text', 'json']).optional().describe('Output format. "text" (default) returns a human-readable summary with the most-used caps. "json" is the source of truth for identifiers and caps: with `type` it returns the raw model documents from the API (identifier, credit, supported_durations, supported_resolutions, supported_aspect_ratios, max_reference_images, max_visual_dna, max_video_duration, …) for EVERY model of that type; without `type` it returns a compact index of every model in the catalog and its exact identifier. Use it whenever you need an identifier you have not seen listed, or must verify a cap before passing a value that might exceed a model-specific limit.'),
@@ -334,9 +334,31 @@ function registerModelTools(server, client, options = {}) {
334
334
  // Text models bill per token — the flat `credit` is not what the user pays,
335
335
  // so show the real per-1K rates when the API supplies them. Without this the
336
336
  // "cheapest model that fits" rule is unusable for chat.
337
+ //
338
+ // Video-type models are the same problem in a different shape: kolbo-api's
339
+ // credit engine (credManagment.js) treats "charge per second of requested
340
+ // duration" as the UNIVERSAL rule for any type in
341
+ // [video, firstlast, elements, motion_graphic, cast] — not a per-model
342
+ // exception, the default. So `credit: 9` on a model with duration 8 is
343
+ // really 72 credits, and nothing in the catalog said so: an agent quoting
344
+ // cost from the bare `credit` field alone is wrong by exactly the
345
+ // requested duration, every time. `flat_credit_by_resolution` is the one
346
+ // carve-out — those models are charged the flat rate regardless of
347
+ // duration, so they're excluded here the same way credManagment.js
348
+ // excludes them (resolveFlatCredit wins over the multiplier).
349
+ const PER_SECOND_TYPES = ['video', 'firstlast', 'elements', 'motion_graphic', 'cast'];
350
+ const isPerSecondVideo = m => {
351
+ const types = Array.isArray(m.types) ? m.types : (m.type ? [m.type] : []);
352
+ const billedPerSecond = types.some(t => PER_SECOND_TYPES.some(kw => String(t).includes(kw)));
353
+ const hasFlatOverride = m.flat_credit_by_resolution && typeof m.flat_credit_by_resolution === 'object'
354
+ && Object.keys(m.flat_credit_by_resolution).length > 0;
355
+ return billedPerSecond && !hasFlatOverride;
356
+ };
337
357
  const cost = m => (m.output_token_rate != null
338
358
  ? `${m.input_token_rate ?? '?'}/${m.output_token_rate} credits per 1K tokens (in/out)`
339
- : `${m.credit} credits`);
359
+ : isPerSecondVideo(m)
360
+ ? `${m.credit} credits/second (× requested duration — NOT a flat per-clip price)`
361
+ : `${m.credit} credits`);
340
362
  const formatModel = m =>
341
363
  `${m.identifier} (${m.name}) - ${cost(m)}${m.recommended ? ' [RECOMMENDED]' : ''}${m.new_model ? ' [NEW]' : ''}${m.summary ? ` — ${detailed ? m.summary : brief(m.summary)}` : ''}${detailed ? formatSpecs(m) : ''}`;
342
364