@clipit-ai/cli 0.2.6 → 0.2.8

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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/bin/clipit.mjs +150 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -110,6 +110,9 @@ clipit exports wait <jobId> --stream
110
110
  clipit exports download <jobId> --open
111
111
  clipit exports cancel <jobId> --confirm --json
112
112
 
113
+ clipit deliverables create --export-id <exportId> --title "Client-ready clip" --confirm --json
114
+ clipit deliverables list --status selected --json
115
+
113
116
  clipit assets list --json
114
117
  clipit assets upload ./brand-logo.png --kind image --confirm --json
115
118
  clipit assets delete <assetId> --confirm --json
@@ -131,6 +134,8 @@ clipit social get <postId> --json
131
134
  clipit social cancel <postId> --confirm --json
132
135
  ```
133
136
 
137
+ For enterprise workspace profiles, create a ready delivery after the exact export completes, then send the returned `clientSelectionUrl` to the client. Publishing and scheduling remain blocked until that client selects the delivery. `clipit deliverables list --export-id <exportId> --json` shows the current delivery state; after selection, the social commands automatically pin the exact delivery ID and granted social account.
138
+
134
139
  ## Reliability And Spend Guards
135
140
 
136
141
  The CLI retries only idempotent `GET` requests, with at most two retries for network failures, `429`, `502`, `503`, and `504`. `Retry-After` is honored up to 10 seconds. `POST`, `PATCH`, and `DELETE` requests are never retried. Pass `--no-retry` to disable GET retries.
@@ -275,6 +280,7 @@ Only use `--allow-custom-host` for hosts you control or trust. It permits the CL
275
280
  | Credits | `credits balance`, `credits usage`, `credits estimate` | Estimate accepts `--metrics @file.json`. |
276
281
  | Analytics | `analytics overview`, `analytics top-clips`, `analytics post` | `overview` returns aggregate and by-platform metrics. |
277
282
  | Exports | `exports start/list/get/wait/download/cancel` | Export jobs poll through `/api/v1/exports/:jobId`; paid start requires `--confirm`. |
283
+ | Enterprise deliveries | `deliverables list/create` | Workspace profiles can deliver exact exports and inspect client selection; only the client can select. |
278
284
  | Assets | `assets list/upload/delete` | Upload signs, PUTs the file to object storage, then finalizes the library asset. Delete requires `--confirm`. |
279
285
  | Thumbnails and B-Roll | `thumbnails generate/get/list`, `broll plan/generate/list/get` | Paid generation commands require `--confirm`. |
280
286
  | Social | `social accounts/post/schedule/posts/get/cancel` | Publish/schedule/cancel require `--confirm`; `x` maps to the server `twitter` platform. |
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.6';
12
+ const VERSION = '0.2.8';
13
13
  const DEFAULT_BASE_URL = 'https://clipit.dev';
14
14
  const DEFAULT_SCOPES = [
15
15
  'clippy_agent',
@@ -466,12 +466,13 @@ function usage() {
466
466
  ' clipit mcp [stdio]',
467
467
  ' clipit run <functionName> [--params @file.json] [--clip-id id] [--video-id id] [--confirm] [--max-credits n]',
468
468
  ' clipit videos list|get|upload|abort-upload|import-url|transcribe|transcript|suggest-clips|delete ...',
469
- ' clipit clips list|get|delivery-state|create|update|render|download|delete ...',
469
+ ' clipit clips list|get|delivery-state|create|update|initialize-snapshot|render|download|delete ...',
470
470
  ' clipit jobs get|wait <jobId>',
471
471
  ' clipit credits balance|usage|estimate ...',
472
472
  ' clipit billing capabilities|catalog|create-attempt|attempt|receipt|subscription ...',
473
473
  ' clipit analytics overview|top-clips|post ...',
474
474
  ' clipit exports start|list|get|wait|download|cancel ...',
475
+ ' clipit deliverables list|create ...',
475
476
  ' clipit assets list|upload|delete ...',
476
477
  ' clipit thumbnails generate|get|list ...',
477
478
  ' clipit broll plan|generate|list|get ...',
@@ -1239,18 +1240,28 @@ async function enforceMaxCredits(config, options, commandKey, context = {}) {
1239
1240
  }
1240
1241
  }
1241
1242
 
1242
- async function enforceRunMaxCredits(config, options, functionName, parameters = {}, payload = {}) {
1243
+ async function enforceRunMaxCredits(config, options, functionName, parameters = {}, payload = {}, catalogTool = null) {
1243
1244
  const limit = maxCreditsLimit(options);
1244
1245
  const needsConfirmationPreflight = !boolOption(options.confirm);
1245
1246
  if (limit === null && !needsConfirmationPreflight) return null;
1246
1247
 
1247
- const catalog = await apiFetch(config, options, 'GET', '/api/v1/agent/tools');
1248
- const tools = Array.isArray(catalog?.tools) ? catalog.tools : Array.isArray(catalog) ? catalog : [];
1249
- const tool = tools.find((item) => item.name === functionName);
1248
+ let tool = catalogTool;
1249
+ if (!tool) {
1250
+ const catalog = await apiFetch(config, options, 'GET', '/api/v1/agent/tools');
1251
+ const tools = Array.isArray(catalog?.tools) ? catalog.tools : Array.isArray(catalog) ? catalog : [];
1252
+ tool = tools.find((item) => item.name === functionName);
1253
+ }
1250
1254
  const runEstimate = tool?.estimate ?? tool?.confirmation?.estimate ?? tool?.confirmation?.costEstimate ?? null;
1251
1255
  const isMetered = Boolean(tool?.costBand && tool.costBand !== 'free');
1252
1256
  const isMeteredExempt = RUN_METERED_CONFIRMATION_EXEMPTIONS.has(functionName);
1253
1257
 
1258
+ if (tool?.costBand === 'free') {
1259
+ if (tool?.confirmation?.required) {
1260
+ requireConfirm(options, `Running confirmation-gated tool ${functionName}`);
1261
+ }
1262
+ return null;
1263
+ }
1264
+
1254
1265
  let staticEstimates = null;
1255
1266
  if (!runEstimate && (limit !== null || needsConfirmationPreflight)) {
1256
1267
  staticEstimates = await buildStaticRunEstimates(config, options, functionName, parameters, payload);
@@ -1656,14 +1667,26 @@ async function contextCommand(config, options, action) {
1656
1667
  throw Object.assign(new Error(`Unknown context command: ${action || ''}`), { exitCode: EXIT.USAGE });
1657
1668
  }
1658
1669
 
1659
- function applyContextToAgentPayload(payload, parameters, context) {
1670
+ function toolAcceptsContextField(tool, field) {
1671
+ return Boolean(
1672
+ tool?.contextRequirements?.[field]
1673
+ || tool?.parameters?.properties?.[field],
1674
+ );
1675
+ }
1676
+
1677
+ function applyContextToAgentPayload(payload, parameters, context, tool) {
1660
1678
  const canMutateParameters = parameters && typeof parameters === 'object' && !Array.isArray(parameters);
1661
1679
  const hadExplicitVideoId = canMutateParameters && parameters.videoId !== undefined;
1662
1680
  for (const field of ['videoId', 'clipId', 'projectId', 'sequenceId']) {
1663
- if (context[field] && !payload[field]) {
1664
- payload[field] = context[field];
1665
- if (canMutateParameters && parameters[field] === undefined) {
1666
- parameters[field] = context[field];
1681
+ const value = payload[field] || context[field];
1682
+ if (value) {
1683
+ if (!payload[field]) payload[field] = value;
1684
+ if (
1685
+ canMutateParameters
1686
+ && parameters[field] === undefined
1687
+ && toolAcceptsContextField(tool, field)
1688
+ ) {
1689
+ parameters[field] = value;
1667
1690
  }
1668
1691
  }
1669
1692
  }
@@ -1703,6 +1726,13 @@ function normalizeAgentExecuteResult(result) {
1703
1726
 
1704
1727
  async function runTool(config, options, functionName) {
1705
1728
  if (!functionName) throw Object.assign(new Error('Function name is required.'), { exitCode: EXIT.USAGE });
1729
+ if (RUN_CONFIRMATION_LABELS[functionName]) {
1730
+ confirmPaid(options, RUN_CONFIRMATION_LABELS[functionName]);
1731
+ }
1732
+ const catalog = await apiFetch(config, options, 'GET', '/api/v1/agent/tools');
1733
+ const tools = Array.isArray(catalog?.tools) ? catalog.tools : Array.isArray(catalog) ? catalog : [];
1734
+ const tool = tools.find((item) => item.name === functionName);
1735
+ if (!tool) throw Object.assign(new Error(`Tool not found: ${functionName}`), { exitCode: EXIT.USAGE });
1706
1736
  const parameters = await readJsonOption(options.params || options['params-json']);
1707
1737
  const context = await buildContext(config, options);
1708
1738
  const payload = {
@@ -1719,16 +1749,10 @@ async function runTool(config, options, functionName) {
1719
1749
  if (options[flag]) {
1720
1750
  const value = String(options[flag]);
1721
1751
  payload[field] = value;
1722
- if (parameters && typeof parameters === 'object' && !Array.isArray(parameters) && parameters[field] === undefined) {
1723
- parameters[field] = value;
1724
- }
1725
1752
  }
1726
1753
  }
1727
- applyContextToAgentPayload(payload, parameters, context);
1728
- if (RUN_CONFIRMATION_LABELS[functionName]) {
1729
- confirmPaid(options, RUN_CONFIRMATION_LABELS[functionName]);
1730
- }
1731
- const estimate = await enforceRunMaxCredits(config, options, functionName, parameters, payload);
1754
+ applyContextToAgentPayload(payload, parameters, context, tool);
1755
+ const estimate = await enforceRunMaxCredits(config, options, functionName, parameters, payload, tool);
1732
1756
  const result = normalizeAgentExecuteResult(await apiFetch(config, options, 'POST', '/api/v1/agent/execute', payload));
1733
1757
  const outputResult = estimate && result && typeof result === 'object' && !Array.isArray(result) && result.estimate === undefined
1734
1758
  ? { ...result, estimate }
@@ -2718,6 +2742,12 @@ async function clips(config, options, action, args) {
2718
2742
  }
2719
2743
  if (action === 'update') {
2720
2744
  if (!args[0]) throw Object.assign(new Error('Clip id is required.'), { exitCode: EXIT.USAGE });
2745
+ if (options.aspect !== undefined || options['aspect-ratio'] !== undefined) {
2746
+ throw Object.assign(
2747
+ new Error('clips update does not accept --aspect-ratio. Use clipit run setClipAspectRatio --clip-id <id> --params \'{"aspectRatio":"4:5"}\', then start an explicit render.'),
2748
+ { exitCode: EXIT.USAGE },
2749
+ );
2750
+ }
2721
2751
  const body = options.params
2722
2752
  ? await readJsonOption(String(options.params))
2723
2753
  : {
@@ -2732,6 +2762,26 @@ async function clips(config, options, action, args) {
2732
2762
  output(await apiFetch(config, options, 'PATCH', `/api/v1/clips/${encodeURIComponent(args[0])}`, body), options);
2733
2763
  return;
2734
2764
  }
2765
+ if (action === 'initialize-snapshot') {
2766
+ if (!args[0]) throw Object.assign(new Error('Clip id is required.'), { exitCode: EXIT.USAGE });
2767
+ const body = options.params
2768
+ ? await readJsonOption(String(options.params))
2769
+ : compactObject({
2770
+ aspectRatio: options.aspect || options['aspect-ratio'],
2771
+ fitBackground: options['fit-background'],
2772
+ quality: options.quality,
2773
+ includeCaptions: options.captions === undefined ? undefined : boolOption(options.captions),
2774
+ captionStyle: options['caption-style'],
2775
+ });
2776
+ output(await apiFetch(
2777
+ config,
2778
+ options,
2779
+ 'POST',
2780
+ `/api/v1/clips/${encodeURIComponent(args[0])}/editor-snapshot/initialize`,
2781
+ body,
2782
+ ), options);
2783
+ return;
2784
+ }
2735
2785
  if (action === 'render') {
2736
2786
  if (!args[0]) throw Object.assign(new Error('Clip id is required.'), { exitCode: EXIT.USAGE });
2737
2787
  const body = options.params
@@ -2919,7 +2969,6 @@ function defaultExportStartBody(clipId) {
2919
2969
  audioSampleRate: 48000,
2920
2970
  },
2921
2971
  format: 'mp4',
2922
- includeAudio: true,
2923
2972
  };
2924
2973
  }
2925
2974
 
@@ -3013,6 +3062,43 @@ async function exportsCommand(config, options, action, args) {
3013
3062
  throw Object.assign(new Error(`Unknown exports command: ${action || ''}`), { exitCode: EXIT.USAGE });
3014
3063
  }
3015
3064
 
3065
+ function enterpriseClientSelectionUrl(config, options, selectionPath = '/enterprise?tab=delivered') {
3066
+ return new URL(selectionPath, getBaseUrl(config, options)).toString();
3067
+ }
3068
+
3069
+ async function deliverables(config, options, action) {
3070
+ if (action === 'list') {
3071
+ const result = await apiFetch(config, options, 'GET', `/api/v1/deliverables${queryString({
3072
+ status: options.status,
3073
+ exportId: options['export-id'],
3074
+ limit: options.limit,
3075
+ offset: options.offset,
3076
+ })}`);
3077
+ output({
3078
+ ...result,
3079
+ clientSelectionUrl: enterpriseClientSelectionUrl(config, options, result.clientSelectionPath),
3080
+ }, options);
3081
+ return;
3082
+ }
3083
+ if (action === 'create') {
3084
+ const exportId = requiredString(options['export-id'], '--export-id');
3085
+ const title = requiredString(options.title, '--title');
3086
+ requireConfirm(options, 'Delivering an export to the enterprise client');
3087
+ const result = await apiFetch(config, options, 'POST', '/api/v1/deliverables', {
3088
+ exportId,
3089
+ title,
3090
+ note: options.note,
3091
+ });
3092
+ output({
3093
+ ...result,
3094
+ clientSelectionPath: '/enterprise?tab=delivered',
3095
+ clientSelectionUrl: enterpriseClientSelectionUrl(config, options),
3096
+ }, options);
3097
+ return;
3098
+ }
3099
+ throw Object.assign(new Error(`Unknown deliverables command: ${action || ''}`), { exitCode: EXIT.USAGE });
3100
+ }
3101
+
3016
3102
  async function putSignedUpload(uploadUrl, filePath, contentType, size, options, requiredHeaders) {
3017
3103
  const headers = requiredHeaders && typeof requiredHeaders === 'object'
3018
3104
  ? Object.fromEntries(Object.entries(requiredHeaders).map(([key, value]) => [key, String(value)]))
@@ -3259,6 +3345,41 @@ function socialAccountIdPins(value) {
3259
3345
  }));
3260
3346
  }
3261
3347
 
3348
+ async function resolveEnterpriseSelectedDelivery(config, options, clipId, exportId) {
3349
+ if (profileData(config, options).scope?.enterprise !== true) return null;
3350
+ const result = await apiFetch(
3351
+ config,
3352
+ options,
3353
+ 'GET',
3354
+ `/api/v1/deliverables${queryString({ exportId, limit: 100, offset: 0 })}`,
3355
+ );
3356
+ const deliveries = Array.isArray(result?.deliverables) ? result.deliverables : [];
3357
+ const selected = deliveries.find((delivery) => (
3358
+ delivery?.clipId === clipId
3359
+ && delivery?.exportId === exportId
3360
+ && delivery?.status === 'selected'
3361
+ ));
3362
+ if (selected?.id) return selected;
3363
+ const current = deliveries.find((delivery) => (
3364
+ delivery?.clipId === clipId && delivery?.exportId === exportId
3365
+ )) ?? null;
3366
+ const clientSelectionPath = result?.clientSelectionPath || '/enterprise?tab=delivered';
3367
+ throw Object.assign(
3368
+ new Error('The client has not selected this delivered export for publishing.'),
3369
+ {
3370
+ exitCode: EXIT.USAGE,
3371
+ data: {
3372
+ clipId,
3373
+ exportId,
3374
+ currentDelivery: current,
3375
+ clientSelectionPath,
3376
+ clientSelectionUrl: enterpriseClientSelectionUrl(config, options, clientSelectionPath),
3377
+ nextCommand: `clipit deliverables list --export-id ${shellQuote(exportId)} --json`,
3378
+ },
3379
+ },
3380
+ );
3381
+ }
3382
+
3262
3383
  async function socialPostBody(config, options, scheduled) {
3263
3384
  const clipId = requiredString(options['clip-id'], '--clip-id');
3264
3385
  const platforms = socialPlatformList(options.platforms);
@@ -3275,6 +3396,12 @@ async function socialPostBody(config, options, scheduled) {
3275
3396
  requestedExportId,
3276
3397
  requireReadyToPublish: true,
3277
3398
  });
3399
+ const enterpriseDelivery = await resolveEnterpriseSelectedDelivery(
3400
+ config,
3401
+ options,
3402
+ clipId,
3403
+ selectedExport.exportId,
3404
+ );
3278
3405
  const accountsResponse = await apiFetch(config, options, 'GET', '/api/v1/social/accounts');
3279
3406
  const connectedAccounts = Array.isArray(accountsResponse?.accounts)
3280
3407
  ? accountsResponse.accounts.filter((account) => account?.connected && typeof account?.accountId === 'string')
@@ -3310,6 +3437,7 @@ async function socialPostBody(config, options, scheduled) {
3310
3437
  expectedSnapshotId: editorState.snapshotId,
3311
3438
  expectedOutputObjectFingerprint: selectedExport.outputObjectFingerprint,
3312
3439
  expectedAccountIds,
3440
+ enterpriseDeliverableId: enterpriseDelivery?.id,
3313
3441
  publishExactCurrentArtifact: true,
3314
3442
  });
3315
3443
  if (scheduled) body.scheduledFor = requiredString(options.at, '--at');
@@ -3767,7 +3895,7 @@ async function handleMcpRequest(config, options, message) {
3767
3895
  const result = await handleLocalMcpBillingTool(config, options, name, parameters);
3768
3896
  return { jsonrpc: '2.0', id, result: mcpTextResult(result, Boolean(result?.error)) };
3769
3897
  }
3770
- applyContextToAgentPayload(payload, parameters, await buildContext(config, options));
3898
+ applyContextToAgentPayload(payload, parameters, await buildContext(config, options), tool);
3771
3899
  if (!confirmed && mcpToolRequiresConfirmation(tool, name)) {
3772
3900
  return {
3773
3901
  jsonrpc: '2.0',
@@ -4341,6 +4469,7 @@ async function main() {
4341
4469
  if (command === 'billing') return billing(config, options, subcommand, rest);
4342
4470
  if (command === 'analytics') return analytics(config, options, subcommand, rest);
4343
4471
  if (command === 'exports') return exportsCommand(config, options, subcommand, rest);
4472
+ if (command === 'deliverables') return deliverables(config, options, subcommand);
4344
4473
  if (command === 'assets') return assets(config, options, subcommand, rest);
4345
4474
  if (command === 'thumbnails') return thumbnails(config, options, subcommand, rest);
4346
4475
  if (command === 'broll') return broll(config, options, subcommand, rest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipit-ai/cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "ClipIt CLI for connecting shell-capable agents to ClipIt.",
5
5
  "type": "module",
6
6
  "bin": {