@clipit-ai/cli 0.2.7 → 0.2.9

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 +162 -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.7';
12
+ const VERSION = '0.2.9';
13
13
  const DEFAULT_BASE_URL = 'https://clipit.dev';
14
14
  const DEFAULT_SCOPES = [
15
15
  'clippy_agent',
@@ -472,6 +472,7 @@ function usage() {
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 ...',
@@ -483,6 +484,51 @@ function usage() {
483
484
  ].join('\n');
484
485
  }
485
486
 
487
+ function commandUsage(command, subcommand) {
488
+ if (command === 'social' && subcommand === 'schedule') {
489
+ return [
490
+ 'Usage:',
491
+ ' clipit social schedule --clip-id <id> --platforms <platform[,platform]> --caption <text> --at <ISO-8601> --confirm',
492
+ '',
493
+ 'Required:',
494
+ ' --clip-id <id> Clip to schedule',
495
+ ' --platforms <list> Comma-separated destination platforms',
496
+ ' --caption <text> Post caption',
497
+ ' --at <ISO-8601> Scheduled publish time',
498
+ ' --confirm Confirm the scheduling mutation',
499
+ '',
500
+ 'Optional:',
501
+ ' --export-id <id> Pin the exact completed export',
502
+ ' --account-ids <map> Exact accounts as platform=accountId pairs',
503
+ ' --title <text> Post title',
504
+ ' --hashtags <list> Comma-separated hashtags',
505
+ ' --profile <name> Named ClipIt credential profile',
506
+ ' --json Emit machine-readable JSON',
507
+ '',
508
+ 'Enterprise scheduling requires a client-selected deliverable and a granted connected account.',
509
+ ].join('\n');
510
+ }
511
+ if (command === 'deliverables' && subcommand === 'create') {
512
+ return [
513
+ 'Usage:',
514
+ ' clipit deliverables create --export-id <id> --title <text> --confirm',
515
+ '',
516
+ 'Required:',
517
+ ' --export-id <id> Exact completed export to deliver',
518
+ ' --title <text> Client-facing deliverable title',
519
+ ' --confirm Confirm the delivery mutation',
520
+ '',
521
+ 'Optional:',
522
+ ' --note <text> Client-facing note',
523
+ ' --profile <name> Named ClipIt credential profile',
524
+ ' --json Emit machine-readable JSON',
525
+ '',
526
+ 'The response includes clientSelectionUrl; the client must select the delivery before enterprise social publishing.',
527
+ ].join('\n');
528
+ }
529
+ return null;
530
+ }
531
+
486
532
  function getBaseUrl(config, options) {
487
533
  const profile = profileData(config, options);
488
534
  const selected = profileIsAuthoritative(config, options);
@@ -1239,14 +1285,17 @@ async function enforceMaxCredits(config, options, commandKey, context = {}) {
1239
1285
  }
1240
1286
  }
1241
1287
 
1242
- async function enforceRunMaxCredits(config, options, functionName, parameters = {}, payload = {}) {
1288
+ async function enforceRunMaxCredits(config, options, functionName, parameters = {}, payload = {}, catalogTool = null) {
1243
1289
  const limit = maxCreditsLimit(options);
1244
1290
  const needsConfirmationPreflight = !boolOption(options.confirm);
1245
1291
  if (limit === null && !needsConfirmationPreflight) return null;
1246
1292
 
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);
1293
+ let tool = catalogTool;
1294
+ if (!tool) {
1295
+ const catalog = await apiFetch(config, options, 'GET', '/api/v1/agent/tools');
1296
+ const tools = Array.isArray(catalog?.tools) ? catalog.tools : Array.isArray(catalog) ? catalog : [];
1297
+ tool = tools.find((item) => item.name === functionName);
1298
+ }
1250
1299
  const runEstimate = tool?.estimate ?? tool?.confirmation?.estimate ?? tool?.confirmation?.costEstimate ?? null;
1251
1300
  const isMetered = Boolean(tool?.costBand && tool.costBand !== 'free');
1252
1301
  const isMeteredExempt = RUN_METERED_CONFIRMATION_EXEMPTIONS.has(functionName);
@@ -1663,14 +1712,26 @@ async function contextCommand(config, options, action) {
1663
1712
  throw Object.assign(new Error(`Unknown context command: ${action || ''}`), { exitCode: EXIT.USAGE });
1664
1713
  }
1665
1714
 
1666
- function applyContextToAgentPayload(payload, parameters, context) {
1715
+ function toolAcceptsContextField(tool, field) {
1716
+ return Boolean(
1717
+ tool?.contextRequirements?.[field]
1718
+ || tool?.parameters?.properties?.[field],
1719
+ );
1720
+ }
1721
+
1722
+ function applyContextToAgentPayload(payload, parameters, context, tool) {
1667
1723
  const canMutateParameters = parameters && typeof parameters === 'object' && !Array.isArray(parameters);
1668
1724
  const hadExplicitVideoId = canMutateParameters && parameters.videoId !== undefined;
1669
1725
  for (const field of ['videoId', 'clipId', 'projectId', 'sequenceId']) {
1670
- if (context[field] && !payload[field]) {
1671
- payload[field] = context[field];
1672
- if (canMutateParameters && parameters[field] === undefined) {
1673
- parameters[field] = context[field];
1726
+ const value = payload[field] || context[field];
1727
+ if (value) {
1728
+ if (!payload[field]) payload[field] = value;
1729
+ if (
1730
+ canMutateParameters
1731
+ && parameters[field] === undefined
1732
+ && toolAcceptsContextField(tool, field)
1733
+ ) {
1734
+ parameters[field] = value;
1674
1735
  }
1675
1736
  }
1676
1737
  }
@@ -1710,6 +1771,13 @@ function normalizeAgentExecuteResult(result) {
1710
1771
 
1711
1772
  async function runTool(config, options, functionName) {
1712
1773
  if (!functionName) throw Object.assign(new Error('Function name is required.'), { exitCode: EXIT.USAGE });
1774
+ if (RUN_CONFIRMATION_LABELS[functionName]) {
1775
+ confirmPaid(options, RUN_CONFIRMATION_LABELS[functionName]);
1776
+ }
1777
+ const catalog = await apiFetch(config, options, 'GET', '/api/v1/agent/tools');
1778
+ const tools = Array.isArray(catalog?.tools) ? catalog.tools : Array.isArray(catalog) ? catalog : [];
1779
+ const tool = tools.find((item) => item.name === functionName);
1780
+ if (!tool) throw Object.assign(new Error(`Tool not found: ${functionName}`), { exitCode: EXIT.USAGE });
1713
1781
  const parameters = await readJsonOption(options.params || options['params-json']);
1714
1782
  const context = await buildContext(config, options);
1715
1783
  const payload = {
@@ -1726,16 +1794,10 @@ async function runTool(config, options, functionName) {
1726
1794
  if (options[flag]) {
1727
1795
  const value = String(options[flag]);
1728
1796
  payload[field] = value;
1729
- if (parameters && typeof parameters === 'object' && !Array.isArray(parameters) && parameters[field] === undefined) {
1730
- parameters[field] = value;
1731
- }
1732
1797
  }
1733
1798
  }
1734
- applyContextToAgentPayload(payload, parameters, context);
1735
- if (RUN_CONFIRMATION_LABELS[functionName]) {
1736
- confirmPaid(options, RUN_CONFIRMATION_LABELS[functionName]);
1737
- }
1738
- const estimate = await enforceRunMaxCredits(config, options, functionName, parameters, payload);
1799
+ applyContextToAgentPayload(payload, parameters, context, tool);
1800
+ const estimate = await enforceRunMaxCredits(config, options, functionName, parameters, payload, tool);
1739
1801
  const result = normalizeAgentExecuteResult(await apiFetch(config, options, 'POST', '/api/v1/agent/execute', payload));
1740
1802
  const outputResult = estimate && result && typeof result === 'object' && !Array.isArray(result) && result.estimate === undefined
1741
1803
  ? { ...result, estimate }
@@ -2952,7 +3014,6 @@ function defaultExportStartBody(clipId) {
2952
3014
  audioSampleRate: 48000,
2953
3015
  },
2954
3016
  format: 'mp4',
2955
- includeAudio: true,
2956
3017
  };
2957
3018
  }
2958
3019
 
@@ -3046,6 +3107,43 @@ async function exportsCommand(config, options, action, args) {
3046
3107
  throw Object.assign(new Error(`Unknown exports command: ${action || ''}`), { exitCode: EXIT.USAGE });
3047
3108
  }
3048
3109
 
3110
+ function enterpriseClientSelectionUrl(config, options, selectionPath = '/enterprise?tab=delivered') {
3111
+ return new URL(selectionPath, getBaseUrl(config, options)).toString();
3112
+ }
3113
+
3114
+ async function deliverables(config, options, action) {
3115
+ if (action === 'list') {
3116
+ const result = await apiFetch(config, options, 'GET', `/api/v1/deliverables${queryString({
3117
+ status: options.status,
3118
+ exportId: options['export-id'],
3119
+ limit: options.limit,
3120
+ offset: options.offset,
3121
+ })}`);
3122
+ output({
3123
+ ...result,
3124
+ clientSelectionUrl: enterpriseClientSelectionUrl(config, options, result.clientSelectionPath),
3125
+ }, options);
3126
+ return;
3127
+ }
3128
+ if (action === 'create') {
3129
+ const exportId = requiredString(options['export-id'], '--export-id');
3130
+ const title = requiredString(options.title, '--title');
3131
+ requireConfirm(options, 'Delivering an export to the enterprise client');
3132
+ const result = await apiFetch(config, options, 'POST', '/api/v1/deliverables', {
3133
+ exportId,
3134
+ title,
3135
+ note: options.note,
3136
+ });
3137
+ output({
3138
+ ...result,
3139
+ clientSelectionPath: '/enterprise?tab=delivered',
3140
+ clientSelectionUrl: enterpriseClientSelectionUrl(config, options),
3141
+ }, options);
3142
+ return;
3143
+ }
3144
+ throw Object.assign(new Error(`Unknown deliverables command: ${action || ''}`), { exitCode: EXIT.USAGE });
3145
+ }
3146
+
3049
3147
  async function putSignedUpload(uploadUrl, filePath, contentType, size, options, requiredHeaders) {
3050
3148
  const headers = requiredHeaders && typeof requiredHeaders === 'object'
3051
3149
  ? Object.fromEntries(Object.entries(requiredHeaders).map(([key, value]) => [key, String(value)]))
@@ -3292,6 +3390,41 @@ function socialAccountIdPins(value) {
3292
3390
  }));
3293
3391
  }
3294
3392
 
3393
+ async function resolveEnterpriseSelectedDelivery(config, options, clipId, exportId) {
3394
+ if (profileData(config, options).scope?.enterprise !== true) return null;
3395
+ const result = await apiFetch(
3396
+ config,
3397
+ options,
3398
+ 'GET',
3399
+ `/api/v1/deliverables${queryString({ exportId, limit: 100, offset: 0 })}`,
3400
+ );
3401
+ const deliveries = Array.isArray(result?.deliverables) ? result.deliverables : [];
3402
+ const selected = deliveries.find((delivery) => (
3403
+ delivery?.clipId === clipId
3404
+ && delivery?.exportId === exportId
3405
+ && delivery?.status === 'selected'
3406
+ ));
3407
+ if (selected?.id) return selected;
3408
+ const current = deliveries.find((delivery) => (
3409
+ delivery?.clipId === clipId && delivery?.exportId === exportId
3410
+ )) ?? null;
3411
+ const clientSelectionPath = result?.clientSelectionPath || '/enterprise?tab=delivered';
3412
+ throw Object.assign(
3413
+ new Error('The client has not selected this delivered export for publishing.'),
3414
+ {
3415
+ exitCode: EXIT.USAGE,
3416
+ data: {
3417
+ clipId,
3418
+ exportId,
3419
+ currentDelivery: current,
3420
+ clientSelectionPath,
3421
+ clientSelectionUrl: enterpriseClientSelectionUrl(config, options, clientSelectionPath),
3422
+ nextCommand: `clipit deliverables list --export-id ${shellQuote(exportId)} --json`,
3423
+ },
3424
+ },
3425
+ );
3426
+ }
3427
+
3295
3428
  async function socialPostBody(config, options, scheduled) {
3296
3429
  const clipId = requiredString(options['clip-id'], '--clip-id');
3297
3430
  const platforms = socialPlatformList(options.platforms);
@@ -3308,6 +3441,12 @@ async function socialPostBody(config, options, scheduled) {
3308
3441
  requestedExportId,
3309
3442
  requireReadyToPublish: true,
3310
3443
  });
3444
+ const enterpriseDelivery = await resolveEnterpriseSelectedDelivery(
3445
+ config,
3446
+ options,
3447
+ clipId,
3448
+ selectedExport.exportId,
3449
+ );
3311
3450
  const accountsResponse = await apiFetch(config, options, 'GET', '/api/v1/social/accounts');
3312
3451
  const connectedAccounts = Array.isArray(accountsResponse?.accounts)
3313
3452
  ? accountsResponse.accounts.filter((account) => account?.connected && typeof account?.accountId === 'string')
@@ -3343,6 +3482,7 @@ async function socialPostBody(config, options, scheduled) {
3343
3482
  expectedSnapshotId: editorState.snapshotId,
3344
3483
  expectedOutputObjectFingerprint: selectedExport.outputObjectFingerprint,
3345
3484
  expectedAccountIds,
3485
+ enterpriseDeliverableId: enterpriseDelivery?.id,
3346
3486
  publishExactCurrentArtifact: true,
3347
3487
  });
3348
3488
  if (scheduled) body.scheduledFor = requiredString(options.at, '--at');
@@ -3800,7 +3940,7 @@ async function handleMcpRequest(config, options, message) {
3800
3940
  const result = await handleLocalMcpBillingTool(config, options, name, parameters);
3801
3941
  return { jsonrpc: '2.0', id, result: mcpTextResult(result, Boolean(result?.error)) };
3802
3942
  }
3803
- applyContextToAgentPayload(payload, parameters, await buildContext(config, options));
3943
+ applyContextToAgentPayload(payload, parameters, await buildContext(config, options), tool);
3804
3944
  if (!confirmed && mcpToolRequiresConfirmation(tool, name)) {
3805
3945
  return {
3806
3946
  jsonrpc: '2.0',
@@ -4343,7 +4483,7 @@ async function main() {
4343
4483
  return;
4344
4484
  }
4345
4485
  if (!command || options.help) {
4346
- console.log(usage());
4486
+ console.log(commandUsage(command, subcommand) ?? usage());
4347
4487
  return;
4348
4488
  }
4349
4489
  if (command === 'version' || command === '--version' || command === '-v') {
@@ -4374,6 +4514,7 @@ async function main() {
4374
4514
  if (command === 'billing') return billing(config, options, subcommand, rest);
4375
4515
  if (command === 'analytics') return analytics(config, options, subcommand, rest);
4376
4516
  if (command === 'exports') return exportsCommand(config, options, subcommand, rest);
4517
+ if (command === 'deliverables') return deliverables(config, options, subcommand);
4377
4518
  if (command === 'assets') return assets(config, options, subcommand, rest);
4378
4519
  if (command === 'thumbnails') return thumbnails(config, options, subcommand, rest);
4379
4520
  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.7",
3
+ "version": "0.2.9",
4
4
  "description": "ClipIt CLI for connecting shell-capable agents to ClipIt.",
5
5
  "type": "module",
6
6
  "bin": {