yesilcam-studios 0.1.0 → 0.3.1
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 +30 -11
- package/package.json +20 -6
- package/src/cli.js +187 -95
- package/src/client.js +95 -25
- package/src/config.js +32 -10
- package/src/constants.js +4 -1
- package/src/files.js +87 -0
- package/src/mcp.js +88 -50
- package/src/ops/_util.js +43 -0
- package/src/ops/account.js +62 -0
- package/src/ops/animation.js +126 -0
- package/src/ops/cinema.js +58 -0
- package/src/ops/design.js +75 -0
- package/src/ops/fashion.js +51 -0
- package/src/ops/film.js +197 -0
- package/src/ops/influencer.js +140 -0
- package/src/ops/library.js +56 -0
- package/src/ops/longvideo.js +106 -0
- package/src/ops/maket.js +65 -0
- package/src/ops/social.js +26 -0
- package/src/ops/website.js +92 -0
- package/src/registry.js +38 -0
- package/src/run.js +36 -0
- package/src/wait.js +167 -0
- package/src/jobs.js +0 -45
package/README.md
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
# yesilcam-studios
|
|
2
2
|
|
|
3
|
-
Yeşilçam Studios from your terminal (`yesilcam`) and from
|
|
3
|
+
Every Yeşilçam Studios studio from your terminal (`yesilcam`) and from AI assistants over MCP (`yesilcam-mcp`): Image Studio, Film Studio, Animation Studio (25 styles, Ink Landscape, Inkmation, Custom), Cinema Studio, Design Studio, Website Studio, Website Promo, Assemble / Disassemble, Fashion Try-On, Maket and AI Influencer Studio (long videos, automations, Discover). Same account and token wallet as https://yesilcamstudios.com/app/.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g yesilcam-studios
|
|
7
|
-
yesilcam login
|
|
8
|
-
yesilcam
|
|
9
|
-
yesilcam image "a lighthouse at dusk, 35mm film" --tier
|
|
10
|
-
yesilcam
|
|
11
|
-
yesilcam
|
|
12
|
-
yesilcam
|
|
13
|
-
yesilcam maket
|
|
14
|
-
yesilcam
|
|
7
|
+
yesilcam login # paste an API key from https://yesilcamstudios.com/app/account/developer
|
|
8
|
+
yesilcam account show
|
|
9
|
+
yesilcam image "a lighthouse at dusk, 35mm film" --tier draft --quote # price only
|
|
10
|
+
yesilcam image "a lighthouse at dusk, 35mm film" --tier draft --yes --out lighthouse.png
|
|
11
|
+
yesilcam influencer video --mode talk --influencer-id 70 --text "Hello!" --yes --wait
|
|
12
|
+
yesilcam longvideo plan --influencer-id 70 --topic "3 tips for phone photos" --seconds 60 --yes --wait
|
|
13
|
+
yesilcam maket analyze plan.dxf # local, free
|
|
14
|
+
yesilcam --help # all studios; yesilcam <studio> --help for actions
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
Anything that spends tokens shows a quote first: interactive prompt, `--yes` to approve in scripts, `--max-tokens` as a ceiling, `--quote` to only price. `--json` for machine output, `--wait` / `--out` for long jobs. Exit codes: 3 quote not approved, 2 not signed in.
|
|
18
|
+
|
|
19
|
+
## MCP (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf …)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
claude mcp add yesilcam -- yesilcam-mcp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{ "mcpServers": { "yesilcam": { "command": "yesilcam-mcp", "args": [] } } }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
No secret in the config: the server uses the key saved by `yesilcam login` (or `YESILCAM_API_KEY`). `--studios image,film,influencer` loads fewer tools, `--read-only` only reading tools. Tools that spend tokens return a quote and charge nothing until called again with `confirm=true` after the user approves. `yesilcam mcp` prints the snippets.
|
|
30
|
+
|
|
31
|
+
## Security
|
|
32
|
+
|
|
33
|
+
- API keys: read-only or read-and-create, optional expiry, revocable in the app; they cannot delete the account, change password/email, pay, create keys or connect social accounts. Only a hash is stored on the server.
|
|
34
|
+
- Credentials live in `~/.config/yesilcam/config.json` (0600) and are never printed. `yesilcam logout` removes them.
|
|
35
|
+
- Legacy sign-in with email and password: `yesilcam login --password`.
|
|
36
|
+
|
|
37
|
+
Docs: https://yesilcamstudios.com/developers
|
package/package.json
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yesilcam-studios",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Yeşilçam Studios CLI and MCP server: AI film, fashion try-on and Maket from your terminal or from Claude.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
"bin": {
|
|
8
|
+
"yesilcam": "bin/yesilcam.js",
|
|
9
|
+
"yesilcam-mcp": "bin/yesilcam-mcp.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"pack": "npm pack"
|
|
22
|
+
},
|
|
11
23
|
"dependencies": {
|
|
12
24
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
13
25
|
"commander": "^15.0.0",
|
|
14
26
|
"zod": "^4.6.5"
|
|
15
27
|
},
|
|
16
|
-
"devDependencies": {
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"vitest": "^2.1.8"
|
|
30
|
+
}
|
|
17
31
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,120 +1,212 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { createInterface } from 'node:readline
|
|
5
|
-
import {
|
|
6
|
-
import { login, makeClient, ApiError } from './client.js'
|
|
7
|
-
import { readConfig, writeConfig, clearConfig,
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
1
|
+
// yesilcam CLI: `yesilcam <stüdyo> <eylem>`; komutlar işlem kataloğundan türetilir (MCP araçlarıyla birebir).
|
|
2
|
+
import { Command, Option } from 'commander'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { createInterface } from 'node:readline'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { login, makeClient, exchangeKey, isApiKey, ApiError, friendlyError } from './client.js'
|
|
7
|
+
import { readConfig, writeConfig, clearConfig, getCredentials, permissionWarning, configFile } from './config.js'
|
|
8
|
+
import { OPS } from './registry.js'
|
|
9
|
+
import { execute } from './run.js'
|
|
10
|
+
import { checkOnce } from './wait.js'
|
|
11
|
+
import { VERSION, KEYS_URL, SITE_URL } from './constants.js'
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
export const GROUPS = {
|
|
14
|
+
account: 'Account, plan, balance, prices', jobs: 'Generation jobs', library: 'Your library: upload, download, share', wait: 'Wait for any job or project',
|
|
15
|
+
image: 'Image Studio', video: 'Film Studio videos (text, image, restyle, characters, lip sync)', characters: 'Characters and Character Bible', voices: 'Voices', prompt: 'Prompt help from Sam',
|
|
16
|
+
film: 'Film Studio: Story to Film and Flow', animation: 'Animation Studio (25 styles, Ink Landscape, Custom)', inkmation: 'Inkmation', cinema: 'Cinema Studio (video editor)',
|
|
17
|
+
design: 'Design Studio', website: 'Website Studio', promo: 'Website Promo', assemble: 'Assemble / Disassemble product videos', fashion: 'Fashion Try-On', maket: 'Maket (floor plan to 3D)',
|
|
18
|
+
influencer: 'AI Influencer Studio', discover: 'Discover ready-made influencers', longvideo: 'Long videos (15 s to 10 min)', automation: 'Influencer automations', social: 'Social media publishing',
|
|
19
|
+
}
|
|
20
|
+
const DEFAULT_ACTION = { wait: 'until', image: 'create', video: 'create', characters: 'list', jobs: 'list', library: 'list', voices: 'list' }
|
|
21
|
+
|
|
22
|
+
// ── zod alan çözümleme
|
|
23
|
+
function unwrap(s) { let t = s; let optional = false; let desc = s.description; while (['optional', 'default', 'nullable'].includes(t?._zod?.def?.type)) { optional = true; t = t._zod.def.innerType; desc = desc || t.description } return { t, optional, desc: desc || t.description } }
|
|
24
|
+
export function fieldInfo(schema) {
|
|
25
|
+
const { t, optional, desc } = unwrap(schema)
|
|
26
|
+
const type = t._zod.def.type
|
|
27
|
+
const info = { optional, desc, type }
|
|
28
|
+
if (type === 'enum') info.choices = Object.values(t._zod.def.entries)
|
|
29
|
+
if (type === 'union') { const opts = t._zod.def.options.map((o) => o._zod.def); info.literals = opts.every((o) => o.type === 'literal') ? opts.flatMap((o) => o.values) : null; info.hasNumber = opts.some((o) => o.type === 'number'); info.hasString = opts.some((o) => o.type === 'string'); info.hasLiteral = opts.some((o) => o.type === 'literal'); info.numeric = opts.some((o) => o.type === 'number') || (info.literals || []).every((v) => typeof v === 'number') }
|
|
30
|
+
if (type === 'array') { const el = t._zod.def.element._zod.def; info.element = el.type === 'union' ? 'id' : el.type; if (el.type === 'enum') info.choices = Object.values(el.entries) }
|
|
31
|
+
return info
|
|
32
|
+
}
|
|
33
|
+
const kebab = (s) => s.replace(/_/g, '-')
|
|
34
|
+
const camel = (s) => s.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
|
|
35
|
+
|
|
36
|
+
/** Komut satırı değeri → alan türü. JSON alanlar: '{...}' / '[...]' ya da @dosya.json */
|
|
37
|
+
export function coerce(info, raw) {
|
|
38
|
+
if (raw === undefined) return undefined
|
|
39
|
+
const json = (v) => { const s = String(v); const txt = s.startsWith('@') ? readFileSync(s.slice(1), 'utf8') : s; try { return JSON.parse(txt) } catch { throw new Error(`invalid JSON: ${s.slice(0, 40)}`) } }
|
|
40
|
+
switch (info.type) {
|
|
41
|
+
case 'number': return Number(raw)
|
|
42
|
+
case 'boolean': return Boolean(raw)
|
|
43
|
+
case 'union': { const isNum = /^-?\d+(\.\d+)?$/.test(String(raw)); if (!isNum) return raw; if (info.literals) return info.literals.includes(Number(raw)) ? Number(raw) : raw; return info.hasNumber && !info.hasString ? Number(raw) : (info.hasNumber && info.hasLiteral ? Number(raw) : raw) }
|
|
44
|
+
case 'array':
|
|
45
|
+
if (['object', 'record'].includes(info.element) || String(raw).trim().startsWith('[') || String(raw).startsWith('@')) return json(raw)
|
|
46
|
+
return String(raw).split(',').map((x) => x.trim()).filter(Boolean).map((x) => (info.element === 'number' ? Number(x) : x))
|
|
47
|
+
case 'object': case 'record': return json(raw)
|
|
48
|
+
default: return raw
|
|
49
|
+
}
|
|
50
|
+
}
|
|
16
51
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
52
|
+
// ── çıktı
|
|
53
|
+
const fmt = (n) => (typeof n === 'number' ? n.toLocaleString('en-US') : n)
|
|
54
|
+
const MONEY = /(token|balance|total|price|cost|charged|need|spent|cap|amount|per_run|per_month)/i
|
|
55
|
+
const fmtKV = (k, v) => (typeof v === 'number' && MONEY.test(k) ? fmt(v) : v)
|
|
56
|
+
const LINKY = /(url|thumb|poster|cover|preview|avatar|image)$/i
|
|
57
|
+
export function human(v, indent = '') {
|
|
58
|
+
if (v == null) return `${indent}-`
|
|
59
|
+
if (Array.isArray(v)) {
|
|
60
|
+
if (!v.length) return `${indent}(none)`
|
|
61
|
+
return v.map((x) => (x && typeof x === 'object' ? `${indent}- ${Object.entries(x).filter(([k, y]) => y != null && typeof y !== 'object' && !LINKY.test(k)).slice(0, 7).map(([k, y]) => `${k}: ${fmtKV(k, y)}`).join(' ')}` : `${indent}- ${x}`)).join('\n')
|
|
62
|
+
}
|
|
63
|
+
if (typeof v === 'object') return Object.entries(v).filter(([, y]) => y != null).map(([k, y]) => (y && typeof y === 'object' ? `${indent}${k}:\n${human(y, indent + ' ')}` : `${indent}${k}: ${fmtKV(k, y)}`)).join('\n')
|
|
64
|
+
return `${indent}${v}`
|
|
65
|
+
}
|
|
66
|
+
export function quoteText(q) {
|
|
67
|
+
const lines = (q.items || []).map((i) => ` ${String(i.item).padEnd(44)} ${fmt(i.tokens).padStart(12)}`)
|
|
68
|
+
return [`Quote${q.approximate ? ' (estimate)' : ''}:`, ...lines, ` ${'Total'.padEnd(44)} ${fmt(q.total).padStart(12)} tokens`, q.balance != null ? ` Balance: ${fmt(q.balance)} tokens` : null, q.notes ? ` ${q.notes}` : null].filter(Boolean).join('\n')
|
|
20
69
|
}
|
|
21
70
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
71
|
+
function ask(question, { hidden = false } = {}) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true })
|
|
74
|
+
if (hidden) { rl._writeToOutput = (s) => { if (s.includes(question)) rl.output.write(s) } }
|
|
75
|
+
rl.question(question, (ans) => { rl.close(); if (hidden) process.stderr.write('\n'); resolve(ans) })
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
const readStdin = async () => { let d = ''; for await (const ch of process.stdin) d += ch; return d.trim() }
|
|
79
|
+
|
|
80
|
+
export function getClient() {
|
|
81
|
+
const cred = getCredentials()
|
|
82
|
+
if (!cred) throw new ApiError('not_logged_in')
|
|
83
|
+
const w = permissionWarning(); if (w) process.stderr.write(`${w}\n`)
|
|
84
|
+
return makeClient(cred)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function progressLine(st) {
|
|
88
|
+
if (!process.stderr.isTTY || !st) return
|
|
89
|
+
const pct = st.progress != null ? ` ${st.progress}%` : ''
|
|
90
|
+
process.stderr.write(`\r${String(st.status || '').padEnd(12)}${pct}${st.stage ? ` · ${st.stage}` : ''}`.padEnd(70).slice(0, 78))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Bir işlemi commander komutuna bağlar. */
|
|
94
|
+
export function attachOp(cmd, op, program) {
|
|
95
|
+
const entries = Object.entries(op.input)
|
|
96
|
+
const posList = op.positional ? [].concat(op.positional) : []
|
|
97
|
+
for (const p of posList) { const info = fieldInfo(op.input[p]); cmd.argument(info.optional ? `[${p}]` : `<${p}>`, info.desc || p) }
|
|
98
|
+
for (const [name, schema] of entries) {
|
|
99
|
+
if (posList.includes(name)) continue
|
|
100
|
+
const info = fieldInfo(schema)
|
|
101
|
+
const flag = `--${kebab(name)}`
|
|
102
|
+
const hint = [info.desc, info.choices ? `(${info.choices.join('|')})` : null, info.literals ? `(${info.literals.join('|')})` : null, ['object', 'record'].includes(info.type) || ['object', 'record'].includes(info.element) ? '(JSON or @file.json)' : null, info.type === 'array' && !['object', 'record'].includes(info.element) ? '(comma-separated)' : null].filter(Boolean).join(' ')
|
|
103
|
+
if (info.type === 'boolean') { cmd.option(flag, hint); cmd.addOption(new Option(`--no-${kebab(name)}`).hideHelp()) }
|
|
104
|
+
else cmd.option(`${flag} <value>`, `${hint}${info.optional ? '' : ' (required)'}`)
|
|
105
|
+
}
|
|
106
|
+
if (op.kind === 'spend') cmd.option('-y, --yes', 'approve the quoted token cost without asking').option('--max-tokens <n>', 'refuse if the quote is above this').option('--quote', 'only show the price')
|
|
107
|
+
if (op.wait) cmd.option('-w, --wait', 'wait for the result').option('-o, --out <path>', 'download the result to a file or folder (implies --wait)').option('--timeout <seconds>', 'max wait (default 900)')
|
|
108
|
+
cmd.action(async (...argv) => {
|
|
109
|
+
const opts = argv[argv.length - 2] || {}
|
|
110
|
+
const json = program.opts().json
|
|
111
|
+
const args = {}
|
|
112
|
+
posList.forEach((p, i) => { if (argv[i] !== undefined && i < argv.length - 2) args[p] = coerce(fieldInfo(op.input[p]), argv[i]) })
|
|
113
|
+
for (const [name, schema] of entries) {
|
|
114
|
+
if (posList.includes(name)) continue
|
|
115
|
+
const v = opts[camel(name)]
|
|
116
|
+
if (v !== undefined) args[name] = coerce(fieldInfo(schema), v)
|
|
117
|
+
}
|
|
118
|
+
const parsed = z.object(op.input).safeParse(args)
|
|
119
|
+
if (!parsed.success) { const i = parsed.error.issues[0]; throw new Error(`--${kebab(String(i.path[0] || ''))}: ${i.message}`) }
|
|
120
|
+
const shared = { max_tokens: opts.maxTokens != null ? Number(opts.maxTokens) : undefined, wait: Boolean(opts.wait || opts.out), save_to: opts.out, max_wait_seconds: opts.timeout ? Number(opts.timeout) : undefined }
|
|
121
|
+
const c = op.kind === 'local' ? null : getClient()
|
|
122
|
+
const askConfirm = async (q) => {
|
|
123
|
+
if (opts.quote) return false
|
|
124
|
+
if (!process.stdin.isTTY || json) { return false }
|
|
125
|
+
process.stderr.write(`${quoteText(q)}\n`)
|
|
126
|
+
const ans = await ask(`Spend about ${fmt(q.total)} tokens? [y/N] `)
|
|
127
|
+
return /^y(es)?$/i.test(String(ans).trim())
|
|
128
|
+
}
|
|
129
|
+
const r = await execute(op, c, { ...parsed.data, ...shared }, { confirmed: Boolean(opts.yes) && !opts.quote, askConfirm, onProgress: json ? undefined : progressLine })
|
|
130
|
+
if (process.stderr.isTTY && shared.wait && !json) process.stderr.write('\n')
|
|
131
|
+
if (r.status === 'quote') {
|
|
132
|
+
if (json) console.log(JSON.stringify({ status: 'quote', quote: r.quote }, null, 2))
|
|
133
|
+
else { console.log(quoteText(r.quote)); console.log(opts.quote ? 'Nothing was charged.' : 'Nothing was charged. Re-run with --yes to approve.') }
|
|
134
|
+
process.exitCode = opts.quote ? 0 : 3; return
|
|
135
|
+
}
|
|
136
|
+
if (r.status !== 'done') { if (json) console.log(JSON.stringify(r, null, 2)); else { if (r.quote) console.log(quoteText(r.quote)); console.log(r.message || r.quote?.notes || r.status) } process.exitCode = 4; return }
|
|
137
|
+
if (json) { console.log(JSON.stringify({ result: r.result, ...(r.final ? { final: r.final } : {}), ...(r.track ? { track: r.track } : {}) }, null, 2)); return }
|
|
138
|
+
console.log(human(r.result))
|
|
139
|
+
if (r.final) { console.log(`\n${human({ status: r.final.status, url: r.final.url, saved: r.final.saved, error: r.final.error, next: r.final.needs_action || r.final.hint })}`) }
|
|
140
|
+
else if (r.track) console.log(`\nTrack: yesilcam wait ${r.track.kind} ${r.track.id} (or add --wait next time)`)
|
|
141
|
+
})
|
|
142
|
+
return cmd
|
|
32
143
|
}
|
|
33
144
|
|
|
34
145
|
export function buildProgram() {
|
|
35
146
|
const program = new Command()
|
|
36
|
-
program.name('yesilcam').description(
|
|
37
|
-
.option('--json', 'machine-readable output')
|
|
147
|
+
program.name('yesilcam').description(`Yeşilçam Studios ${VERSION}: AI film, image, animation, design, website, fashion, Maket and AI Influencer studios from your terminal.\nPrices are shown before anything is charged. Docs: ${SITE_URL}/developers`)
|
|
148
|
+
.version(VERSION).option('--json', 'machine-readable JSON output').showHelpAfterError()
|
|
38
149
|
|
|
39
|
-
|
|
40
|
-
|
|
150
|
+
// ── giriş
|
|
151
|
+
program.command('login').description(`Sign in. Default: paste an API key (create one at ${KEYS_URL}). Stored in ${configFile()} (owner-only), never printed.`)
|
|
152
|
+
.option('--key-stdin', 'read the API key from stdin (CI)').option('--password', 'sign in with email + password instead (legacy session)').option('--email <email>')
|
|
41
153
|
.action(async (o) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
154
|
+
const json = program.opts().json
|
|
155
|
+
if (o.password) {
|
|
156
|
+
const email = o.email || await ask('Email: ')
|
|
157
|
+
const pw = process.env.YESILCAM_PASSWORD || await ask('Password: ', { hidden: true })
|
|
158
|
+
const r = await login(String(email).trim(), pw)
|
|
159
|
+
writeConfig({ token: r.token, user: r.user, method: 'password' })
|
|
160
|
+
const msg = `Signed in as ${r.user.name} ${r.user.lastname} <${r.user.email}> (session saved to ${configFile()}).`
|
|
161
|
+
console.log(json ? JSON.stringify({ ok: true, user: r.user, method: 'password' }) : msg); return
|
|
49
162
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
163
|
+
const key = (o.keyStdin ? await readStdin() : (process.env.YESILCAM_API_KEY || await ask(`API key (from ${KEYS_URL}): `, { hidden: true }))).trim()
|
|
164
|
+
if (!isApiKey(key)) throw new ApiError('api_key_invalid')
|
|
165
|
+
const s = await exchangeKey(key)
|
|
166
|
+
writeConfig({ apiKey: key, user: s.user, key: s.key, scopes: s.scopes, method: 'api_key' })
|
|
167
|
+
const msg = `Signed in as ${s.user.name} ${s.user.lastname} <${s.user.email}> with key "${s.key.name}" (${s.key.prefix}…, scopes: ${s.scopes.join(', ')}). Saved to ${configFile()}.`
|
|
168
|
+
console.log(json ? JSON.stringify({ ok: true, user: s.user, key: s.key, scopes: s.scopes, method: 'api_key' }) : msg)
|
|
53
169
|
})
|
|
54
|
-
program.command('logout').description('Remove the saved
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
program.command('pricing').description('Production costs (tokens) and token packs').action(async () => {
|
|
58
|
-
const c = client(); const [ops, packs] = await Promise.all([c.modulePricing(), c.tokenPackages()])
|
|
59
|
-
const rows = ops.filter((r) => ['ai-studio', 'fashion-tryon'].includes(r.module_key) && String(r.status ?? '1') === '1').map((r) => ({ module: r.module_key, operation: r.operation_key, tokens: Number(r.base_cost_tokens), free_per_month: Number(r.monthly_free_limit || 0) })).sort((a, b) => a.tokens - b.tokens)
|
|
60
|
-
const pk = packs.map((p) => ({ id: p.id, title: p.title, tokens: Number(p.token_amount), bonus_percent: Number(p.bonus_percentage), usd: Number(p.price_usd), usd_without_plan: p.price_usd_standard != null ? Number(p.price_usd_standard) : null, try: Number(p.price_tr), try_without_plan: p.price_tr_standard != null ? Number(p.price_tr_standard) : null }))
|
|
61
|
-
out(program, { operations: rows, packs: pk }, [...rows.map((r) => `${r.module.padEnd(14)} ${r.operation.padEnd(24)} ${fmtInt(r.tokens).padStart(10)} tokens${r.free_per_month ? ` (${r.free_per_month} free/month)` : ''}`), '', ...pk.map((p) => `${p.title.padEnd(14)} ${fmtInt(p.tokens).padStart(10)} tokens $${p.usd} with plan / $${p.usd_without_plan ?? p.usd} without${p.bonus_percent ? ` +${p.bonus_percent}% bonus` : ''}`)].join('\n'))
|
|
170
|
+
program.command('logout').description('Remove the saved credentials from this computer').action(() => {
|
|
171
|
+
const had = readConfig(); clearConfig()
|
|
172
|
+
console.log(program.opts().json ? JSON.stringify({ ok: true }) : `Signed out.${had.apiKey ? ` The API key still works until you revoke it at ${KEYS_URL}` : ''}`)
|
|
62
173
|
})
|
|
174
|
+
program.command('mcp').description('Show how to connect the MCP server to Claude Code, Claude Desktop, Cursor and others').action(() => {
|
|
175
|
+
console.log(`Sign in once with "yesilcam login"; the MCP server reads the same credentials (no secret in the config below).
|
|
63
176
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const r = await client().assets({ kind: o.kind, limit: Number(o.limit) })
|
|
67
|
-
const list = r?.assets || r?.items || r || []
|
|
68
|
-
out(program, list, list.map((a) => `${String(a.id).padEnd(8)} ${String(a.kind).padEnd(6)} ${a.label || ''} ${a.spaces_url || ''}`).join('\n') || 'No assets.')
|
|
69
|
-
})
|
|
70
|
-
assets.command('upload <file>').option('--kind <kind>', 'image|video|audio').option('--label <label>').action(async (file, o) => {
|
|
71
|
-
const ext = extname(file).toLowerCase(); const mime = MIME[ext] || 'application/octet-stream'
|
|
72
|
-
const kind = o.kind || (mime.startsWith('video') ? 'video' : mime.startsWith('audio') ? 'audio' : 'image')
|
|
73
|
-
const b64 = `data:${mime};base64,${readFileSync(file).toString('base64')}`
|
|
74
|
-
const r = await client().uploadAsset(b64, kind, o.label || basename(file))
|
|
75
|
-
out(program, r, `Uploaded asset ${r?.asset_id || r?.id || ''} ${r?.url || r?.spaces_url || ''}`)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
const jobs = program.command('jobs').description('Generation jobs')
|
|
79
|
-
jobs.command('list').option('--limit <n>', '', '20').action(async (o) => { const r = await client().jobs({ limit: Number(o.limit) }); const list = r?.jobs || []; out(program, list, list.map((j) => `${String(j.id).padEnd(8)} ${String(j.job_type).padEnd(16)} ${String(j.status).padEnd(11)} ${j.quality_tier || ''}`).join('\n') || 'No jobs.') })
|
|
80
|
-
jobs.command('status <id>').option('--wait').option('--out <file>').action(async (id, o) => {
|
|
81
|
-
const c = client(); const job = o.wait ? await waitForJob(c, id) : (await c.jobStatus(Number(id))).job
|
|
82
|
-
const d = describeJob(job); if (o.out && d?.result?.url) { await download(d.result.url, o.out); d.saved = o.out }
|
|
83
|
-
out(program, d, `${d.id} ${d.type} ${d.status}${d.result ? ` ${d.result.url}` : ''}${d.saved ? `\nSaved: ${d.saved}` : ''}`)
|
|
84
|
-
})
|
|
177
|
+
Claude Code:
|
|
178
|
+
claude mcp add yesilcam -- yesilcam-mcp
|
|
85
179
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.option('--wait', 'wait for the result').option('--out <file>', 'download result (implies --wait)')
|
|
89
|
-
.action(async (prompt, o) => runJob(program, client(), imageJob({ prompt, tier: o.tier, aspect: o.aspect, framing: o.framing }), { wait: o.wait || Boolean(o.out), out: o.out }))
|
|
90
|
-
program.command('video [prompt]').description('Generate a video (text2video, or image2video with --from)')
|
|
91
|
-
.option('--from <assetId>', 'source image asset id').option('--tier <tier>', TIERS.join('|'), 'draft').option('--aspect <ratio>', '', '16:9').option('--duration <s>').option('--resolution <r>', '480p|720p|1080p').option('--audio', 'generate audio')
|
|
92
|
-
.option('--wait').option('--out <file>')
|
|
93
|
-
.action(async (prompt = '', o) => runJob(program, client(), videoJob({ prompt, tier: o.tier, aspect: o.aspect, sourceAssetId: o.from, duration: o.duration, resolution: o.resolution, generateAudio: o.audio }), { wait: o.wait || Boolean(o.out), out: o.out }))
|
|
94
|
-
program.command('tryon').description('Fashion try-on: garment on a person or character')
|
|
95
|
-
.requiredOption('--garment <assetId>').option('--person <assetId>').option('--character <id>').option('--prompt <text>', 'style note').option('--wait').option('--out <file>')
|
|
96
|
-
.action(async (o) => { if (!o.person && !o.character) { console.error('--person <assetId> or --character <id> is required'); process.exit(2) } return runJob(program, client(), tryonJob({ garmentAssetId: o.garment, personAssetId: o.person, characterId: o.character, prompt: o.prompt }), { wait: o.wait || Boolean(o.out), out: o.out }) })
|
|
97
|
-
program.command('characters').description('List your characters').action(async () => { const r = await client().characters(); const list = r?.characters || r || []; out(program, list, list.map((c) => `${String(c.id).padEnd(8)} ${c.name || ''}`).join('\n') || 'No characters.') })
|
|
180
|
+
Claude Desktop (claude_desktop_config.json), Cursor (~/.cursor/mcp.json), Windsurf, VS Code and others:
|
|
181
|
+
${JSON.stringify({ mcpServers: { yesilcam: { command: 'yesilcam-mcp', args: [] } } }, null, 2)}
|
|
98
182
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
maket.command('save <file>').description('Save a plan as a Maket project').option('--title <title>').action(async (file, o) => {
|
|
103
|
-
const text = readFileSync(file, 'utf8'); const a = analyzePlanText(text, basename(file))
|
|
104
|
-
const r = await client().maketCreate({ title: o.title || basename(file, extname(file)), source_kind: a.source_kind, source_text: text, settings: { overrides: {}, wallHeight: 2.9 }, stats: a.stats })
|
|
105
|
-
out(program, r, `Saved project ${r.id}. Open it at ${SITE_URL}/app/maket`)
|
|
183
|
+
Without a global install: "command": "npx", "args": ["-y", "-p", "yesilcam-studios", "yesilcam-mcp"]
|
|
184
|
+
Fewer tools: "args": ["--studios", "image,film,influencer"] · Read-only: "args": ["--read-only"]
|
|
185
|
+
On a server or CI, set YESILCAM_API_KEY in the environment instead of signing in.`)
|
|
106
186
|
})
|
|
107
|
-
maket.command('get <id>').description('Download a project’s plan').option('--out <file>').action(async (id, o) => { const r = await client().maketGet(Number(id)); const p = r.project; const file = o.out || `${p.title || 'maket'}.${p.source_kind === 'json' ? 'json' : 'dxf'}`; writeFileSync(file, p.source_text); out(program, { id: p.id, title: p.title, saved: file }, `Saved: ${file}`) })
|
|
108
|
-
maket.command('delete <id>').action(async (id) => { await client().maketDelete(Number(id)); out(program, { ok: true }, 'Deleted.') })
|
|
109
187
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
188
|
+
// ── katalogdan komutlar
|
|
189
|
+
const groups = new Map()
|
|
190
|
+
for (const op of OPS) {
|
|
191
|
+
if (!groups.has(op.group)) { const g = program.command(op.group).description(GROUPS[op.group] || op.group); groups.set(op.group, g); if (op.group === 'library') g.alias('assets') }
|
|
192
|
+
const g = groups.get(op.group)
|
|
193
|
+
const sub = g.command(op.action, { isDefault: DEFAULT_ACTION[op.group] === op.action }).description(`${op.summary}${op.kind === 'spend' ? ' [tokens]' : ''}`)
|
|
194
|
+
attachOp(sub, op, program)
|
|
195
|
+
}
|
|
196
|
+
// kısayollar (0.1 / 0.2 uyumu)
|
|
197
|
+
const byName = Object.fromEntries(OPS.map((o) => [o.name, o]))
|
|
198
|
+
for (const [alias, name] of [['balance', 'balance'], ['pricing', 'pricing'], ['whoami', 'account'], ['tryon', 'tryon']]) attachOp(program.command(alias).description(byName[name].summary), byName[name], program)
|
|
113
199
|
return program
|
|
114
200
|
}
|
|
115
201
|
|
|
116
202
|
export async function main(argv) {
|
|
117
203
|
const program = buildProgram()
|
|
118
204
|
try { await program.parseAsync(argv) }
|
|
119
|
-
catch (e) {
|
|
205
|
+
catch (e) {
|
|
206
|
+
const msg = e instanceof ApiError ? friendlyError(e) : (e?.message || String(e))
|
|
207
|
+
if (program.opts().json) console.log(JSON.stringify({ error: e?.code || 'error', message: msg }))
|
|
208
|
+
else console.error(`Error: ${msg}`)
|
|
209
|
+
process.exitCode = e?.code === 'not_logged_in' ? 2 : 1
|
|
210
|
+
}
|
|
120
211
|
}
|
|
212
|
+
export { checkOnce }
|
package/src/client.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
// Apiollon istemcisi: POST <API>/flow/<uç>, JSON gövde, Bearer belirteç; yanıt { status, result } (hata da HTTP 200 + status:false).
|
|
2
|
+
// Kimlik: (1) API anahtarı (ysk_…) → ys-apikey-exchange ile 1 saatlik oturum (bellekte; süresi dolunca yenilenir),
|
|
3
|
+
// (2) eski usul kullanıcı oturum belirteci (e-posta/şifre girişi). Gizli bilgiler hiçbir zaman yazdırılmaz.
|
|
4
|
+
import { API_URL, APP_KEY, APP_SECRET } from './constants.js'
|
|
2
5
|
|
|
3
6
|
const base = API_URL.replace(/\/+$/, '') + '/'
|
|
4
7
|
let appTokenCache = null
|
|
5
8
|
|
|
6
|
-
export class ApiError extends Error {
|
|
9
|
+
export class ApiError extends Error {
|
|
10
|
+
constructor(message, data) { super(message); this.code = String(message || ''); this.data = data }
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
async function post(path, body, bearer, fetchImpl = globalThis.fetch) {
|
|
9
|
-
const headers = { 'Content-Type': 'application/json' }
|
|
14
|
+
const headers = { 'Content-Type': 'application/json', 'User-Agent': 'yesilcam-studios-cli' }
|
|
10
15
|
if (bearer) headers.Authorization = `Bearer ${bearer}`
|
|
11
|
-
|
|
16
|
+
let res
|
|
17
|
+
try { res = await fetchImpl(base + path, { method: 'POST', headers, body: JSON.stringify(body || {}) }) }
|
|
18
|
+
catch (e) { throw new ApiError('network_error', { cause: String(e?.message || e) }) }
|
|
12
19
|
const data = await res.json().catch(() => null)
|
|
13
|
-
if (!data || data.status !== true)
|
|
20
|
+
if (!data || data.status !== true) {
|
|
21
|
+
const msg = (data && (data.message || data?.result?.error)) || `request_failed (${res.status})`
|
|
22
|
+
throw new ApiError(String(msg).split(' — ')[0], data)
|
|
23
|
+
}
|
|
14
24
|
return data.result
|
|
15
25
|
}
|
|
16
26
|
|
|
@@ -21,8 +31,9 @@ export async function getAppToken(fetchImpl) {
|
|
|
21
31
|
appTokenCache = r.token
|
|
22
32
|
return appTokenCache
|
|
23
33
|
}
|
|
34
|
+
export function _resetAppToken() { appTokenCache = null }
|
|
24
35
|
|
|
25
|
-
/** E-posta + şifre → kullanıcı
|
|
36
|
+
/** E-posta + şifre → kullanıcı oturum belirteci ve profil (eski yöntem; API anahtarı önerilir). */
|
|
26
37
|
export async function login(email, password, fetchImpl) {
|
|
27
38
|
const app = await getAppToken(fetchImpl)
|
|
28
39
|
const r = await post('flow/user-login', { credential: email, password }, app, fetchImpl)
|
|
@@ -30,25 +41,84 @@ export async function login(email, password, fetchImpl) {
|
|
|
30
41
|
return { token: r.token, user: { id: String(r.id), name: r.name || '', lastname: r.lastname || '', email: r.email || '' } }
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
44
|
+
/** API anahtarı → kısa ömürlü oturum ({ token, expires_at, scopes, key, user }). */
|
|
45
|
+
export async function exchangeKey(apiKey, fetchImpl) {
|
|
46
|
+
const app = await getAppToken(fetchImpl)
|
|
47
|
+
try { return await post('flow/ys-apikey-exchange', { key: apiKey }, app, fetchImpl) }
|
|
48
|
+
catch (e) {
|
|
49
|
+
if (/invalid method/i.test(e.message)) throw new ApiError('api_keys_not_enabled', e.data)
|
|
50
|
+
throw e
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const isApiKey = (s) => /^ysk_[a-f0-9]{48}$/.test(String(s || '').trim())
|
|
55
|
+
|
|
56
|
+
const SESSION_ERRORS = /^(api_session_expired|api_session_invalid|invalid token)$/i
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* makeClient({ apiKey } | { token }) ya da makeClient('<token>') (geriye uyum).
|
|
60
|
+
* c.call(uç, gövde) → result. c.session() → anahtar oturum bilgisi (kapsamlar, kullanıcı).
|
|
61
|
+
*/
|
|
62
|
+
export function makeClient(cred, fetchImpl) {
|
|
63
|
+
const creds = typeof cred === 'string' ? (isApiKey(cred) ? { apiKey: cred } : { token: cred }) : (cred || {})
|
|
64
|
+
if (!creds.apiKey && !creds.token) throw new ApiError('not_logged_in')
|
|
65
|
+
let session = null
|
|
66
|
+
const bearer = async (force = false) => {
|
|
67
|
+
if (!creds.apiKey) return creds.token
|
|
68
|
+
if (force || !session || session.expires_at * 1000 - Date.now() < 60_000) session = await exchangeKey(creds.apiKey, fetchImpl)
|
|
69
|
+
return session.token
|
|
70
|
+
}
|
|
71
|
+
const call = async (name, body = {}) => {
|
|
72
|
+
try { return await post(`flow/${name}`, body, await bearer(), fetchImpl) }
|
|
73
|
+
catch (e) {
|
|
74
|
+
if (creds.apiKey && SESSION_ERRORS.test(e.message)) return post(`flow/${name}`, body, await bearer(true), fetchImpl)
|
|
75
|
+
throw e
|
|
76
|
+
}
|
|
77
|
+
}
|
|
36
78
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
createJob: (p) => flow('aistudio-create-job', p),
|
|
46
|
-
jobStatus: (jobId) => flow('aistudio-job-status', { job_id: jobId }),
|
|
47
|
-
jobs: (p = {}) => flow('aistudio-job-list', p),
|
|
48
|
-
characters: () => flow('aistudio-character-list'),
|
|
49
|
-
maketList: () => flow('maket-list-projects'),
|
|
50
|
-
maketGet: (projectId) => flow('maket-get-project', { projectId }),
|
|
51
|
-
maketCreate: (p) => flow('maket-create-project', p),
|
|
52
|
-
maketDelete: (projectId) => flow('maket-delete-project', { projectId }),
|
|
79
|
+
call,
|
|
80
|
+
flow: call,
|
|
81
|
+
kind: creds.apiKey ? 'api_key' : 'token',
|
|
82
|
+
session: async () => { if (creds.apiKey) await bearer(); return session },
|
|
83
|
+
// geriye uyum kısayolları (0.2)
|
|
84
|
+
balance: () => call('get-token-balance'),
|
|
85
|
+
me: () => call('get-user-subscription-infos'),
|
|
86
|
+
jobStatus: (jobId) => call('aistudio-job-status', { job_id: Number(jobId) }),
|
|
53
87
|
}
|
|
54
88
|
}
|
|
89
|
+
|
|
90
|
+
/** Sunucu hata kodu → sade İngilizce açıklama (bilinmeyen kod olduğu gibi döner). */
|
|
91
|
+
const FRIENDLY = {
|
|
92
|
+
'Insufficient tokens': 'Not enough tokens. Top up or upgrade at https://yesilcamstudios.com/app/billing',
|
|
93
|
+
insufficient_tokens: 'Not enough tokens. Top up or upgrade at https://yesilcamstudios.com/app/billing',
|
|
94
|
+
subscription_required: 'This needs a Basic or Pro plan: https://yesilcamstudios.com/app/billing',
|
|
95
|
+
pro_required: 'This needs the Pro plan: https://yesilcamstudios.com/app/billing',
|
|
96
|
+
too_many_running: 'Too many jobs are running at once. Wait for one to finish and try again.',
|
|
97
|
+
minor_text: 'The text seems to describe a minor. Characters must be adults.',
|
|
98
|
+
public_figure_text: 'Real public figures cannot be used.',
|
|
99
|
+
unsafe_content: 'The request was blocked by the safety check.',
|
|
100
|
+
safety_filter: 'The request was blocked by the safety check.',
|
|
101
|
+
consent_required: 'Explicit consent is required for this upload (pass consent / --consent).',
|
|
102
|
+
not_logged_in: 'Not signed in. Run: yesilcam login',
|
|
103
|
+
api_keys_not_enabled: 'API keys are not enabled on the server yet. Use: yesilcam login --password',
|
|
104
|
+
api_key_invalid: 'The API key is not valid. Create a new one at https://yesilcamstudios.com/app/account/developer',
|
|
105
|
+
api_key_revoked: 'This API key was revoked. Create a new one at https://yesilcamstudios.com/app/account/developer',
|
|
106
|
+
api_key_expired: 'This API key has expired. Create a new one at https://yesilcamstudios.com/app/account/developer',
|
|
107
|
+
api_key_scope: 'This API key is read-only. Create a key with the "write" scope to create, edit or spend tokens.',
|
|
108
|
+
api_key_forbidden: 'API keys cannot do this (account, payment and key management stay in the web app).',
|
|
109
|
+
social_unavailable: 'Publishing to social media is coming soon.',
|
|
110
|
+
edit_unavailable: 'Long videos are not available on this account yet.',
|
|
111
|
+
auto_unavailable: 'Automations are not available on this account yet.',
|
|
112
|
+
network_error: 'Could not reach the Yeşilçam Studios API. Check your connection.',
|
|
113
|
+
quote_changed: 'The price changed since the quote. Get a fresh quote and confirm again.',
|
|
114
|
+
voice_not_set: 'Set a voice for this character first (influencer voice-set).',
|
|
115
|
+
edit_busy: 'The project is busy (planning or producing). Wait and try again.',
|
|
116
|
+
still_running: 'It is still running. Wait for it to finish first.',
|
|
117
|
+
'Invalid method': 'This feature is not available on the server yet.',
|
|
118
|
+
}
|
|
119
|
+
export function friendlyError(e) {
|
|
120
|
+
const code = String(e?.code || e?.message || e || '')
|
|
121
|
+
const extra = e?.data && typeof e.data === 'object' ? ['need', 'total', 'reason'].filter((k) => e.data[k] != null).map((k) => `${k}: ${e.data[k]}`).join(', ') : ''
|
|
122
|
+
const text = FRIENDLY[code] || code || 'Unknown error'
|
|
123
|
+
return extra ? `${text} (${extra})` : text
|
|
124
|
+
}
|