@clipit-ai/cli 0.2.7 → 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 +116 -20
  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.8';
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 ...',
@@ -1239,14 +1240,17 @@ 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);
@@ -1663,14 +1667,26 @@ async function contextCommand(config, options, action) {
1663
1667
  throw Object.assign(new Error(`Unknown context command: ${action || ''}`), { exitCode: EXIT.USAGE });
1664
1668
  }
1665
1669
 
1666
- 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) {
1667
1678
  const canMutateParameters = parameters && typeof parameters === 'object' && !Array.isArray(parameters);
1668
1679
  const hadExplicitVideoId = canMutateParameters && parameters.videoId !== undefined;
1669
1680
  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];
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;
1674
1690
  }
1675
1691
  }
1676
1692
  }
@@ -1710,6 +1726,13 @@ function normalizeAgentExecuteResult(result) {
1710
1726
 
1711
1727
  async function runTool(config, options, functionName) {
1712
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 });
1713
1736
  const parameters = await readJsonOption(options.params || options['params-json']);
1714
1737
  const context = await buildContext(config, options);
1715
1738
  const payload = {
@@ -1726,16 +1749,10 @@ async function runTool(config, options, functionName) {
1726
1749
  if (options[flag]) {
1727
1750
  const value = String(options[flag]);
1728
1751
  payload[field] = value;
1729
- if (parameters && typeof parameters === 'object' && !Array.isArray(parameters) && parameters[field] === undefined) {
1730
- parameters[field] = value;
1731
- }
1732
1752
  }
1733
1753
  }
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);
1754
+ applyContextToAgentPayload(payload, parameters, context, tool);
1755
+ const estimate = await enforceRunMaxCredits(config, options, functionName, parameters, payload, tool);
1739
1756
  const result = normalizeAgentExecuteResult(await apiFetch(config, options, 'POST', '/api/v1/agent/execute', payload));
1740
1757
  const outputResult = estimate && result && typeof result === 'object' && !Array.isArray(result) && result.estimate === undefined
1741
1758
  ? { ...result, estimate }
@@ -2952,7 +2969,6 @@ function defaultExportStartBody(clipId) {
2952
2969
  audioSampleRate: 48000,
2953
2970
  },
2954
2971
  format: 'mp4',
2955
- includeAudio: true,
2956
2972
  };
2957
2973
  }
2958
2974
 
@@ -3046,6 +3062,43 @@ async function exportsCommand(config, options, action, args) {
3046
3062
  throw Object.assign(new Error(`Unknown exports command: ${action || ''}`), { exitCode: EXIT.USAGE });
3047
3063
  }
3048
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
+
3049
3102
  async function putSignedUpload(uploadUrl, filePath, contentType, size, options, requiredHeaders) {
3050
3103
  const headers = requiredHeaders && typeof requiredHeaders === 'object'
3051
3104
  ? Object.fromEntries(Object.entries(requiredHeaders).map(([key, value]) => [key, String(value)]))
@@ -3292,6 +3345,41 @@ function socialAccountIdPins(value) {
3292
3345
  }));
3293
3346
  }
3294
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
+
3295
3383
  async function socialPostBody(config, options, scheduled) {
3296
3384
  const clipId = requiredString(options['clip-id'], '--clip-id');
3297
3385
  const platforms = socialPlatformList(options.platforms);
@@ -3308,6 +3396,12 @@ async function socialPostBody(config, options, scheduled) {
3308
3396
  requestedExportId,
3309
3397
  requireReadyToPublish: true,
3310
3398
  });
3399
+ const enterpriseDelivery = await resolveEnterpriseSelectedDelivery(
3400
+ config,
3401
+ options,
3402
+ clipId,
3403
+ selectedExport.exportId,
3404
+ );
3311
3405
  const accountsResponse = await apiFetch(config, options, 'GET', '/api/v1/social/accounts');
3312
3406
  const connectedAccounts = Array.isArray(accountsResponse?.accounts)
3313
3407
  ? accountsResponse.accounts.filter((account) => account?.connected && typeof account?.accountId === 'string')
@@ -3343,6 +3437,7 @@ async function socialPostBody(config, options, scheduled) {
3343
3437
  expectedSnapshotId: editorState.snapshotId,
3344
3438
  expectedOutputObjectFingerprint: selectedExport.outputObjectFingerprint,
3345
3439
  expectedAccountIds,
3440
+ enterpriseDeliverableId: enterpriseDelivery?.id,
3346
3441
  publishExactCurrentArtifact: true,
3347
3442
  });
3348
3443
  if (scheduled) body.scheduledFor = requiredString(options.at, '--at');
@@ -3800,7 +3895,7 @@ async function handleMcpRequest(config, options, message) {
3800
3895
  const result = await handleLocalMcpBillingTool(config, options, name, parameters);
3801
3896
  return { jsonrpc: '2.0', id, result: mcpTextResult(result, Boolean(result?.error)) };
3802
3897
  }
3803
- applyContextToAgentPayload(payload, parameters, await buildContext(config, options));
3898
+ applyContextToAgentPayload(payload, parameters, await buildContext(config, options), tool);
3804
3899
  if (!confirmed && mcpToolRequiresConfirmation(tool, name)) {
3805
3900
  return {
3806
3901
  jsonrpc: '2.0',
@@ -4374,6 +4469,7 @@ async function main() {
4374
4469
  if (command === 'billing') return billing(config, options, subcommand, rest);
4375
4470
  if (command === 'analytics') return analytics(config, options, subcommand, rest);
4376
4471
  if (command === 'exports') return exportsCommand(config, options, subcommand, rest);
4472
+ if (command === 'deliverables') return deliverables(config, options, subcommand);
4377
4473
  if (command === 'assets') return assets(config, options, subcommand, rest);
4378
4474
  if (command === 'thumbnails') return thumbnails(config, options, subcommand, rest);
4379
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.7",
3
+ "version": "0.2.8",
4
4
  "description": "ClipIt CLI for connecting shell-capable agents to ClipIt.",
5
5
  "type": "module",
6
6
  "bin": {