@clipit-ai/cli 0.2.1 → 0.2.3

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 (2) hide show
  1. package/bin/clipit.mjs +72 -17
  2. package/package.json +1 -1
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.1';
12
+ const VERSION = '0.2.3';
13
13
  const DEFAULT_BASE_URL = 'https://clipit.dev';
14
14
  const DEFAULT_SCOPES = [
15
15
  'clippy_agent',
@@ -724,14 +724,33 @@ async function enforceRunMaxCredits(config, options, functionName) {
724
724
  async function login(config, options) {
725
725
  const { verifier, challenge } = pkcePair();
726
726
  const baseUrl = getBaseUrl(config, options);
727
- const started = await apiFetch(config, options, 'POST', '/api/v1/cli-auth/start', {
728
- codeChallenge: challenge,
729
- codeChallengeMethod: 'S256',
730
- requestedScopes: DEFAULT_SCOPES,
731
- deviceName: os.hostname(),
732
- cliVersion: VERSION,
733
- platform: `${process.platform}/${process.arch}`,
734
- }, { noAuth: true });
727
+ let started;
728
+ try {
729
+ started = await apiFetch(config, options, 'POST', '/api/v1/cli-auth/start', {
730
+ codeChallenge: challenge,
731
+ codeChallengeMethod: 'S256',
732
+ requestedScopes: DEFAULT_SCOPES,
733
+ deviceName: os.hostname(),
734
+ cliVersion: VERSION,
735
+ platform: `${process.platform}/${process.arch}`,
736
+ }, { noAuth: true });
737
+ } catch (error) {
738
+ // A login start should never need credentials — a 401/404 here means the
739
+ // server predates the CLI auth API entirely.
740
+ if (error.status === 401 || error.status === 404) {
741
+ throw Object.assign(
742
+ new Error([
743
+ `${baseUrl} does not support browser login yet (its API layer predates this CLI).`,
744
+ 'Use an API key instead:',
745
+ ` 1. Create one at ${baseUrl} -> Settings -> API Keys -> Connect an Agent`,
746
+ " 2. Store it: printf '%s' \"$CLIPPER_API_KEY\" | clipit auth set-key --stdin",
747
+ ' 3. Verify: clipit videos list',
748
+ ].join('\n')),
749
+ { exitCode: EXIT.AUTH, status: error.status, requestId: error.requestId },
750
+ );
751
+ }
752
+ throw error;
753
+ }
735
754
 
736
755
  if (!options['no-browser']) {
737
756
  try {
@@ -1806,25 +1825,61 @@ async function jobs(config, options, action, args) {
1806
1825
  }
1807
1826
  }
1808
1827
 
1809
- function appUrl(config, options, kind, id) {
1828
+ async function appUrl(config, options, kind, id) {
1810
1829
  const baseUrl = getBaseUrl(config, options);
1811
1830
  if (!kind || kind === 'home') return baseUrl;
1812
1831
  if (kind === 'settings') return `${baseUrl}/settings?tab=${encodeURIComponent(options.tab || 'api-keys')}`;
1813
1832
  if (kind === 'dashboard') return `${baseUrl}/dashboard`;
1814
1833
  if (kind === 'library') return `${baseUrl}/clips`;
1815
- if (kind === 'editor') return `${baseUrl}/editor`;
1834
+ if (kind === 'editor') {
1835
+ // The editor reads ?project=, ?video=, ?clip= (VideoEditorPage). Thread any
1836
+ // provided context so `open editor --video <id>` lands on real content rather
1837
+ // than the empty "select a video" state.
1838
+ const params = new URLSearchParams();
1839
+ const project = id || options.project || null;
1840
+ if (project) params.set('project', project);
1841
+ if (options.video || options.videoId) params.set('video', options.video || options.videoId);
1842
+ if (options.clip || options.clipId) params.set('clip', options.clip || options.clipId);
1843
+ const qs = params.toString();
1844
+ return `${baseUrl}/editor${qs ? `?${qs}` : ''}`;
1845
+ }
1816
1846
  if (kind === 'pricing') return `${baseUrl}/pricing`;
1817
1847
  if (kind === 'credits') return `${baseUrl}/settings/credits`;
1818
- if (kind === 'clip') return `${baseUrl}/clips/review${id ? `?clipId=${encodeURIComponent(id)}` : ''}`;
1819
- if (kind === 'video') return `${baseUrl}/clips${id ? `?videoId=${encodeURIComponent(id)}` : ''}`;
1820
- if (kind === 'project') return `${baseUrl}/editor/projects${id ? `?projectId=${encodeURIComponent(id)}` : ''}`;
1821
- if (kind === 'sequence') return `${baseUrl}/editor/projects${id ? `?sequenceId=${encodeURIComponent(id)}` : ''}`;
1848
+ if (kind === 'clip') {
1849
+ // A clip lives inside a video. The Clips Review page keys off ?video=<videoId>
1850
+ // without it the page renders "No video selected", so a bare clip link
1851
+ // dead-ends. Resolve the clip's parent video so the link lands on the clip,
1852
+ // selected. ?clip=<clipId> focuses it (the page reads `clip`, not `clipId`).
1853
+ if (!id) return `${baseUrl}/clips/review`;
1854
+ let videoId = options.video || options.videoId || null;
1855
+ if (!videoId) {
1856
+ try {
1857
+ const clip = await apiFetch(config, options, 'GET', `/api/v1/clips/${encodeURIComponent(id)}`);
1858
+ videoId = clip?.videoId || clip?.video?.id || null;
1859
+ } catch {
1860
+ // Network/auth/not-found — fall through to a clip-only link below.
1861
+ }
1862
+ }
1863
+ const params = new URLSearchParams();
1864
+ if (videoId) params.set('video', videoId);
1865
+ params.set('clip', id);
1866
+ return `${baseUrl}/clips/review?${params.toString()}`;
1867
+ }
1868
+ // A video link should open that video's clips in the Review page (which keys
1869
+ // off ?video=), not the generic /clips library list which has no selection.
1870
+ if (kind === 'video') return `${baseUrl}/clips/review${id ? `?video=${encodeURIComponent(id)}` : ''}`;
1871
+ // The editor lives at /editor and reads ?project= (NOT /editor/projects, which
1872
+ // redirects to /clips and drops the query — the same dead-end class as the
1873
+ // clip/video link bug). There is no surface that consumes a raw sequenceId, so
1874
+ // a sequence link just opens the editor.
1875
+ if (kind === 'project') return `${baseUrl}/editor${id ? `?project=${encodeURIComponent(id)}` : ''}`;
1876
+ if (kind === 'sequence') return `${baseUrl}/editor`;
1822
1877
  if (kind === 'route') return `${baseUrl}/${String(id || '').replace(/^\/+/, '')}`;
1823
1878
  return baseUrl;
1824
1879
  }
1825
1880
 
1826
1881
  async function openCommand(config, options, kind, id) {
1827
- const url = appUrl(config, options, kind, id);
1882
+ const url = await appUrl(config, options, kind, id);
1828
1883
  if (options.print) {
1829
1884
  output(url, options);
1830
1885
  return;
@@ -1837,7 +1892,7 @@ async function links(config, options, kind, id) {
1837
1892
  const result = {
1838
1893
  kind: kind || 'home',
1839
1894
  id: id || null,
1840
- appUrl: appUrl(config, options, kind, id),
1895
+ appUrl: await appUrl(config, options, kind, id),
1841
1896
  };
1842
1897
  if (kind === 'clip' && id && boolOption(options.download)) {
1843
1898
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipit-ai/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "ClipIt CLI for connecting shell-capable agents to ClipIt.",
5
5
  "type": "module",
6
6
  "bin": {