@clipit-ai/cli 0.2.10 → 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.10';
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,19 @@ 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
+ }
488
528
  if (command === 'clips' && subcommand === 'initialize-snapshot') {
489
529
  return [
490
530
  'Usage:',
@@ -499,7 +539,7 @@ function commandUsage(command, subcommand) {
499
539
  ' --quality <quality> standard, high, or 4k',
500
540
  ' --captions <true|false> Explicitly enable or disable captions',
501
541
  ' --caption-style <style> bold, minimal, neon, or classic',
502
- ' --params <json|@file> Supply the full initializer request, including captionPresetId',
542
+ ' --params <json|@file> Supply the full initializer request, including captionPresetId or a strict rich captionStyle object',
503
543
  ' --profile <name> Named ClipIt credential profile',
504
544
  ' --json Emit machine-readable JSON',
505
545
  '',
@@ -551,19 +591,17 @@ function commandUsage(command, subcommand) {
551
591
  if (command === 'deliverables' && subcommand === 'create') {
552
592
  return [
553
593
  'Usage:',
554
- ' clipit deliverables create --export-id <id> --title <text> --confirm',
594
+ ' clipit deliverables create --export-id <id> --confirm',
555
595
  '',
556
596
  'Required:',
557
597
  ' --export-id <id> Exact completed export to deliver',
558
- ' --title <text> Client-facing deliverable title',
559
598
  ' --confirm Confirm the delivery mutation',
560
599
  '',
561
600
  'Optional:',
562
- ' --note <text> Client-facing note',
563
601
  ' --profile <name> Named ClipIt credential profile',
564
602
  ' --json Emit machine-readable JSON',
565
603
  '',
566
- '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.',
567
605
  ].join('\n');
568
606
  }
569
607
  return null;
@@ -1040,8 +1078,9 @@ function normalizeCreditEstimateRequest(request) {
1040
1078
  }
1041
1079
 
1042
1080
  function brollVideoProviderCostUsd(durationSeconds, resolution) {
1043
- const perSecondUsd = resolution === '1080p' ? 0.28 : 0.14;
1044
- 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;
1045
1084
  }
1046
1085
 
1047
1086
  function brollImageProviderCostUsd(quality) {
@@ -1059,10 +1098,8 @@ function ttsProviderCostUsd(text, prompt) {
1059
1098
  }
1060
1099
 
1061
1100
  function videoAlterProviderCostUsd(durationSeconds, mode) {
1062
- const perSecond = mode === 'pro'
1063
- ? Number(process.env.KLING_OMNI_PRO_COST_PER_SECOND_USD ?? '0.56')
1064
- : Number(process.env.KLING_OMNI_STANDARD_COST_PER_SECOND_USD ?? '0.28');
1065
- 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);
1066
1103
  }
1067
1104
 
1068
1105
  function durationFromClip(clip, body = {}) {
@@ -1136,7 +1173,7 @@ async function buildMaxCreditsEstimateRequest(config, options, spec, context = {
1136
1173
 
1137
1174
  if (spec.metrics === 'broll-images') {
1138
1175
  const mode = context.body?.mode || 'single_image';
1139
- const generationCount = mode === 'start_end_frame' ? 3 : 1;
1176
+ const generationCount = mode === 'start_end_frame' ? 2 : 1;
1140
1177
  return {
1141
1178
  ...spec,
1142
1179
  metrics: {
@@ -1148,7 +1185,7 @@ async function buildMaxCreditsEstimateRequest(config, options, spec, context = {
1148
1185
 
1149
1186
  if (spec.metrics === 'broll-video') {
1150
1187
  const durationSeconds = Number(context.body?.durationSeconds ?? context.body?.duration ?? 6);
1151
- const resolution = context.body?.resolution || '720p';
1188
+ const resolution = context.body?.resolution || '768p';
1152
1189
  if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return null;
1153
1190
  return {
1154
1191
  ...spec,
@@ -1198,7 +1235,7 @@ async function buildMaxCreditsEstimateRequest(config, options, spec, context = {
1198
1235
  return null;
1199
1236
  }
1200
1237
 
1201
- async function buildStaticRunEstimates(config, options, functionName, parameters, payload) {
1238
+ async function buildStaticRunEstimates(config, options, functionName, parameters, payload, maxCredits = null) {
1202
1239
  const specs = RUN_MAX_CREDITS_ESTIMATE_MAP[functionName];
1203
1240
  if (!specs) return null;
1204
1241
 
@@ -1221,12 +1258,19 @@ async function buildStaticRunEstimates(config, options, functionName, parameters
1221
1258
 
1222
1259
  const estimates = [];
1223
1260
  for (const request of requests) {
1224
- 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
+ }));
1225
1265
  estimates.push({
1226
1266
  operationType: request.operationType,
1227
1267
  provider: request.provider,
1228
1268
  modelId: request.modelId,
1229
- 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,
1230
1274
  affordable: estimate?.affordable,
1231
1275
  spendLimitViolation: estimate?.spendLimitViolation ?? null,
1232
1276
  });
@@ -1257,7 +1301,7 @@ function throwMaxCreditsEstimateUnavailable(commandKey) {
1257
1301
 
1258
1302
  function estimatedClipCostFromData(data) {
1259
1303
  if (!data || typeof data !== 'object') return null;
1260
- for (const key of ['estimatedCostClip', 'totalEstimatedCostClip', 'costClip', 'totalCostClip']) {
1304
+ for (const key of ['internalEstimatedUsageClip', 'estimatedCostClip', 'totalEstimatedCostClip', 'costClip', 'totalCostClip']) {
1261
1305
  const value = Number(data[key]);
1262
1306
  if (Number.isFinite(value)) return value;
1263
1307
  }
@@ -1293,12 +1337,19 @@ async function enforceMaxCredits(config, options, commandKey, context = {}) {
1293
1337
 
1294
1338
  const estimates = [];
1295
1339
  for (const request of requests) {
1296
- 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
+ });
1297
1344
  estimates.push({
1298
1345
  operationType: request.operationType,
1299
1346
  provider: request.provider,
1300
1347
  modelId: request.modelId,
1301
- 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,
1302
1353
  affordable: estimate?.affordable,
1303
1354
  spendLimitViolation: estimate?.spendLimitViolation ?? null,
1304
1355
  });
@@ -1309,15 +1360,18 @@ async function enforceMaxCredits(config, options, commandKey, context = {}) {
1309
1360
  warnIfEstimateUnaffordable(estimates);
1310
1361
 
1311
1362
  const estimatedCostClip = estimates.reduce((sum, estimate) => sum + estimate.estimatedCostClip, 0);
1312
- if (estimatedCostClip > limit) {
1363
+ const clientCreditChargeClip = estimates.reduce((sum, estimate) => sum + estimate.clientCreditChargeClip, 0);
1364
+ if (estimatedCostClip > limit || estimates.some((estimate) => !estimate.withinApprovalCap)) {
1313
1365
  throw Object.assign(
1314
- 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)}.`),
1315
1367
  {
1316
1368
  exitCode: EXIT.CONFIRMATION,
1317
1369
  data: {
1318
1370
  command: commandKey,
1319
1371
  maxCredits: limit,
1320
1372
  estimatedCostClip,
1373
+ internalEstimatedUsageClip: estimatedCostClip,
1374
+ clientCreditChargeClip,
1321
1375
  estimates,
1322
1376
  },
1323
1377
  },
@@ -1349,7 +1403,7 @@ async function enforceRunMaxCredits(config, options, functionName, parameters =
1349
1403
 
1350
1404
  let staticEstimates = null;
1351
1405
  if (!runEstimate && (limit !== null || needsConfirmationPreflight)) {
1352
- staticEstimates = await buildStaticRunEstimates(config, options, functionName, parameters, payload);
1406
+ staticEstimates = await buildStaticRunEstimates(config, options, functionName, parameters, payload, limit);
1353
1407
  }
1354
1408
 
1355
1409
  if (limit === null) {
@@ -1387,7 +1441,7 @@ async function enforceRunMaxCredits(config, options, functionName, parameters =
1387
1441
  warnIfEstimateUnaffordable(staticEstimates);
1388
1442
 
1389
1443
  const estimatedCostClip = staticEstimates.reduce((sum, estimate) => sum + estimate.estimatedCostClip, 0);
1390
- if (estimatedCostClip > limit) {
1444
+ if (estimatedCostClip > limit || staticEstimates.some((estimate) => !estimate.withinApprovalCap)) {
1391
1445
  throw Object.assign(
1392
1446
  new Error(`Estimated cost ${clipCostLabel(estimatedCostClip)} exceeds --max-credits ${clipCostLabel(limit)} for run ${functionName}.`),
1393
1447
  {
@@ -1665,6 +1719,28 @@ async function listSkills(config, options) {
1665
1719
  output(await apiFetch(config, options, 'GET', '/api/v1/agent/skills'), options);
1666
1720
  }
1667
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
+
1668
1744
  async function listTools(config, options) {
1669
1745
  const params = new URLSearchParams();
1670
1746
  if (options.skill) params.set('skill', String(options.skill));
@@ -1809,6 +1885,57 @@ function normalizeAgentExecuteResult(result) {
1809
1885
  });
1810
1886
  }
1811
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
+
1812
1939
  async function runTool(config, options, functionName) {
1813
1940
  if (!functionName) throw Object.assign(new Error('Function name is required.'), { exitCode: EXIT.USAGE });
1814
1941
  if (RUN_CONFIRMATION_LABELS[functionName]) {
@@ -1847,6 +1974,19 @@ async function runTool(config, options, functionName) {
1847
1974
  process.exitCode = EXIT.CONFIRMATION;
1848
1975
  return;
1849
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
+ }
1850
1990
  output(outputResult, options);
1851
1991
  if (result?.success === false) {
1852
1992
  process.exitCode = EXIT.SERVER;
@@ -3150,7 +3290,7 @@ async function exportsCommand(config, options, action, args) {
3150
3290
  throw Object.assign(new Error(`Unknown exports command: ${action || ''}`), { exitCode: EXIT.USAGE });
3151
3291
  }
3152
3292
 
3153
- function enterpriseClientSelectionUrl(config, options, selectionPath = '/enterprise?tab=delivered') {
3293
+ function enterpriseClientSelectionUrl(config, options, selectionPath = '/enterprise/workspace?tab=delivered') {
3154
3294
  return new URL(selectionPath, getBaseUrl(config, options)).toString();
3155
3295
  }
3156
3296
 
@@ -3170,16 +3310,55 @@ async function deliverables(config, options, action) {
3170
3310
  }
3171
3311
  if (action === 'create') {
3172
3312
  const exportId = requiredString(options['export-id'], '--export-id');
3173
- const title = requiredString(options.title, '--title');
3174
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
+ }
3175
3355
  const result = await apiFetch(config, options, 'POST', '/api/v1/deliverables', {
3176
3356
  exportId,
3177
- title,
3178
- note: options.note,
3357
+ expectations,
3179
3358
  });
3180
3359
  output({
3181
3360
  ...result,
3182
- clientSelectionPath: '/enterprise?tab=delivered',
3361
+ clientSelectionPath: '/enterprise/workspace?tab=delivered',
3183
3362
  clientSelectionUrl: enterpriseClientSelectionUrl(config, options),
3184
3363
  }, options);
3185
3364
  return;
@@ -3451,7 +3630,7 @@ async function resolveEnterpriseSelectedDelivery(config, options, clipId, export
3451
3630
  const current = deliveries.find((delivery) => (
3452
3631
  delivery?.clipId === clipId && delivery?.exportId === exportId
3453
3632
  )) ?? null;
3454
- const clientSelectionPath = result?.clientSelectionPath || '/enterprise?tab=delivered';
3633
+ const clientSelectionPath = result?.clientSelectionPath || '/enterprise/workspace?tab=delivered';
3455
3634
  throw Object.assign(
3456
3635
  new Error('The client has not selected this delivered export for publishing.'),
3457
3636
  {
@@ -3904,6 +4083,91 @@ async function handleLocalMcpBillingTool(config, options, name, parameters = {})
3904
4083
  throw Object.assign(new Error(`Unknown local billing tool: ${name}`), { exitCode: EXIT.USAGE });
3905
4084
  }
3906
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
+
3907
4171
  async function handleMcpRequest(config, options, message) {
3908
4172
  if (!message || typeof message !== 'object' || Array.isArray(message)) {
3909
4173
  return { jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Invalid JSON-RPC request.' } };
@@ -3920,7 +4184,10 @@ async function handleMcpRequest(config, options, message) {
3920
4184
  id,
3921
4185
  result: {
3922
4186
  protocolVersion: message.params?.protocolVersion || '2024-11-05',
3923
- capabilities: { tools: {} },
4187
+ capabilities: {
4188
+ tools: {},
4189
+ resources: { subscribe: false, listChanged: false },
4190
+ },
3924
4191
  serverInfo: { name: 'clipit', version: VERSION },
3925
4192
  },
3926
4193
  };
@@ -3943,6 +4210,30 @@ async function handleMcpRequest(config, options, message) {
3943
4210
  };
3944
4211
  }
3945
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
+
3946
4237
  if (method === 'tools/call') {
3947
4238
  const name = typeof message.params?.name === 'string' ? message.params.name : null;
3948
4239
  if (!name) {
@@ -4193,6 +4484,8 @@ async function examples(options) {
4193
4484
  thumbnail: 'clipit thumbnails generate --clip-id <clipId> --prompt "Expressive high-contrast thumbnail" --confirm --json',
4194
4485
  brollPlan: 'clipit broll plan <clipId> --count 3 --confirm --json',
4195
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',
4196
4489
  runTool: 'clipit run <functionName> --clip-id <clipId> --params @params.json --json',
4197
4490
  reviewLink: 'clipit links clip <clipId> --json',
4198
4491
  };
@@ -4204,7 +4497,14 @@ async function examples(options) {
4204
4497
  }
4205
4498
 
4206
4499
  function skillMarkdown(agent) {
4207
- 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
4208
4508
 
4209
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.
4210
4510
 
@@ -4212,12 +4512,18 @@ Rules:
4212
4512
  - Run \`clipit auth status --json\` before assuming ClipIt is connected.
4213
4513
  - If not connected, ask the user to run \`clipit login\` and approve the browser link.
4214
4514
  - Never ask the user to paste API keys into chat.
4215
- - Use \`clipit skills list --json\` and \`clipit tools list --json\` to discover capability.
4216
- - 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.
4217
4522
  - Prefer friendly commands such as \`clipit videos list --json\`, \`clipit clips list --json\`, \`clipit credits balance --json\`, and \`clipit jobs wait <jobId> --json\`.
4218
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.
4219
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.
4220
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.
4221
4527
  - Treat paid generation, publishing, deletion, broad mutation, and \`requiresConfirmation\` responses as user approval checkpoints.
4222
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.
4223
4529
  - When credits are insufficient, discover payment rails with \`clipit billing capabilities --json\` and \`clipit billing catalog --json\`.
@@ -4233,6 +4539,9 @@ Useful commands:
4233
4539
  clipit doctor --json
4234
4540
  clipit auth status --json
4235
4541
  clipit skills list --json
4542
+ clipit skills describe broll --json
4543
+ clipit skills manifest --json
4544
+ clipit media-guides list --json
4236
4545
  clipit tools list --json
4237
4546
  clipit tools describe <functionName> --json
4238
4547
  clipit mcp stdio
@@ -4260,6 +4569,7 @@ clipit analytics overview --days 30 --json
4260
4569
  clipit exports start --clip-id <clipId> --confirm --json
4261
4570
  clipit thumbnails generate --clip-id <clipId> --prompt "High contrast thumbnail" --confirm --json
4262
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
4263
4573
  clipit run renderClipWithRemotion --clip-id <clipId> --params @params.json --confirm --json
4264
4574
  \`\`\`
4265
4575
 
@@ -4279,6 +4589,7 @@ function mcpSkillAddendum() {
4279
4589
  '## MCP Stdio Bridge',
4280
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.',
4281
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.',
4282
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.',
4283
4594
  '- The bridge also exposes local billing discovery tools: `getPaymentCapabilities`, `getBillingCatalog`, `createPaymentAttempt`, `getPaymentAttempt`, `getPaymentReceipt`, and `getBillingSubscription`.',
4284
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.',
@@ -4300,6 +4611,7 @@ function fallbackSkillResult(target, reason) {
4300
4611
  const generatedAt = new Date().toISOString();
4301
4612
  return {
4302
4613
  markdown: skillMarkdown(target),
4614
+ references: [],
4303
4615
  source: 'fallback',
4304
4616
  fallbackReason: reason,
4305
4617
  meta: {
@@ -4345,9 +4657,27 @@ async function resolveAgentSkill(config, options, target) {
4345
4657
  });
4346
4658
  }
4347
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
+
4348
4677
  const augmented = withLocalCliSkillAddenda(response.markdown);
4349
4678
  return {
4350
4679
  markdown: augmented.markdown,
4680
+ references,
4351
4681
  source: 'server',
4352
4682
  meta: compactObject({
4353
4683
  ...response.meta,
@@ -4368,12 +4698,83 @@ async function readAgentSkillMeta(baseDir) {
4368
4698
  }
4369
4699
  }
4370
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
+
4371
4767
  function agentSkillSidecar(target, skill) {
4372
4768
  return compactObject({
4373
4769
  target,
4374
4770
  source: skill.source,
4375
4771
  generatedAt: skill.meta?.generatedAt,
4376
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),
4377
4778
  serverMeta: skill.source === 'server' ? skill.meta : undefined,
4378
4779
  fallbackReason: skill.fallbackReason,
4379
4780
  });
@@ -4403,6 +4804,19 @@ async function installedAgentStatus(target, options) {
4403
4804
  fs.readFile(skillPath, 'utf8'),
4404
4805
  ]);
4405
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');
4406
4820
  return {
4407
4821
  target,
4408
4822
  installed: true,
@@ -4412,6 +4826,11 @@ async function installedAgentStatus(target, options) {
4412
4826
  generatedAt: sidecar?.generatedAt || null,
4413
4827
  fallbackReason: sidecar?.fallbackReason || null,
4414
4828
  markdownHash: sidecar?.markdownHash || hashText(markdown),
4829
+ contentHash: missingReferences.length === 0
4830
+ ? agentSkillContentHash(markdown, installedReferences)
4831
+ : null,
4832
+ referenceCount: references.length,
4833
+ missingReferences,
4415
4834
  serverMeta: sidecar?.serverMeta || null,
4416
4835
  };
4417
4836
  } catch {
@@ -4424,6 +4843,14 @@ function agentFreshnessReason(status, currentSkill) {
4424
4843
  if (status.skillSource !== currentSkill.source) {
4425
4844
  return `installed source ${status.skillSource || 'unknown'} differs from current source ${currentSkill.source}`;
4426
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
+ }
4427
4854
  if (status.serverMeta?.toolCount !== undefined
4428
4855
  && currentMeta?.toolCount !== undefined
4429
4856
  && status.serverMeta.toolCount !== currentMeta.toolCount) {
@@ -4437,6 +4864,28 @@ function agentFreshnessReason(status, currentSkill) {
4437
4864
  return 'installed skill content differs from current generated instructions';
4438
4865
  }
4439
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
+
4440
4889
  async function agent(config, options, action, args) {
4441
4890
  const target = args[0] || 'generic';
4442
4891
  const baseDir = agentInstallDir(target, options);
@@ -4478,8 +4927,8 @@ async function agent(config, options, action, args) {
4478
4927
  status.stale = null;
4479
4928
  status.freshnessError = `Cannot verify installed skill freshness: ${currentSkill.fallbackReason || 'current instructions unavailable'}`;
4480
4929
  } else {
4481
- const currentHash = hashText(currentSkill.markdown);
4482
- status.upToDate = status.markdownHash === currentHash;
4930
+ const currentHash = agentSkillContentHash(currentSkill.markdown, currentSkill.references);
4931
+ status.upToDate = status.contentHash === currentHash;
4483
4932
  status.stale = !status.upToDate;
4484
4933
  if (status.stale) {
4485
4934
  status.staleReason = agentFreshnessReason(status, currentSkill);
@@ -4495,7 +4944,9 @@ async function agent(config, options, action, args) {
4495
4944
  const skill = await resolveAgentSkill(config, options, target);
4496
4945
  printFallbackNotice(skill, true);
4497
4946
  await fs.mkdir(baseDir, { recursive: true });
4947
+ const previousMeta = await readAgentSkillMeta(baseDir);
4498
4948
  await fs.writeFile(path.join(baseDir, 'SKILL.md'), skill.markdown, 'utf8');
4949
+ await writeAgentReferences(baseDir, skill.references, previousMeta);
4499
4950
  await fs.writeFile(agentSkillMetaPath(baseDir), `${JSON.stringify(agentSkillSidecar(target, skill), null, 2)}\n`, 'utf8');
4500
4951
  output({
4501
4952
  success: true,
@@ -4504,6 +4955,7 @@ async function agent(config, options, action, args) {
4504
4955
  path: baseDir,
4505
4956
  skillSource: skill.source,
4506
4957
  generatedAt: skill.meta?.generatedAt || null,
4958
+ referenceCount: skill.references?.length || 0,
4507
4959
  fallback: skill.source === 'fallback',
4508
4960
  }, options);
4509
4961
  return;
@@ -4544,6 +4996,9 @@ async function main() {
4544
4996
  if (command === 'auth' && subcommand === 'use') return useProfile(config, options, rest[0]);
4545
4997
  if (command === 'context') return contextCommand(config, options, subcommand);
4546
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]);
4547
5002
  if (command === 'tools' && subcommand === 'list') return listTools(config, options);
4548
5003
  if (command === 'tools' && subcommand === 'describe') return describeTool(config, options, rest[0]);
4549
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.10",
3
+ "version": "0.3.0",
4
4
  "description": "ClipIt CLI for connecting shell-capable agents to ClipIt.",
5
5
  "type": "module",
6
6
  "bin": {