@clipit-ai/cli 0.2.9 → 0.3.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/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -59,6 +59,10 @@ clipit auth open-settings
59
59
  clipit auth profiles --json
60
60
  clipit auth use <profile>
61
61
  clipit skills list --json
62
+ clipit skills describe <skill-or-bundle-id> --json
63
+ clipit skills manifest --json
64
+ clipit media-guides list --json
65
+ clipit media-guides describe <guide-id> --json
62
66
  clipit tools list --json
63
67
  clipit tools describe <functionName> --json
64
68
  clipit run <functionName> --params @params.json --max-credits 25 --json
@@ -110,7 +114,7 @@ clipit exports wait <jobId> --stream
110
114
  clipit exports download <jobId> --open
111
115
  clipit exports cancel <jobId> --confirm --json
112
116
 
113
- clipit deliverables create --export-id <exportId> --title "Client-ready clip" --confirm --json
117
+ clipit deliverables create --export-id <exportId> --confirm --json
114
118
  clipit deliverables list --status selected --json
115
119
 
116
120
  clipit assets list --json
@@ -204,7 +208,14 @@ clipit workflow approve <jobId> --approval-id <approvalId> --decision cheaper
204
208
 
205
209
  ## MCP Stdio Bridge
206
210
 
207
- Use `clipit mcp stdio` from MCP-compatible clients that can launch a local command. It speaks standard MCP `Content-Length` framed JSON-RPC over stdio, exposes ClipIt agent tools through the authenticated CLI profile, supports `initialize`, `tools/list`, and `tools/call`, and delegates execution to `/api/v1/agent/execute`.
211
+ Use `clipit mcp stdio` from MCP-compatible clients that can launch a local command. It speaks standard MCP `Content-Length` framed JSON-RPC over stdio and exposes both executable ClipIt tools and read-only capability resources through the authenticated CLI profile. Tool calls delegate to `/api/v1/agent/execute`; capability resources are resolved from the live permission-scoped agent registry.
212
+
213
+ Supported MCP methods include `initialize`, `tools/list`, `tools/call`, `resources/list`, and `resources/read`. The resource catalog uses stable URIs:
214
+
215
+ - `clipit://instructions` — the installed agent operating contract
216
+ - `clipit://manifest` — versions, checksums, available capabilities, and the current model estate
217
+ - `clipit://skills/<id>` — one full skill or workflow-bundle guide
218
+ - `clipit://media-guides/<id>` — one provider-aware media prompting guide
208
219
 
209
220
  Paid generation, publishing, deletion, and other metered tools still require an explicit confirmation step. If a tool call may spend credits or mutate user-visible ClipIt state, the MCP bridge returns a `requiresConfirmation` result instead of executing it; retry only after the user approves and include `confirmed: true` in the tool arguments.
210
221
 
@@ -218,7 +229,11 @@ clipit links clip <clipId> --download --json
218
229
 
219
230
  ## Agent Skills
220
231
 
221
- Install credential-free instructions for a shell-capable agent. The CLI fetches server-rendered instructions from `/api/v1/agent/instructions?target=<agent>&format=json` when authenticated. If the fetch fails or the CLI is offline, it installs a generic fallback skill and records the source in `SKILL.meta.json` next to `SKILL.md`.
232
+ Install credential-free instructions for a shell-capable agent. The CLI fetches server-rendered instructions from `/api/v1/agent/instructions?target=<agent>&format=json` when authenticated. A server-rendered install contains a concise root `SKILL.md` plus permission-scoped `references/` for editorial workflow, state/time/trust rules, the capability manifest, relevant skills and workflows, and media prompting guides. The agent loads those references progressively as the edit requires them instead of carrying the entire platform manual in every turn.
233
+
234
+ The root skill establishes one execution owner for mutating work and a repeatable edit loop: inspect, plan, edit, review, render, and deliver. Live tool schemas, the capability manifest, provider contracts, and server estimates remain authoritative over copied examples or remembered model behavior.
235
+
236
+ If the fetch fails or the CLI is offline, the installer writes a generic fallback skill and records the source in `SKILL.meta.json` next to `SKILL.md`.
222
237
 
223
238
  ```bash
224
239
  clipit agent install codex
@@ -227,7 +242,7 @@ clipit agent install hermes
227
242
  clipit agent install generic
228
243
  ```
229
244
 
230
- Inspect or remove generated skills:
245
+ Inspect, refresh, or remove generated skills:
231
246
 
232
247
  ```bash
233
248
  clipit agent doctor codex --json
@@ -237,6 +252,8 @@ clipit agent uninstall codex
237
252
  clipit agent print-skill generic
238
253
  ```
239
254
 
255
+ `agent doctor` validates the generated root skill and its reference files against the sidecar hashes. Install/update also rejects a live pack whose declared minimum CLI version is newer than the running CLI and tells the operator to install `@clipit-ai/cli@latest`. After reconnecting, run `agent update` to replace an offline fallback or refresh a stale capability-manifest version.
256
+
240
257
  ## Profiles And Environment
241
258
 
242
259
  The default profile is `default`.
@@ -274,7 +291,7 @@ Only use `--allow-custom-host` for hosts you control or trust. It permits the CL
274
291
  | Area | Commands | Notes |
275
292
  | --- | --- | --- |
276
293
  | Auth and setup | `login`, `setup`, `logout`, `doctor`, `auth status`, `auth set-key`, `auth open-settings`, `auth profiles`, `auth use` | Browser-link login, manual key fallback, and fail-closed named workspace profiles. |
277
- | Agent tools | `skills list`, `tools list`, `tools describe`, `run`, `ask`, `workflow status/wait/approve`, `mcp stdio` | Direct tool execution, natural-language Clippy workflows, and MCP stdio access. |
294
+ | Agent tools | `skills list/describe/manifest`, `media-guides list/describe`, `tools list/describe`, `run`, `ask`, `workflow status/wait/approve`, `mcp stdio` | Permission-scoped discovery, provider-aware prompt guidance, direct execution, Clippy workflows, and MCP tools/resources. |
278
295
  | Context and navigation | `context use/show/clear/build`, `open`, `links`, `examples` | Persist active IDs and open ClipIt review surfaces. |
279
296
  | Videos and clips | `videos list/get/upload/abort-upload/import-url/transcribe/transcript/suggest-clips/delete`, `clips list/get/create/update/render/download/delete`, `jobs get/wait` | Core media and render job helpers. |
280
297
  | Credits | `credits balance`, `credits usage`, `credits estimate` | Estimate accepts `--metrics @file.json`. |
@@ -284,4 +301,4 @@ Only use `--allow-custom-host` for hosts you control or trust. It permits the CL
284
301
  | Assets | `assets list/upload/delete` | Upload signs, PUTs the file to object storage, then finalizes the library asset. Delete requires `--confirm`. |
285
302
  | Thumbnails and B-Roll | `thumbnails generate/get/list`, `broll plan/generate/list/get` | Paid generation commands require `--confirm`. |
286
303
  | Social | `social accounts/post/schedule/posts/get/cancel` | Publish/schedule/cancel require `--confirm`; `x` maps to the server `twitter` platform. |
287
- | Agent skills | `agent install/update/uninstall/doctor/print-skill/list` | `doctor` reports whether the installed skill came from the server or fallback. |
304
+ | Agent skills | `agent install/update/uninstall/doctor/print-skill/list` | Installs a progressive multi-file editor skill; `doctor` validates source, root content, references, and capability-manifest freshness. |
package/bin/clipit.mjs CHANGED
@@ -9,7 +9,7 @@ import path from 'node:path';
9
9
  import process from 'node:process';
10
10
  import { fileURLToPath } from 'node:url';
11
11
 
12
- const VERSION = '0.2.9';
12
+ const VERSION = '0.3.0';
13
13
  const DEFAULT_BASE_URL = 'https://clipit.dev';
14
14
  const DEFAULT_SCOPES = [
15
15
  'clippy_agent',
@@ -53,17 +53,17 @@ const API_REQUEST_TIMEOUT_MS = readPositiveIntegerEnv(
53
53
  const MAX_CREDITS_ESTIMATE_MAP = Object.freeze({
54
54
  'exports start': [{ operationType: 'lambda_render', provider: 'aws_lambda', modelId: 'remotion-4.0', metrics: 'remotion-render' }],
55
55
  'thumbnails generate': [{ operationType: 'thumbnail_generation', provider: 'replicate', modelId: 'openai/gpt-image-2', metrics: 'one-generation' }],
56
- 'broll plan': [{ operationType: 'ai_chat', provider: 'openrouter', modelId: 'minimax/minimax-m2.7', metrics: 'broll-plan' }],
56
+ 'broll plan': [{ operationType: 'ai_chat', provider: 'openrouter', modelId: 'openai/gpt-5.6-terra-pro', metrics: 'broll-plan' }],
57
57
  'broll generate': [
58
58
  { operationType: 'image_generation', provider: 'replicate', modelId: 'openai/gpt-image-2', metrics: 'broll-images' },
59
- { operationType: 'video_generation', provider: 'replicate', modelId: 'alibaba/happyhorse-1.0', metrics: 'broll-video' },
59
+ { operationType: 'video_generation', provider: 'fal', modelId: 'minimax/h3-max/image-to-video', metrics: 'broll-video' },
60
60
  ],
61
61
  'clips create': [{ operationType: 'clip', provider: 'deepgram', metrics: 'clip-create' }],
62
62
  'clips render': [{ operationType: 'lambda_render', provider: 'aws_lambda', modelId: 'remotion-4.0', metrics: 'remotion-render' }],
63
63
  'videos upload': [{ operationType: 'video_storage', provider: 'railway_s3', metrics: 'video-upload-storage' }],
64
64
  'videos import-url': [{ operationType: null, provider: null, metrics: 'url-import' }],
65
65
  'videos transcribe': [{ operationType: 'transcription', provider: 'deepgram', modelId: 'nova-3', metrics: 'transcription-video' }],
66
- 'videos suggest-clips': [{ operationType: 'ai_chat', provider: 'openrouter', modelId: 'x-ai/grok-4.20-beta', metrics: 'suggest-clips' }],
66
+ 'videos suggest-clips': [{ operationType: 'ai_chat', provider: 'openrouter', modelId: 'openai/gpt-5.6-sol-pro', metrics: 'suggest-clips' }],
67
67
  'social post': [{ operationType: 'social_post', provider: 'zernio', metrics: 'social-platforms' }],
68
68
  'social schedule': [{ operationType: 'social_post', provider: 'zernio', metrics: 'social-platforms' }],
69
69
  });
@@ -77,9 +77,9 @@ const RUN_MAX_CREDITS_ESTIMATE_MAP = Object.freeze({
77
77
  generateImage: [{ operationType: 'image_generation', provider: 'replicate', modelId: 'openai/gpt-image-2', metrics: 'one-generation' }],
78
78
  generateBRoll: MAX_CREDITS_ESTIMATE_MAP['broll generate'],
79
79
  generateVoiceover: [{ operationType: 'tts_generation', provider: 'replicate', modelId: 'google/gemini-3.1-flash-tts', metrics: 'tts-provider-cost' }],
80
- generateMusicBed: [{ operationType: 'music_generation', provider: 'replicate', modelId: 'minimax/music-2.5', metrics: 'one-generation' }],
81
- generateVideoAlter: [{ operationType: 'video_alter_generation', provider: 'replicate', modelId: 'kwaivgi/kling-v3-omni-video', metrics: 'video-alter-provider-cost' }],
82
- generatePlatformCaption: [{ operationType: 'ai_chat', provider: 'openrouter', modelId: 'anthropic/claude-opus-4.7', metrics: 'platform-caption' }],
80
+ generateMusicBed: [{ operationType: 'music_generation', provider: 'replicate', modelId: 'minimax/music-2.6', metrics: 'one-generation' }],
81
+ generateVideoAlter: [{ operationType: 'video_alter_generation', provider: 'fal', modelId: 'google/gemini-omni-flash/v1.1/edit', metrics: 'video-alter-provider-cost' }],
82
+ generatePlatformCaption: [{ operationType: 'ai_chat', provider: 'openrouter', modelId: 'openai/gpt-5.6-terra-pro', metrics: 'platform-caption' }],
83
83
  applyHookToFront: REMOTION_RUN_ESTIMATE,
84
84
  addLibraryAssetToClip: REMOTION_RUN_ESTIMATE,
85
85
  renderClipWithRemotion: REMOTION_RUN_ESTIMATE,
@@ -441,6 +441,29 @@ function hashText(value) {
441
441
  return createHash('sha256').update(String(value)).digest('hex');
442
442
  }
443
443
 
444
+ function compareSemver(left, right) {
445
+ const parse = (value) => {
446
+ const match = String(value || '').match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
447
+ if (!match) return null;
448
+ return {
449
+ numbers: match.slice(1, 4).map(Number),
450
+ prerelease: match[4] || null,
451
+ };
452
+ };
453
+ const current = parse(left);
454
+ const minimum = parse(right);
455
+ if (!current || !minimum) return null;
456
+ for (let index = 0; index < current.numbers.length; index += 1) {
457
+ if (current.numbers[index] !== minimum.numbers[index]) {
458
+ return current.numbers[index] > minimum.numbers[index] ? 1 : -1;
459
+ }
460
+ }
461
+ if (current.prerelease === minimum.prerelease) return 0;
462
+ if (!current.prerelease) return 1;
463
+ if (!minimum.prerelease) return -1;
464
+ return current.prerelease.localeCompare(minimum.prerelease, undefined, { numeric: true });
465
+ }
466
+
444
467
  function usage() {
445
468
  return [
446
469
  'ClipIt CLI',
@@ -458,6 +481,9 @@ function usage() {
458
481
  ' clipit context use [--video-id id] [--clip-id id] [--project-id id] [--sequence-id id]',
459
482
  ' clipit context show|clear|build [--json]',
460
483
  ' clipit skills list [--json]',
484
+ ' clipit skills describe <skill-or-workflow> [--json]',
485
+ ' clipit skills manifest [--json]',
486
+ ' clipit media-guides list|describe [guide-id] [--json]',
461
487
  ' clipit tools list [--skill clip] [--json]',
462
488
  ' clipit tools describe <functionName>',
463
489
  ' clipit ask "<prompt>" [--video-id id] [--clip-id id] [--conversation-id id] [--quick] [--auto-confirm-costly] [--stream] [--json]',
@@ -465,6 +491,7 @@ function usage() {
465
491
  ' clipit workflow approve <jobId> --approval-id id [--decision approved|cheaper|cancelled]',
466
492
  ' clipit mcp [stdio]',
467
493
  ' clipit run <functionName> [--params @file.json] [--clip-id id] [--video-id id] [--confirm] [--max-credits n]',
494
+ ' clipit run setCaptionStyle --clip-id id --params @caption-style.json # exact style or bounded captionDirective; no render',
468
495
  ' clipit videos list|get|upload|abort-upload|import-url|transcribe|transcript|suggest-clips|delete ...',
469
496
  ' clipit clips list|get|delivery-state|create|update|initialize-snapshot|render|download|delete ...',
470
497
  ' clipit jobs get|wait <jobId>',
@@ -485,6 +512,59 @@ function usage() {
485
512
  }
486
513
 
487
514
  function commandUsage(command, subcommand) {
515
+ if (command === 'run' && subcommand === 'setCaptionStyle') {
516
+ return [
517
+ 'Usage:',
518
+ ' clipit run setCaptionStyle --clip-id <id> --params <json|@file> --json',
519
+ '',
520
+ 'The params object must pin expectedEditorVersion, expectedEditorStateHash, and expectedClipSettingsRevision,',
521
+ 'then supply exactly one of captionStyle or captionDirective.',
522
+ '',
523
+ 'captionDirective accepts the strict form: caption size <50-300>% <auto|single|single-line|stacked>.',
524
+ 'Example: "caption size 200% single" changes only fontSizeScale to 2 and captionLineMode to single-line;',
525
+ 'it preserves wordsPerLine and every other current caption-style field.',
526
+ ].join('\n');
527
+ }
528
+ if (command === 'clips' && subcommand === 'initialize-snapshot') {
529
+ return [
530
+ 'Usage:',
531
+ ' clipit clips initialize-snapshot <clipId> [options]',
532
+ '',
533
+ 'Required:',
534
+ ' <clipId> Clip to initialize or safely reinitialize',
535
+ '',
536
+ 'Optional:',
537
+ ' --aspect-ratio <ratio> 16:9, 9:16, 1:1, or 4:5',
538
+ ' --fit-background <mode> black or blur',
539
+ ' --quality <quality> standard, high, or 4k',
540
+ ' --captions <true|false> Explicitly enable or disable captions',
541
+ ' --caption-style <style> bold, minimal, neon, or classic',
542
+ ' --params <json|@file> Supply the full initializer request, including captionPresetId or a strict rich captionStyle object',
543
+ ' --profile <name> Named ClipIt credential profile',
544
+ ' --json Emit machine-readable JSON',
545
+ '',
546
+ 'Omitted snapshot settings preserve the clip\'s exact current editor state.',
547
+ ].join('\n');
548
+ }
549
+ if (command === 'exports' && subcommand === 'start') {
550
+ return [
551
+ 'Usage:',
552
+ ' clipit exports start --clip-id <id> --confirm [options]',
553
+ '',
554
+ 'Required:',
555
+ ' --clip-id <id> Clip to export from its verified current snapshot',
556
+ ' --confirm Confirm the export mutation',
557
+ '',
558
+ 'Optional:',
559
+ ' --params <json|@file> Additional export settings',
560
+ ' --idempotency-key <key> Stable retry identity; generated when omitted',
561
+ ' --max-credits <n> Refuse an estimated client charge above this amount',
562
+ ' --profile <name> Named ClipIt credential profile',
563
+ ' --json Emit machine-readable JSON',
564
+ '',
565
+ 'The CLI pins the current editor version and state hash before starting the export.',
566
+ ].join('\n');
567
+ }
488
568
  if (command === 'social' && subcommand === 'schedule') {
489
569
  return [
490
570
  'Usage:',
@@ -511,19 +591,17 @@ function commandUsage(command, subcommand) {
511
591
  if (command === 'deliverables' && subcommand === 'create') {
512
592
  return [
513
593
  'Usage:',
514
- ' clipit deliverables create --export-id <id> --title <text> --confirm',
594
+ ' clipit deliverables create --export-id <id> --confirm',
515
595
  '',
516
596
  'Required:',
517
597
  ' --export-id <id> Exact completed export to deliver',
518
- ' --title <text> Client-facing deliverable title',
519
598
  ' --confirm Confirm the delivery mutation',
520
599
  '',
521
600
  'Optional:',
522
- ' --note <text> Client-facing note',
523
601
  ' --profile <name> Named ClipIt credential profile',
524
602
  ' --json Emit machine-readable JSON',
525
603
  '',
526
- 'The response includes clientSelectionUrl; the client must select the delivery before enterprise social publishing.',
604
+ 'The CLI pins the export execution, snapshot, caption-style hash, object fingerprint, outro policy, and verified artifact duration. ClipIt derives the title and note from canonical metadata.',
527
605
  ].join('\n');
528
606
  }
529
607
  return null;
@@ -1000,8 +1078,9 @@ function normalizeCreditEstimateRequest(request) {
1000
1078
  }
1001
1079
 
1002
1080
  function brollVideoProviderCostUsd(durationSeconds, resolution) {
1003
- const perSecondUsd = resolution === '1080p' ? 0.28 : 0.14;
1004
- return Math.max(0, durationSeconds * perSecondUsd);
1081
+ const billableDuration = Math.max(5, Math.min(15, Math.round(durationSeconds)));
1082
+ const perSecondUsd = resolution === '480p' ? 0.05 : 0.08;
1083
+ return billableDuration * perSecondUsd;
1005
1084
  }
1006
1085
 
1007
1086
  function brollImageProviderCostUsd(quality) {
@@ -1019,10 +1098,8 @@ function ttsProviderCostUsd(text, prompt) {
1019
1098
  }
1020
1099
 
1021
1100
  function videoAlterProviderCostUsd(durationSeconds, mode) {
1022
- const perSecond = mode === 'pro'
1023
- ? Number(process.env.KLING_OMNI_PRO_COST_PER_SECOND_USD ?? '0.56')
1024
- : Number(process.env.KLING_OMNI_STANDARD_COST_PER_SECOND_USD ?? '0.28');
1025
- return Math.max(0, durationSeconds * (Number.isFinite(perSecond) ? perSecond : 0.28));
1101
+ const perSecond = mode === 'pro' ? 0.15 : 0.10;
1102
+ return Math.max(0, durationSeconds * perSecond);
1026
1103
  }
1027
1104
 
1028
1105
  function durationFromClip(clip, body = {}) {
@@ -1096,7 +1173,7 @@ async function buildMaxCreditsEstimateRequest(config, options, spec, context = {
1096
1173
 
1097
1174
  if (spec.metrics === 'broll-images') {
1098
1175
  const mode = context.body?.mode || 'single_image';
1099
- const generationCount = mode === 'start_end_frame' ? 3 : 1;
1176
+ const generationCount = mode === 'start_end_frame' ? 2 : 1;
1100
1177
  return {
1101
1178
  ...spec,
1102
1179
  metrics: {
@@ -1108,7 +1185,7 @@ async function buildMaxCreditsEstimateRequest(config, options, spec, context = {
1108
1185
 
1109
1186
  if (spec.metrics === 'broll-video') {
1110
1187
  const durationSeconds = Number(context.body?.durationSeconds ?? context.body?.duration ?? 6);
1111
- const resolution = context.body?.resolution || '720p';
1188
+ const resolution = context.body?.resolution || '768p';
1112
1189
  if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return null;
1113
1190
  return {
1114
1191
  ...spec,
@@ -1158,7 +1235,7 @@ async function buildMaxCreditsEstimateRequest(config, options, spec, context = {
1158
1235
  return null;
1159
1236
  }
1160
1237
 
1161
- async function buildStaticRunEstimates(config, options, functionName, parameters, payload) {
1238
+ async function buildStaticRunEstimates(config, options, functionName, parameters, payload, maxCredits = null) {
1162
1239
  const specs = RUN_MAX_CREDITS_ESTIMATE_MAP[functionName];
1163
1240
  if (!specs) return null;
1164
1241
 
@@ -1181,12 +1258,19 @@ async function buildStaticRunEstimates(config, options, functionName, parameters
1181
1258
 
1182
1259
  const estimates = [];
1183
1260
  for (const request of requests) {
1184
- const estimate = await apiFetch(config, options, 'POST', '/api/v1/credits/estimate', request);
1261
+ const estimate = await apiFetch(config, options, 'POST', '/api/v1/credits/preflight', compactObject({
1262
+ ...request,
1263
+ maxCredits,
1264
+ }));
1185
1265
  estimates.push({
1186
1266
  operationType: request.operationType,
1187
1267
  provider: request.provider,
1188
1268
  modelId: request.modelId,
1189
- estimatedCostClip: Number(estimate?.estimatedCostClip ?? 0),
1269
+ estimatedCostClip: Number(estimate?.internalEstimatedUsageClip ?? 0),
1270
+ internalEstimatedUsageClip: Number(estimate?.internalEstimatedUsageClip ?? 0),
1271
+ clientCreditChargeClip: Number(estimate?.clientCreditChargeClip ?? 0),
1272
+ settlementMode: estimate?.settlementMode ?? 'direct',
1273
+ withinApprovalCap: estimate?.withinApprovalCap !== false,
1190
1274
  affordable: estimate?.affordable,
1191
1275
  spendLimitViolation: estimate?.spendLimitViolation ?? null,
1192
1276
  });
@@ -1217,7 +1301,7 @@ function throwMaxCreditsEstimateUnavailable(commandKey) {
1217
1301
 
1218
1302
  function estimatedClipCostFromData(data) {
1219
1303
  if (!data || typeof data !== 'object') return null;
1220
- for (const key of ['estimatedCostClip', 'totalEstimatedCostClip', 'costClip', 'totalCostClip']) {
1304
+ for (const key of ['internalEstimatedUsageClip', 'estimatedCostClip', 'totalEstimatedCostClip', 'costClip', 'totalCostClip']) {
1221
1305
  const value = Number(data[key]);
1222
1306
  if (Number.isFinite(value)) return value;
1223
1307
  }
@@ -1253,12 +1337,19 @@ async function enforceMaxCredits(config, options, commandKey, context = {}) {
1253
1337
 
1254
1338
  const estimates = [];
1255
1339
  for (const request of requests) {
1256
- const estimate = await apiFetch(config, options, 'POST', '/api/v1/credits/estimate', request);
1340
+ const estimate = await apiFetch(config, options, 'POST', '/api/v1/credits/preflight', {
1341
+ ...request,
1342
+ maxCredits: limit,
1343
+ });
1257
1344
  estimates.push({
1258
1345
  operationType: request.operationType,
1259
1346
  provider: request.provider,
1260
1347
  modelId: request.modelId,
1261
- estimatedCostClip: Number(estimate?.estimatedCostClip ?? 0),
1348
+ estimatedCostClip: Number(estimate?.internalEstimatedUsageClip ?? 0),
1349
+ internalEstimatedUsageClip: Number(estimate?.internalEstimatedUsageClip ?? 0),
1350
+ clientCreditChargeClip: Number(estimate?.clientCreditChargeClip ?? 0),
1351
+ settlementMode: estimate?.settlementMode ?? 'direct',
1352
+ withinApprovalCap: estimate?.withinApprovalCap !== false,
1262
1353
  affordable: estimate?.affordable,
1263
1354
  spendLimitViolation: estimate?.spendLimitViolation ?? null,
1264
1355
  });
@@ -1269,15 +1360,18 @@ async function enforceMaxCredits(config, options, commandKey, context = {}) {
1269
1360
  warnIfEstimateUnaffordable(estimates);
1270
1361
 
1271
1362
  const estimatedCostClip = estimates.reduce((sum, estimate) => sum + estimate.estimatedCostClip, 0);
1272
- if (estimatedCostClip > limit) {
1363
+ const clientCreditChargeClip = estimates.reduce((sum, estimate) => sum + estimate.clientCreditChargeClip, 0);
1364
+ if (estimatedCostClip > limit || estimates.some((estimate) => !estimate.withinApprovalCap)) {
1273
1365
  throw Object.assign(
1274
- new Error(`Estimated cost ${clipCostLabel(estimatedCostClip)} exceeds --max-credits ${clipCostLabel(limit)} for ${commandKey}.`),
1366
+ new Error(`Internal estimated usage ${clipCostLabel(estimatedCostClip)} exceeds --max-credits ${clipCostLabel(limit)} for ${commandKey}; client credit charge is ${clipCostLabel(clientCreditChargeClip)}.`),
1275
1367
  {
1276
1368
  exitCode: EXIT.CONFIRMATION,
1277
1369
  data: {
1278
1370
  command: commandKey,
1279
1371
  maxCredits: limit,
1280
1372
  estimatedCostClip,
1373
+ internalEstimatedUsageClip: estimatedCostClip,
1374
+ clientCreditChargeClip,
1281
1375
  estimates,
1282
1376
  },
1283
1377
  },
@@ -1309,7 +1403,7 @@ async function enforceRunMaxCredits(config, options, functionName, parameters =
1309
1403
 
1310
1404
  let staticEstimates = null;
1311
1405
  if (!runEstimate && (limit !== null || needsConfirmationPreflight)) {
1312
- staticEstimates = await buildStaticRunEstimates(config, options, functionName, parameters, payload);
1406
+ staticEstimates = await buildStaticRunEstimates(config, options, functionName, parameters, payload, limit);
1313
1407
  }
1314
1408
 
1315
1409
  if (limit === null) {
@@ -1347,7 +1441,7 @@ async function enforceRunMaxCredits(config, options, functionName, parameters =
1347
1441
  warnIfEstimateUnaffordable(staticEstimates);
1348
1442
 
1349
1443
  const estimatedCostClip = staticEstimates.reduce((sum, estimate) => sum + estimate.estimatedCostClip, 0);
1350
- if (estimatedCostClip > limit) {
1444
+ if (estimatedCostClip > limit || staticEstimates.some((estimate) => !estimate.withinApprovalCap)) {
1351
1445
  throw Object.assign(
1352
1446
  new Error(`Estimated cost ${clipCostLabel(estimatedCostClip)} exceeds --max-credits ${clipCostLabel(limit)} for run ${functionName}.`),
1353
1447
  {
@@ -1625,6 +1719,28 @@ async function listSkills(config, options) {
1625
1719
  output(await apiFetch(config, options, 'GET', '/api/v1/agent/skills'), options);
1626
1720
  }
1627
1721
 
1722
+ async function describeSkill(config, options, id) {
1723
+ if (!id) throw Object.assign(new Error('Skill or workflow ID is required.'), { exitCode: EXIT.USAGE });
1724
+ output(await apiFetch(config, options, 'GET', `/api/v1/agent/skills/${encodeURIComponent(id)}`), options);
1725
+ }
1726
+
1727
+ async function capabilityManifest(config, options) {
1728
+ output(await apiFetch(config, options, 'GET', '/api/v1/agent/capability-manifest'), options);
1729
+ }
1730
+
1731
+ async function mediaGuides(config, options, action, id) {
1732
+ if (action === 'list') {
1733
+ output(await apiFetch(config, options, 'GET', '/api/v1/agent/media-guides'), options);
1734
+ return;
1735
+ }
1736
+ if (action === 'describe') {
1737
+ if (!id) throw Object.assign(new Error('Media guide ID is required.'), { exitCode: EXIT.USAGE });
1738
+ output(await apiFetch(config, options, 'GET', `/api/v1/agent/media-guides/${encodeURIComponent(id)}`), options);
1739
+ return;
1740
+ }
1741
+ throw Object.assign(new Error(`Unknown media-guides command: ${action || ''}`), { exitCode: EXIT.USAGE });
1742
+ }
1743
+
1628
1744
  async function listTools(config, options) {
1629
1745
  const params = new URLSearchParams();
1630
1746
  if (options.skill) params.set('skill', String(options.skill));
@@ -1769,6 +1885,57 @@ function normalizeAgentExecuteResult(result) {
1769
1885
  });
1770
1886
  }
1771
1887
 
1888
+ function resolveToolResultContext(result, fallback = {}) {
1889
+ if (!result || typeof result !== 'object' || Array.isArray(result)) return null;
1890
+ const data = result.result && typeof result.result === 'object' && !Array.isArray(result.result)
1891
+ ? result.result
1892
+ : result;
1893
+ const libraryClip = data.libraryClip && typeof data.libraryClip === 'object' && !Array.isArray(data.libraryClip)
1894
+ ? data.libraryClip
1895
+ : null;
1896
+ const batchItems = [
1897
+ ...(Array.isArray(data.created) ? data.created : []),
1898
+ ...(Array.isArray(data.skipped) ? data.skipped : []),
1899
+ ].filter((item) => item && typeof item === 'object' && !Array.isArray(item));
1900
+ const selectedClipIds = batchItems
1901
+ .map((item) => item.libraryClipId || item.clipId || item.id)
1902
+ .filter(Boolean)
1903
+ .map(String);
1904
+ const batchVideoIds = [...new Set(batchItems
1905
+ .map((item) => (
1906
+ item.libraryClip && typeof item.libraryClip === 'object' && !Array.isArray(item.libraryClip)
1907
+ ? item.libraryClip.videoId
1908
+ : item.videoId
1909
+ ))
1910
+ .filter(Boolean)
1911
+ .map(String))];
1912
+ const clipId = data.libraryClipId
1913
+ || libraryClip?.id
1914
+ || data.clipId
1915
+ || (selectedClipIds.length === 1 ? selectedClipIds[0] : null);
1916
+ const resultVideoId = libraryClip?.videoId
1917
+ || data.videoId
1918
+ || (batchVideoIds.length === 1 ? batchVideoIds[0] : null);
1919
+ if (!clipId && selectedClipIds.length === 0 && !resultVideoId) return null;
1920
+ const nextClipId = clipId ? String(clipId) : fallback.clipId;
1921
+ const selectedTargetsChanged = selectedClipIds.length > 0
1922
+ && JSON.stringify(selectedClipIds) !== JSON.stringify(fallback.selectedClipIds || []);
1923
+ const clipTargetChanged = Boolean(
1924
+ nextClipId
1925
+ && fallback.clipId
1926
+ && String(nextClipId) !== String(fallback.clipId),
1927
+ );
1928
+ const videoId = resultVideoId
1929
+ ? String(resultVideoId)
1930
+ : clipTargetChanged || selectedTargetsChanged ? undefined : fallback.videoId;
1931
+ return compactObject({
1932
+ ...fallback,
1933
+ videoId,
1934
+ clipId: nextClipId,
1935
+ selectedClipIds: selectedClipIds.length ? selectedClipIds : fallback.selectedClipIds,
1936
+ });
1937
+ }
1938
+
1772
1939
  async function runTool(config, options, functionName) {
1773
1940
  if (!functionName) throw Object.assign(new Error('Function name is required.'), { exitCode: EXIT.USAGE });
1774
1941
  if (RUN_CONFIRMATION_LABELS[functionName]) {
@@ -1807,7 +1974,23 @@ async function runTool(config, options, functionName) {
1807
1974
  process.exitCode = EXIT.CONFIRMATION;
1808
1975
  return;
1809
1976
  }
1977
+ if (result?.success !== false) {
1978
+ const nextContext = resolveToolResultContext(result, context);
1979
+ if (nextContext) {
1980
+ const recent = [
1981
+ { type: 'video', id: nextContext.videoId },
1982
+ { type: 'clip', id: nextContext.clipId },
1983
+ ...(Array.isArray(nextContext.selectedClipIds)
1984
+ ? nextContext.selectedClipIds.map((id) => ({ type: 'clip', id }))
1985
+ : []),
1986
+ ].filter((entry) => entry.id);
1987
+ await persistActiveContext(config, options, nextContext, recent);
1988
+ }
1989
+ }
1810
1990
  output(outputResult, options);
1991
+ if (result?.success === false) {
1992
+ process.exitCode = EXIT.SERVER;
1993
+ }
1811
1994
  }
1812
1995
 
1813
1996
  function workflowEndpoint(jobId) {
@@ -3107,7 +3290,7 @@ async function exportsCommand(config, options, action, args) {
3107
3290
  throw Object.assign(new Error(`Unknown exports command: ${action || ''}`), { exitCode: EXIT.USAGE });
3108
3291
  }
3109
3292
 
3110
- function enterpriseClientSelectionUrl(config, options, selectionPath = '/enterprise?tab=delivered') {
3293
+ function enterpriseClientSelectionUrl(config, options, selectionPath = '/enterprise/workspace?tab=delivered') {
3111
3294
  return new URL(selectionPath, getBaseUrl(config, options)).toString();
3112
3295
  }
3113
3296
 
@@ -3127,16 +3310,55 @@ async function deliverables(config, options, action) {
3127
3310
  }
3128
3311
  if (action === 'create') {
3129
3312
  const exportId = requiredString(options['export-id'], '--export-id');
3130
- const title = requiredString(options.title, '--title');
3131
3313
  requireConfirm(options, 'Delivering an export to the enterprise client');
3314
+ const exactExport = await apiFetch(
3315
+ config,
3316
+ options,
3317
+ 'GET',
3318
+ `/api/v1/exports/${encodeURIComponent(exportId)}`,
3319
+ );
3320
+ const expectations = {
3321
+ enterpriseExecutionId: exactExport?.enterpriseExecutionId,
3322
+ snapshotId: exactExport?.snapshotId,
3323
+ editorVersion: exactExport?.editorVersion,
3324
+ editorStateHash: exactExport?.editorStateHash,
3325
+ outputObjectFingerprint: exactExport?.outputObjectFingerprint,
3326
+ captionStyleHash: exactExport?.captionStyleHash,
3327
+ includeOutro: exactExport?.includeOutro,
3328
+ artifactDuration: exactExport?.approvedArtifactDuration,
3329
+ };
3330
+ const actualArtifactDuration = exactExport?.artifactDuration;
3331
+ const exactIdentityReady = exactExport?.status === 'completed'
3332
+ && typeof expectations.enterpriseExecutionId === 'string'
3333
+ && typeof expectations.snapshotId === 'string'
3334
+ && Number.isSafeInteger(expectations.editorVersion)
3335
+ && typeof expectations.editorStateHash === 'string'
3336
+ && /^[a-f0-9]{64}$/i.test(expectations.editorStateHash)
3337
+ && typeof expectations.outputObjectFingerprint === 'string'
3338
+ && /^[a-f0-9]{64}$/i.test(expectations.outputObjectFingerprint)
3339
+ && typeof expectations.captionStyleHash === 'string'
3340
+ && /^[a-f0-9]{64}$/i.test(expectations.captionStyleHash)
3341
+ && typeof expectations.includeOutro === 'boolean'
3342
+ && typeof expectations.artifactDuration === 'number'
3343
+ && Number.isFinite(expectations.artifactDuration)
3344
+ && expectations.artifactDuration > 0
3345
+ && typeof actualArtifactDuration === 'number'
3346
+ && Number.isFinite(actualArtifactDuration)
3347
+ && actualArtifactDuration > 0
3348
+ && Math.abs(actualArtifactDuration - expectations.artifactDuration) <= 0.15;
3349
+ if (!exactIdentityReady) {
3350
+ throw Object.assign(
3351
+ new Error('The export is not a completed, exact enterprise artifact with verified snapshot, caption-style, object, outro, and duration lineage.'),
3352
+ { exitCode: EXIT.SERVER, data: exactExport },
3353
+ );
3354
+ }
3132
3355
  const result = await apiFetch(config, options, 'POST', '/api/v1/deliverables', {
3133
3356
  exportId,
3134
- title,
3135
- note: options.note,
3357
+ expectations,
3136
3358
  });
3137
3359
  output({
3138
3360
  ...result,
3139
- clientSelectionPath: '/enterprise?tab=delivered',
3361
+ clientSelectionPath: '/enterprise/workspace?tab=delivered',
3140
3362
  clientSelectionUrl: enterpriseClientSelectionUrl(config, options),
3141
3363
  }, options);
3142
3364
  return;
@@ -3408,7 +3630,7 @@ async function resolveEnterpriseSelectedDelivery(config, options, clipId, export
3408
3630
  const current = deliveries.find((delivery) => (
3409
3631
  delivery?.clipId === clipId && delivery?.exportId === exportId
3410
3632
  )) ?? null;
3411
- const clientSelectionPath = result?.clientSelectionPath || '/enterprise?tab=delivered';
3633
+ const clientSelectionPath = result?.clientSelectionPath || '/enterprise/workspace?tab=delivered';
3412
3634
  throw Object.assign(
3413
3635
  new Error('The client has not selected this delivered export for publishing.'),
3414
3636
  {
@@ -3861,6 +4083,91 @@ async function handleLocalMcpBillingTool(config, options, name, parameters = {})
3861
4083
  throw Object.assign(new Error(`Unknown local billing tool: ${name}`), { exitCode: EXIT.USAGE });
3862
4084
  }
3863
4085
 
4086
+ function mcpCapabilityResource(capability) {
4087
+ return {
4088
+ uri: `clipit://skills/${encodeURIComponent(capability.id)}`,
4089
+ name: capability.name || capability.id,
4090
+ description: `${capability.type === 'bundle' ? 'Workflow' : 'Skill'}: ${capability.description || ''}`.trim(),
4091
+ mimeType: 'application/json',
4092
+ };
4093
+ }
4094
+
4095
+ function mcpMediaGuideResource(guide) {
4096
+ return {
4097
+ uri: `clipit://media-guides/${encodeURIComponent(guide.id)}`,
4098
+ name: guide.name || guide.id,
4099
+ description: `${guide.purpose} guide${guide.providerModelId ? ` for ${guide.providerModelId}` : ''}`,
4100
+ mimeType: 'text/markdown',
4101
+ };
4102
+ }
4103
+
4104
+ async function fetchMcpResources(config, options) {
4105
+ const [skills, mediaGuides] = await Promise.all([
4106
+ apiFetch(config, options, 'GET', '/api/v1/agent/skills'),
4107
+ apiFetch(config, options, 'GET', '/api/v1/agent/media-guides'),
4108
+ ]);
4109
+ return [
4110
+ {
4111
+ uri: 'clipit://instructions',
4112
+ name: 'ClipIt agent instructions',
4113
+ description: 'Permission-scoped operating instructions for the authenticated ClipIt profile.',
4114
+ mimeType: 'text/markdown',
4115
+ },
4116
+ {
4117
+ uri: 'clipit://manifest',
4118
+ name: 'ClipIt capability manifest',
4119
+ description: 'Version, parity checksums, model estate, and live capability counts.',
4120
+ mimeType: 'application/json',
4121
+ },
4122
+ ...(Array.isArray(skills?.capabilities) ? skills.capabilities.map(mcpCapabilityResource) : []),
4123
+ ...(Array.isArray(mediaGuides?.guides) ? mediaGuides.guides.map(mcpMediaGuideResource) : []),
4124
+ ];
4125
+ }
4126
+
4127
+ function parseClipItResourceUri(value) {
4128
+ if (typeof value !== 'string') return null;
4129
+ let parsed;
4130
+ try {
4131
+ parsed = new URL(value);
4132
+ } catch {
4133
+ return null;
4134
+ }
4135
+ if (parsed.protocol !== 'clipit:') return null;
4136
+ const id = parsed.pathname.replace(/^\/+/, '');
4137
+ try {
4138
+ return { kind: parsed.hostname, id: id ? decodeURIComponent(id) : null };
4139
+ } catch {
4140
+ return null;
4141
+ }
4142
+ }
4143
+
4144
+ async function readMcpResource(config, options, uri) {
4145
+ const parsed = parseClipItResourceUri(uri);
4146
+ if (!parsed) return null;
4147
+
4148
+ if (parsed.kind === 'instructions' && !parsed.id) {
4149
+ const response = await apiFetch(config, options, 'GET', '/api/v1/agent/instructions?target=generic&format=json');
4150
+ return { uri, mimeType: 'text/markdown', text: response.markdown };
4151
+ }
4152
+ if (parsed.kind === 'manifest' && !parsed.id) {
4153
+ const response = await apiFetch(config, options, 'GET', '/api/v1/agent/capability-manifest');
4154
+ return { uri, mimeType: 'application/json', text: JSON.stringify(redactDeep(response), null, 2) };
4155
+ }
4156
+ if (parsed.kind === 'skills' && parsed.id) {
4157
+ const response = await apiFetch(config, options, 'GET', `/api/v1/agent/skills/${encodeURIComponent(parsed.id)}`);
4158
+ return { uri, mimeType: 'application/json', text: JSON.stringify(redactDeep(response), null, 2) };
4159
+ }
4160
+ if (parsed.kind === 'media-guides' && parsed.id) {
4161
+ const response = await apiFetch(config, options, 'GET', `/api/v1/agent/media-guides/${encodeURIComponent(parsed.id)}`);
4162
+ return {
4163
+ uri,
4164
+ mimeType: 'text/markdown',
4165
+ text: [response.contract, response.guide].filter(Boolean).join('\n\n---\n\n'),
4166
+ };
4167
+ }
4168
+ return null;
4169
+ }
4170
+
3864
4171
  async function handleMcpRequest(config, options, message) {
3865
4172
  if (!message || typeof message !== 'object' || Array.isArray(message)) {
3866
4173
  return { jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Invalid JSON-RPC request.' } };
@@ -3877,7 +4184,10 @@ async function handleMcpRequest(config, options, message) {
3877
4184
  id,
3878
4185
  result: {
3879
4186
  protocolVersion: message.params?.protocolVersion || '2024-11-05',
3880
- capabilities: { tools: {} },
4187
+ capabilities: {
4188
+ tools: {},
4189
+ resources: { subscribe: false, listChanged: false },
4190
+ },
3881
4191
  serverInfo: { name: 'clipit', version: VERSION },
3882
4192
  },
3883
4193
  };
@@ -3900,6 +4210,30 @@ async function handleMcpRequest(config, options, message) {
3900
4210
  };
3901
4211
  }
3902
4212
 
4213
+ if (method === 'resources/list') {
4214
+ return {
4215
+ jsonrpc: '2.0',
4216
+ id,
4217
+ result: { resources: await fetchMcpResources(config, options) },
4218
+ };
4219
+ }
4220
+
4221
+ if (method === 'resources/read') {
4222
+ const uri = typeof message.params?.uri === 'string' ? message.params.uri : null;
4223
+ if (!uri) {
4224
+ return { jsonrpc: '2.0', id, error: { code: -32602, message: 'resources/read requires params.uri.' } };
4225
+ }
4226
+ const resource = await readMcpResource(config, options, uri);
4227
+ if (!resource) {
4228
+ return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown ClipIt resource: ${uri}` } };
4229
+ }
4230
+ return {
4231
+ jsonrpc: '2.0',
4232
+ id,
4233
+ result: { contents: [resource] },
4234
+ };
4235
+ }
4236
+
3903
4237
  if (method === 'tools/call') {
3904
4238
  const name = typeof message.params?.name === 'string' ? message.params.name : null;
3905
4239
  if (!name) {
@@ -4150,6 +4484,8 @@ async function examples(options) {
4150
4484
  thumbnail: 'clipit thumbnails generate --clip-id <clipId> --prompt "Expressive high-contrast thumbnail" --confirm --json',
4151
4485
  brollPlan: 'clipit broll plan <clipId> --count 3 --confirm --json',
4152
4486
  socialPost: 'clipit social post --clip-id <clipId> --platforms x,tiktok --account-ids x=<xAccountId>,tiktok=<tiktokAccountId> --caption "New clip" --confirm --json',
4487
+ setExactCaptionStyle: 'clipit run setCaptionStyle --clip-id <clipId> --params @caption-style.json --json',
4488
+ setCaptionDirective: 'clipit run setCaptionStyle --clip-id <clipId> --params \'{"expectedEditorVersion":1,"expectedEditorStateHash":"<sha256>","expectedClipSettingsRevision":1,"captionDirective":"caption size 200% single"}\' --json',
4153
4489
  runTool: 'clipit run <functionName> --clip-id <clipId> --params @params.json --json',
4154
4490
  reviewLink: 'clipit links clip <clipId> --json',
4155
4491
  };
@@ -4161,7 +4497,14 @@ async function examples(options) {
4161
4497
  }
4162
4498
 
4163
4499
  function skillMarkdown(agent) {
4164
- return `# ClipIt CLI
4500
+ return `---
4501
+ name: clipit-cli
4502
+ description: Edit and create video with ClipIt through its live CLI or MCP tools, including review-first workflows, generated media, audio, QA, export, and publishing.
4503
+ metadata:
4504
+ pack-version: "offline-${VERSION}"
4505
+ ---
4506
+
4507
+ # ClipIt CLI
4165
4508
 
4166
4509
  Use the ClipIt CLI to operate ClipIt through the user's authenticated account. This is the generic offline fallback skill; when possible, run \`clipit agent update ${agent}\` after login to fetch server-rendered instructions scoped to the current API key.
4167
4510
 
@@ -4169,12 +4512,18 @@ Rules:
4169
4512
  - Run \`clipit auth status --json\` before assuming ClipIt is connected.
4170
4513
  - If not connected, ask the user to run \`clipit login\` and approve the browser link.
4171
4514
  - Never ask the user to paste API keys into chat.
4172
- - Use \`clipit skills list --json\` and \`clipit tools list --json\` to discover capability.
4173
- - Use \`clipit mcp stdio\` when an MCP-compatible client can launch local stdio servers; it speaks standard \`Content-Length\` framed JSON-RPC.
4515
+ - Use \`clipit skills list --json\`, \`clipit skills describe <id> --json\`, and \`clipit skills manifest --json\` for progressive discovery. Use \`clipit tools list --json\` and \`clipit tools describe <name> --json\` for the current execution schema.
4516
+ - Use \`clipit media-guides list --json\` and \`clipit media-guides describe <id> --json\` before generated image, video, Alter, voiceover, or music work.
4517
+ - Use \`clipit mcp stdio\` when an MCP-compatible client can launch local stdio servers; it exposes tools plus \`clipit://instructions\`, \`clipit://manifest\`, skill/workflow, and media-guide resources.
4518
+ - Keep one execution owner per task: orchestrate direct CLI/MCP calls or delegate the workflow through \`clipit ask\`, never both independently.
4519
+ - Establish the audience, platform, target duration, communication goal, hook, story arc, exact text, references, preservation invariants, source-audio authority, and definition of done before creative mutation.
4520
+ - Work in passes: inspect; propose lightweight candidates; obtain selection; refine story, pacing, timeline, framing, captions, media, and audio; run QA; then export or publish.
4521
+ - Preserve real-person identity and performance. Provider completion is not creative acceptance; inspect generated media before applying it and run final delivery QA.
4174
4522
  - Prefer friendly commands such as \`clipit videos list --json\`, \`clipit clips list --json\`, \`clipit credits balance --json\`, and \`clipit jobs wait <jobId> --json\`.
4175
4523
  - Use \`clipit ask "..."\` for natural-language Clippy workflows, and \`clipit workflow wait <jobId> --json\` or \`clipit workflow approve <jobId> --approval-id <id>\` for workflow follow-through.
4176
4524
  - Use \`clipit ask "..." --auto-confirm-costly\` only when the human has asked you to work autonomously on ClipIt-only cost-spending tasks. It never authorizes social publishing/scheduling or destructive actions.
4177
4525
  - Use \`clipit run <functionName> --params @file.json --json\` for exact Clippy tools.
4526
+ - For exact captions, run \`clipit run setCaptionStyle --clip-id <clipId> --params @caption-style.json --json\`; the params file pins expectedEditorVersion, expectedEditorStateHash, and expectedClipSettingsRevision and supplies exactly one of a strict \`captionStyle\` object or \`captionDirective\`. The literal \`caption size 200% single\` maps to scale 2 plus \`single-line\` without changing wordsPerLine or other current style fields. Verify the normalized style/hash/capability response before approving any render.
4178
4527
  - Treat paid generation, publishing, deletion, broad mutation, and \`requiresConfirmation\` responses as user approval checkpoints.
4179
4528
  - Treat exit code 13 as insufficient credits or an API key spend-limit block; top up billing or adjust the key's spend limit in ClipIt Settings.
4180
4529
  - When credits are insufficient, discover payment rails with \`clipit billing capabilities --json\` and \`clipit billing catalog --json\`.
@@ -4190,6 +4539,9 @@ Useful commands:
4190
4539
  clipit doctor --json
4191
4540
  clipit auth status --json
4192
4541
  clipit skills list --json
4542
+ clipit skills describe broll --json
4543
+ clipit skills manifest --json
4544
+ clipit media-guides list --json
4193
4545
  clipit tools list --json
4194
4546
  clipit tools describe <functionName> --json
4195
4547
  clipit mcp stdio
@@ -4217,6 +4569,7 @@ clipit analytics overview --days 30 --json
4217
4569
  clipit exports start --clip-id <clipId> --confirm --json
4218
4570
  clipit thumbnails generate --clip-id <clipId> --prompt "High contrast thumbnail" --confirm --json
4219
4571
  clipit social post --clip-id <clipId> --platforms x,tiktok --account-ids x=<xAccountId>,tiktok=<tiktokAccountId> --caption "New clip" --confirm --json
4572
+ clipit run setCaptionStyle --clip-id <clipId> --params @caption-style.json --json
4220
4573
  clipit run renderClipWithRemotion --clip-id <clipId> --params @params.json --confirm --json
4221
4574
  \`\`\`
4222
4575
 
@@ -4236,6 +4589,7 @@ function mcpSkillAddendum() {
4236
4589
  '## MCP Stdio Bridge',
4237
4590
  '- If your agent runtime supports MCP stdio servers, prefer launching `clipit mcp stdio` from the user\'s machine instead of manually shelling every command.',
4238
4591
  '- The bridge uses the authenticated CLI profile, speaks standard `Content-Length` framed JSON-RPC over stdio, and exposes the same live tools as `clipit tools list --json` / `GET /api/v1/agent/tools`.',
4592
+ '- Use `resources/list` and `resources/read` for `clipit://instructions`, `clipit://manifest`, `clipit://skills/<id>`, and `clipit://media-guides/<id>` so only the relevant guide enters context.',
4239
4593
  '- `tools/call` delegates to `/api/v1/agent/execute`, inherits the active CLI context from `clipit context use`, and preserves confirmation gates for paid generation, publishing, deletion, export, and other mutating calls.',
4240
4594
  '- The bridge also exposes local billing discovery tools: `getPaymentCapabilities`, `getBillingCatalog`, `createPaymentAttempt`, `getPaymentAttempt`, `getPaymentReceipt`, and `getBillingSubscription`.',
4241
4595
  '- `createPaymentAttempt` returns a payment URL for direct x402, Stripe-managed x402, or Stripe MPP/Link SPT settlement and requires `confirmed:true`; do not retry it until the human approves the product, amount, rail, and budget policy.',
@@ -4257,6 +4611,7 @@ function fallbackSkillResult(target, reason) {
4257
4611
  const generatedAt = new Date().toISOString();
4258
4612
  return {
4259
4613
  markdown: skillMarkdown(target),
4614
+ references: [],
4260
4615
  source: 'fallback',
4261
4616
  fallbackReason: reason,
4262
4617
  meta: {
@@ -4302,9 +4657,27 @@ async function resolveAgentSkill(config, options, target) {
4302
4657
  });
4303
4658
  }
4304
4659
 
4660
+ if (response.meta.minimumCliVersion !== undefined) {
4661
+ const compatibility = compareSemver(VERSION, response.meta.minimumCliVersion);
4662
+ if (compatibility === null) {
4663
+ throw Object.assign(new Error('Instructions endpoint returned an invalid minimum CLI version.'), {
4664
+ exitCode: EXIT.SERVER,
4665
+ data: { minimumCliVersion: response.meta.minimumCliVersion },
4666
+ });
4667
+ }
4668
+ if (compatibility < 0) {
4669
+ throw Object.assign(new Error(
4670
+ `ClipIt CLI ${VERSION} is too old for agent pack ${response.meta.packVersion || 'current'}; install @clipit-ai/cli@latest (minimum ${response.meta.minimumCliVersion}).`,
4671
+ ), { exitCode: EXIT.SERVER });
4672
+ }
4673
+ }
4674
+
4675
+ const references = validateAgentInstructionReferences(response.references);
4676
+
4305
4677
  const augmented = withLocalCliSkillAddenda(response.markdown);
4306
4678
  return {
4307
4679
  markdown: augmented.markdown,
4680
+ references,
4308
4681
  source: 'server',
4309
4682
  meta: compactObject({
4310
4683
  ...response.meta,
@@ -4325,12 +4698,83 @@ async function readAgentSkillMeta(baseDir) {
4325
4698
  }
4326
4699
  }
4327
4700
 
4701
+ function safeAgentReferencePath(value) {
4702
+ if (typeof value !== 'string') return null;
4703
+ if (value.includes('\0')) return null;
4704
+ const normalized = path.posix.normalize(value.replace(/\\/g, '/'));
4705
+ if (
4706
+ !normalized.startsWith('references/')
4707
+ || normalized === 'references/'
4708
+ || normalized.endsWith('/')
4709
+ || normalized.includes('../')
4710
+ || path.posix.isAbsolute(normalized)
4711
+ ) {
4712
+ return null;
4713
+ }
4714
+ return normalized;
4715
+ }
4716
+
4717
+ function validateAgentInstructionReferences(value) {
4718
+ if (value === undefined) return [];
4719
+ if (!Array.isArray(value) || value.length > 256) {
4720
+ throw Object.assign(new Error('Instructions endpoint returned an invalid references collection.'), {
4721
+ exitCode: EXIT.SERVER,
4722
+ });
4723
+ }
4724
+
4725
+ const references = [];
4726
+ const paths = new Set();
4727
+ let totalBytes = 0;
4728
+ for (const entry of value) {
4729
+ const relativePath = safeAgentReferencePath(entry?.path);
4730
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !relativePath || typeof entry.content !== 'string') {
4731
+ throw Object.assign(new Error('Instructions endpoint returned an invalid or unsafe generated reference.'), {
4732
+ exitCode: EXIT.SERVER,
4733
+ });
4734
+ }
4735
+ if (paths.has(relativePath)) {
4736
+ throw Object.assign(new Error(`Instructions endpoint returned duplicate reference path: ${relativePath}`), {
4737
+ exitCode: EXIT.SERVER,
4738
+ });
4739
+ }
4740
+ paths.add(relativePath);
4741
+ totalBytes += Buffer.byteLength(entry.content, 'utf8');
4742
+ if (totalBytes > 10 * 1024 * 1024) {
4743
+ throw Object.assign(new Error('Instructions endpoint references exceed the 10 MiB install limit.'), {
4744
+ exitCode: EXIT.SERVER,
4745
+ });
4746
+ }
4747
+
4748
+ const actualSha256 = hashText(entry.content);
4749
+ if (entry.sha256 !== undefined && (typeof entry.sha256 !== 'string' || entry.sha256.toLowerCase() !== actualSha256)) {
4750
+ throw Object.assign(new Error(`Instructions endpoint reference checksum mismatch: ${relativePath}`), {
4751
+ exitCode: EXIT.SERVER,
4752
+ });
4753
+ }
4754
+ references.push({ ...entry, path: relativePath, sha256: actualSha256 });
4755
+ }
4756
+ return references;
4757
+ }
4758
+
4759
+ function agentSkillContentHash(markdown, references = []) {
4760
+ const referenceContent = references
4761
+ .map((entry) => ({ path: safeAgentReferencePath(entry.path), content: String(entry.content || '') }))
4762
+ .filter((entry) => entry.path)
4763
+ .sort((left, right) => left.path.localeCompare(right.path));
4764
+ return hashText(JSON.stringify({ markdown: String(markdown || ''), references: referenceContent }));
4765
+ }
4766
+
4328
4767
  function agentSkillSidecar(target, skill) {
4329
4768
  return compactObject({
4330
4769
  target,
4331
4770
  source: skill.source,
4332
4771
  generatedAt: skill.meta?.generatedAt,
4333
4772
  markdownHash: hashText(skill.markdown),
4773
+ contentHash: agentSkillContentHash(skill.markdown, skill.references),
4774
+ references: skill.references?.map((entry) => compactObject({
4775
+ path: safeAgentReferencePath(entry.path),
4776
+ sha256: entry.sha256 || hashText(entry.content),
4777
+ })).filter((entry) => entry.path),
4334
4778
  serverMeta: skill.source === 'server' ? skill.meta : undefined,
4335
4779
  fallbackReason: skill.fallbackReason,
4336
4780
  });
@@ -4360,6 +4804,19 @@ async function installedAgentStatus(target, options) {
4360
4804
  fs.readFile(skillPath, 'utf8'),
4361
4805
  ]);
4362
4806
  const sidecar = await readAgentSkillMeta(baseDir);
4807
+ const references = Array.isArray(sidecar?.references)
4808
+ ? await Promise.all(sidecar.references.map(async (entry) => {
4809
+ const relativePath = safeAgentReferencePath(entry?.path);
4810
+ if (!relativePath) return null;
4811
+ try {
4812
+ return { path: relativePath, content: await fs.readFile(path.join(baseDir, relativePath), 'utf8') };
4813
+ } catch {
4814
+ return { path: relativePath, content: null };
4815
+ }
4816
+ }))
4817
+ : [];
4818
+ const missingReferences = references.filter((entry) => entry && entry.content === null).map((entry) => entry.path);
4819
+ const installedReferences = references.filter((entry) => entry && typeof entry.content === 'string');
4363
4820
  return {
4364
4821
  target,
4365
4822
  installed: true,
@@ -4369,6 +4826,11 @@ async function installedAgentStatus(target, options) {
4369
4826
  generatedAt: sidecar?.generatedAt || null,
4370
4827
  fallbackReason: sidecar?.fallbackReason || null,
4371
4828
  markdownHash: sidecar?.markdownHash || hashText(markdown),
4829
+ contentHash: missingReferences.length === 0
4830
+ ? agentSkillContentHash(markdown, installedReferences)
4831
+ : null,
4832
+ referenceCount: references.length,
4833
+ missingReferences,
4372
4834
  serverMeta: sidecar?.serverMeta || null,
4373
4835
  };
4374
4836
  } catch {
@@ -4381,6 +4843,14 @@ function agentFreshnessReason(status, currentSkill) {
4381
4843
  if (status.skillSource !== currentSkill.source) {
4382
4844
  return `installed source ${status.skillSource || 'unknown'} differs from current source ${currentSkill.source}`;
4383
4845
  }
4846
+ if (status.missingReferences?.length) {
4847
+ return `installed skill is missing ${status.missingReferences.length} generated reference file(s)`;
4848
+ }
4849
+ if (status.serverMeta?.manifestHash
4850
+ && currentMeta?.manifestHash
4851
+ && status.serverMeta.manifestHash !== currentMeta.manifestHash) {
4852
+ return 'installed capability manifest differs from the live registry';
4853
+ }
4384
4854
  if (status.serverMeta?.toolCount !== undefined
4385
4855
  && currentMeta?.toolCount !== undefined
4386
4856
  && status.serverMeta.toolCount !== currentMeta.toolCount) {
@@ -4394,6 +4864,28 @@ function agentFreshnessReason(status, currentSkill) {
4394
4864
  return 'installed skill content differs from current generated instructions';
4395
4865
  }
4396
4866
 
4867
+ async function writeAgentReferences(baseDir, references, previousMeta) {
4868
+ const currentPaths = new Set();
4869
+ for (const entry of references || []) {
4870
+ const relativePath = safeAgentReferencePath(entry.path);
4871
+ if (!relativePath) {
4872
+ throw Object.assign(new Error(`Instructions endpoint returned an unsafe reference path: ${entry.path}`), {
4873
+ exitCode: EXIT.SERVER,
4874
+ });
4875
+ }
4876
+ currentPaths.add(relativePath);
4877
+ const destination = path.join(baseDir, relativePath);
4878
+ await fs.mkdir(path.dirname(destination), { recursive: true });
4879
+ await fs.writeFile(destination, entry.content, 'utf8');
4880
+ }
4881
+
4882
+ for (const previous of previousMeta?.references || []) {
4883
+ const relativePath = safeAgentReferencePath(previous?.path);
4884
+ if (!relativePath || currentPaths.has(relativePath)) continue;
4885
+ await fs.rm(path.join(baseDir, relativePath), { force: true });
4886
+ }
4887
+ }
4888
+
4397
4889
  async function agent(config, options, action, args) {
4398
4890
  const target = args[0] || 'generic';
4399
4891
  const baseDir = agentInstallDir(target, options);
@@ -4435,8 +4927,8 @@ async function agent(config, options, action, args) {
4435
4927
  status.stale = null;
4436
4928
  status.freshnessError = `Cannot verify installed skill freshness: ${currentSkill.fallbackReason || 'current instructions unavailable'}`;
4437
4929
  } else {
4438
- const currentHash = hashText(currentSkill.markdown);
4439
- status.upToDate = status.markdownHash === currentHash;
4930
+ const currentHash = agentSkillContentHash(currentSkill.markdown, currentSkill.references);
4931
+ status.upToDate = status.contentHash === currentHash;
4440
4932
  status.stale = !status.upToDate;
4441
4933
  if (status.stale) {
4442
4934
  status.staleReason = agentFreshnessReason(status, currentSkill);
@@ -4452,7 +4944,9 @@ async function agent(config, options, action, args) {
4452
4944
  const skill = await resolveAgentSkill(config, options, target);
4453
4945
  printFallbackNotice(skill, true);
4454
4946
  await fs.mkdir(baseDir, { recursive: true });
4947
+ const previousMeta = await readAgentSkillMeta(baseDir);
4455
4948
  await fs.writeFile(path.join(baseDir, 'SKILL.md'), skill.markdown, 'utf8');
4949
+ await writeAgentReferences(baseDir, skill.references, previousMeta);
4456
4950
  await fs.writeFile(agentSkillMetaPath(baseDir), `${JSON.stringify(agentSkillSidecar(target, skill), null, 2)}\n`, 'utf8');
4457
4951
  output({
4458
4952
  success: true,
@@ -4461,6 +4955,7 @@ async function agent(config, options, action, args) {
4461
4955
  path: baseDir,
4462
4956
  skillSource: skill.source,
4463
4957
  generatedAt: skill.meta?.generatedAt || null,
4958
+ referenceCount: skill.references?.length || 0,
4464
4959
  fallback: skill.source === 'fallback',
4465
4960
  }, options);
4466
4961
  return;
@@ -4501,6 +4996,9 @@ async function main() {
4501
4996
  if (command === 'auth' && subcommand === 'use') return useProfile(config, options, rest[0]);
4502
4997
  if (command === 'context') return contextCommand(config, options, subcommand);
4503
4998
  if (command === 'skills' && subcommand === 'list') return listSkills(config, options);
4999
+ if (command === 'skills' && subcommand === 'describe') return describeSkill(config, options, rest[0]);
5000
+ if (command === 'skills' && subcommand === 'manifest') return capabilityManifest(config, options);
5001
+ if (command === 'media-guides') return mediaGuides(config, options, subcommand, rest[0]);
4504
5002
  if (command === 'tools' && subcommand === 'list') return listTools(config, options);
4505
5003
  if (command === 'tools' && subcommand === 'describe') return describeTool(config, options, rest[0]);
4506
5004
  if (command === 'ask') return askWorkflow(config, options, [subcommand, ...rest]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipit-ai/cli",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "ClipIt CLI for connecting shell-capable agents to ClipIt.",
5
5
  "type": "module",
6
6
  "bin": {