@afromero/kin3o 0.2.3 → 0.2.5

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/README.md CHANGED
@@ -5,9 +5,12 @@
5
5
  **Text to Motion. From your terminal.**
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/@afromero/kin3o)](https://www.npmjs.com/package/@afromero/kin3o)
8
+ [![downloads](https://img.shields.io/npm/dm/@afromero/kin3o)](https://www.npmjs.com/package/@afromero/kin3o)
8
9
  [![CI](https://github.com/affromero/kin3o/actions/workflows/ci.yml/badge.svg)](https://github.com/affromero/kin3o/actions/workflows/ci.yml)
9
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](https://www.typescriptlang.org/)
10
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
12
+ [![Socket](https://img.shields.io/badge/Socket-protected-blueviolet?logo=socket.dev)](https://socket.dev)
13
+ [![min-release-age](https://img.shields.io/badge/min--release--age-7%20days-brightgreen)](https://docs.npmjs.com/cli/v10/using-npm/config#min-release-age)
11
14
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/affromero/kin3o/pulls)
12
15
 
13
16
  AI-powered Lottie animation generator. Turns natural language prompts into valid, playable Lottie JSON and interactive dotLottie state machines using your existing Claude or Codex subscription.
@@ -237,6 +240,14 @@ npm run ci # typecheck + test
237
240
  npm run build # Compile to dist/
238
241
  ```
239
242
 
243
+ ## Related Projects
244
+
245
+ | Project | Description |
246
+ |---------|-------------|
247
+ | [**Fairtrail**](https://github.com/affromero/fairtrail) | Flight price evolution tracker with natural language search |
248
+ | [**PriceToken**](https://github.com/affromero/pricetoken) | Real-time LLM pricing API, npm/PyPI packages, and live dashboard |
249
+ | [**gitpane**](https://github.com/affromero/gitpane) | Multi-repo Git workspace dashboard for the terminal |
250
+
240
251
  ## License
241
252
 
242
253
  MIT
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { Command } from 'commander';
5
- import { PROVIDERS, detectAvailableProviders, getDefaultProvider } from './providers/registry.js';
5
+ import { PROVIDERS, detectAvailableProviders, getDefaultProvider, diagnoseProvider } from './providers/registry.js';
6
6
  import { buildSystemPrompt, buildInteractiveSystemPrompt, buildRefinementUserPrompt, buildInteractiveRefinementUserPrompt, loadDesignTokens } from './prompts/index.js';
7
7
  import { validateLottie, autoFix } from './validator.js';
8
8
  import { validateStateMachine } from './state-machine-validator.js';
@@ -28,6 +28,7 @@ program
28
28
  .option('--no-preview', 'Skip opening preview in browser')
29
29
  .option('-t, --tokens <path>', 'Path to design tokens JSON (or "sotto" for built-in)')
30
30
  .option('-i, --interactive', 'Generate interactive state machine (.lottie output)', false)
31
+ .option('--timeout <ms>', 'CLI subprocess timeout in milliseconds (default: auto per model)', parseInt)
31
32
  .action(async (prompt, options) => {
32
33
  const mode = options.interactive ? 'interactive' : 'static';
33
34
  console.log(`\nkin3o — Generating ${mode} animation...`);
@@ -36,7 +37,7 @@ program
36
37
  if (!providerKey) {
37
38
  providerKey = await getDefaultProvider() ?? undefined;
38
39
  if (!providerKey) {
39
- console.error(' ✗ No AI providers available. Install Claude Code, Codex, or set ANTHROPIC_API_KEY.');
40
+ console.error(' ✗ No AI providers available. Run `kin3o providers` for diagnostics.');
40
41
  process.exit(1);
41
42
  }
42
43
  }
@@ -56,7 +57,7 @@ program
56
57
  : buildSystemPrompt(tokens);
57
58
  // 4. Generate
58
59
  try {
59
- const result = await provider.generate(model, systemPrompt, prompt);
60
+ const result = await provider.generate(model, systemPrompt, prompt, options.timeout);
60
61
  console.log(` ✓ Generated in ${(result.durationMs / 1000).toFixed(1)}s`);
61
62
  if (options.interactive) {
62
63
  await handleInteractiveOutput(result.content, prompt, options);
@@ -166,6 +167,7 @@ program
166
167
  .option('-o, --output <path>', 'Output file path')
167
168
  .option('--no-preview', 'Skip opening preview in browser')
168
169
  .option('-t, --tokens <path>', 'Path to design tokens JSON (or "sotto" for built-in)')
170
+ .option('--timeout <ms>', 'CLI subprocess timeout in milliseconds (default: 600000)', parseInt)
169
171
  .action(async (file, prompt, rawOptions) => {
170
172
  const resolvedPath = resolve(file);
171
173
  // 1. Validate input file
@@ -202,7 +204,7 @@ program
202
204
  if (!providerKey) {
203
205
  providerKey = await getDefaultProvider() ?? undefined;
204
206
  if (!providerKey) {
205
- console.error(' ✗ No AI providers available. Install Claude Code, Codex, or set ANTHROPIC_API_KEY.');
207
+ console.error(' ✗ No AI providers available. Run `kin3o providers` for diagnostics.');
206
208
  process.exit(1);
207
209
  }
208
210
  }
@@ -225,7 +227,7 @@ program
225
227
  : buildRefinementUserPrompt(currentJson, prompt);
226
228
  // 6. Generate refined output
227
229
  try {
228
- const result = await provider.generate(model, systemPrompt, userPrompt);
230
+ const result = await provider.generate(model, systemPrompt, userPrompt, rawOptions.timeout);
229
231
  console.log(` ✓ Refined in ${(result.durationMs / 1000).toFixed(1)}s`);
230
232
  // 7. Compute output path
231
233
  const outputDir = ensureOutputDir();
@@ -369,9 +371,22 @@ program
369
371
  const available = await detectAvailableProviders();
370
372
  console.log('\nAvailable AI Providers:\n');
371
373
  for (const [key, config] of Object.entries(PROVIDERS)) {
372
- const status = available.includes(key) ? '✓' : '✗';
374
+ const isAvail = available.includes(key);
375
+ const status = isAvail ? '✓' : '✗';
373
376
  console.log(` ${status} ${config.displayName} (${key})`);
374
377
  console.log(` Models: ${config.models.join(', ')}`);
378
+ if (!isAvail) {
379
+ const diag = diagnoseProvider(key);
380
+ if (!diag.binaryFound) {
381
+ console.log(` Problem: "${diag.binaryName}" not found on PATH`);
382
+ console.log(` Fix: Install ${config.displayName} and ensure "${diag.binaryName}" is on your PATH`);
383
+ }
384
+ else if (!diag.authFound) {
385
+ const cmd = key === 'codex' ? 'codex auth' : 'claude';
386
+ console.log(` Problem: CLI found but not authenticated`);
387
+ console.log(` Fix: Run \`${cmd}\` to log in`);
388
+ }
389
+ }
375
390
  }
376
391
  console.log('');
377
392
  });
@@ -1,2 +1,2 @@
1
1
  import type { GenerationResult } from './registry.js';
2
- export declare function generateWithClaude(model: string, systemPrompt: string, userPrompt: string): Promise<GenerationResult>;
2
+ export declare function generateWithClaude(model: string, systemPrompt: string, userPrompt: string, timeoutMs?: number): Promise<GenerationResult>;
@@ -1,13 +1,15 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { getTimeoutMs } from './registry.js';
2
3
  import { filterCliStderr } from '../utils.js';
3
- export async function generateWithClaude(model, systemPrompt, userPrompt) {
4
+ export async function generateWithClaude(model, systemPrompt, userPrompt, timeoutMs) {
4
5
  const start = Date.now();
5
6
  const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
7
+ const timeout = getTimeoutMs(timeoutMs);
6
8
  const content = await new Promise((resolve, reject) => {
7
9
  const env = { ...process.env };
8
10
  delete env.ANTHROPIC_API_KEY;
9
11
  const proc = spawn('claude', ['--print', '--model', model], {
10
- timeout: 240_000,
12
+ timeout,
11
13
  env,
12
14
  });
13
15
  let stdout = '';
@@ -21,7 +23,8 @@ export async function generateWithClaude(model, systemPrompt, userPrompt) {
21
23
  proc.on('close', (code) => {
22
24
  if (code !== 0) {
23
25
  const filtered = filterCliStderr(stderr);
24
- reject(new Error(`claude CLI exited ${code}: ${filtered}`));
26
+ const hint = code === 143 ? ` (timed out after ${Math.round(timeout / 1000)}s — try --timeout <ms> or a faster model)` : '';
27
+ reject(new Error(`claude CLI exited ${code}: ${filtered}${hint}`));
25
28
  }
26
29
  else {
27
30
  resolve(stdout.trim());
@@ -1,2 +1,2 @@
1
1
  import type { GenerationResult } from './registry.js';
2
- export declare function generateWithCodex(model: string, systemPrompt: string, userPrompt: string): Promise<GenerationResult>;
2
+ export declare function generateWithCodex(model: string, systemPrompt: string, userPrompt: string, timeoutMs?: number): Promise<GenerationResult>;
@@ -2,11 +2,13 @@ import { spawn } from 'node:child_process';
2
2
  import { mkdtempSync, readFileSync, unlinkSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
+ import { getTimeoutMs } from './registry.js';
5
6
  import { filterCliStderr } from '../utils.js';
6
- export async function generateWithCodex(model, systemPrompt, userPrompt) {
7
+ export async function generateWithCodex(model, systemPrompt, userPrompt, timeoutMs) {
7
8
  const start = Date.now();
8
9
  const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
9
10
  const tmpFile = join(mkdtempSync(join(tmpdir(), 'codex-')), 'output.txt');
11
+ const timeout = getTimeoutMs(timeoutMs);
10
12
  const args = [
11
13
  'exec', '-',
12
14
  '--skip-git-repo-check',
@@ -16,7 +18,7 @@ export async function generateWithCodex(model, systemPrompt, userPrompt) {
16
18
  ];
17
19
  const content = await new Promise((resolve, reject) => {
18
20
  const proc = spawn('codex', args, {
19
- timeout: 240_000,
21
+ timeout,
20
22
  env: { ...process.env },
21
23
  });
22
24
  let stderr = '';
@@ -27,7 +29,9 @@ export async function generateWithCodex(model, systemPrompt, userPrompt) {
27
29
  const filtered = filterCliStderr(stderr);
28
30
  const hint = filtered.includes('401') || filtered.includes('Unauthorized')
29
31
  ? ' (ensure codex is authenticated via `codex auth`)'
30
- : '';
32
+ : code === 143
33
+ ? ` (timed out after ${Math.round(timeout / 1000)}s — try --timeout <ms> or a faster model)`
34
+ : '';
31
35
  try {
32
36
  const output = readFileSync(tmpFile, 'utf-8').trim();
33
37
  unlinkSync(tmpFile);
@@ -9,9 +9,19 @@ export interface ProviderConfig {
9
9
  models: string[];
10
10
  defaultModel: string;
11
11
  isAvailable: () => Promise<boolean>;
12
- generate: (model: string, systemPrompt: string, userPrompt: string) => Promise<GenerationResult>;
12
+ generate: (model: string, systemPrompt: string, userPrompt: string, timeoutMs?: number) => Promise<GenerationResult>;
13
+ }
14
+ /** Get timeout with optional user override */
15
+ export declare function getTimeoutMs(userOverride?: number): number;
16
+ export interface ProviderDiagnosis {
17
+ binaryFound: boolean;
18
+ authFound: boolean;
19
+ binaryName: string;
20
+ authPaths: string[];
13
21
  }
14
22
  export declare const PROVIDERS: Record<string, ProviderConfig>;
23
+ /** Diagnose why a provider is or isn't available */
24
+ export declare function diagnoseProvider(key: string): ProviderDiagnosis;
15
25
  /** Detect which providers are available (binary + auth) */
16
26
  export declare function detectAvailableProviders(): Promise<string[]>;
17
27
  /** Get default provider (first available in priority order) */
@@ -4,6 +4,12 @@ import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { generateWithClaude } from './claude.js';
6
6
  import { generateWithCodex } from './codex.js';
7
+ /** Default CLI subprocess timeout: 10 minutes */
8
+ const DEFAULT_TIMEOUT_MS = 600_000;
9
+ /** Get timeout with optional user override */
10
+ export function getTimeoutMs(userOverride) {
11
+ return userOverride ?? DEFAULT_TIMEOUT_MS;
12
+ }
7
13
  const home = homedir();
8
14
  function hasBinary(name) {
9
15
  try {
@@ -23,7 +29,8 @@ export const PROVIDERS = {
23
29
  if (!hasBinary('claude'))
24
30
  return false;
25
31
  return existsSync(join(home, '.claude.json'))
26
- || existsSync(join(home, '.claude', 'credentials.json'));
32
+ || existsSync(join(home, '.claude', 'credentials.json'))
33
+ || existsSync(join(home, '.claude', '.credentials.json'));
27
34
  },
28
35
  generate: generateWithClaude,
29
36
  },
@@ -39,6 +46,31 @@ export const PROVIDERS = {
39
46
  generate: generateWithCodex,
40
47
  },
41
48
  };
49
+ const PROVIDER_BINARIES = {
50
+ 'claude-code': 'claude',
51
+ codex: 'codex',
52
+ };
53
+ const PROVIDER_AUTH_PATHS = {
54
+ 'claude-code': [
55
+ join(home, '.claude.json'),
56
+ join(home, '.claude', 'credentials.json'),
57
+ join(home, '.claude', '.credentials.json'),
58
+ ],
59
+ codex: [
60
+ join(home, '.codex', 'auth.json'),
61
+ ],
62
+ };
63
+ /** Diagnose why a provider is or isn't available */
64
+ export function diagnoseProvider(key) {
65
+ const binaryName = PROVIDER_BINARIES[key] ?? key;
66
+ const authPaths = PROVIDER_AUTH_PATHS[key] ?? [];
67
+ return {
68
+ binaryFound: hasBinary(binaryName),
69
+ authFound: authPaths.some(p => existsSync(p)),
70
+ binaryName,
71
+ authPaths,
72
+ };
73
+ }
42
74
  /** Detect which providers are available (binary + auth) */
43
75
  export async function detectAvailableProviders() {
44
76
  const available = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afromero/kin3o",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "AI-powered Lottie animation generator — text to motion from your terminal",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,6 +41,7 @@
41
41
  "@anthropic-ai/sdk": "^0.78.0",
42
42
  "@dotlottie/dotlottie-js": "^1.6.3",
43
43
  "commander": "^13.0.0",
44
+ "lottie-react": "^2.4.1",
44
45
  "open": "^10.1.0",
45
46
  "puppeteer-core": "^24.0.0"
46
47
  },