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