yesilcam-studios 0.1.0 → 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.
@@ -0,0 +1,106 @@
1
+ // Kurgu stüdyosu (uzun video 15 sn – 10 dk) ve Influencer otomasyonu.
2
+ import { z, op, ID, NEG, n, clean, quote } from './_util.js'
3
+
4
+ const SECONDS = z.union([15, 30, 60, 120, 180, 300, 600].map((s) => z.literal(s))).describe('Target length in seconds: 15, 30, 60, 120, 180, 300 or 600 (final video within ±5%)')
5
+ // Sunucunun peşin planlama tahmini (ysinfluenceredit::_planEstimate ile aynı): girdi 9k (+11k/bölüm, 90 sn üstü), çıktı 6k + 90/sn, 10 $ / 50 $ milyon
6
+ export const planEstimate = (seconds, mult) => { const ch = Math.max(1, Math.ceil(seconds / 60)); const inp = 9000 + (ch > 1 ? ch * 11000 : 0); const out = 6000 + Math.ceil(seconds * 90); return Math.ceil(((10 * inp + 50 * out) / 1e6) * 291667 * mult) }
7
+ const edit = (c, op, body = {}) => c.call('ys-influencer-edit', { ...body, op })
8
+ const auto = (c, op, body = {}) => c.call('ys-influencer-auto', { ...body, op })
9
+ const subject = (a) => (a.influencer_id ? { influencer_id: Number(a.influencer_id) } : (a.character_id ? { character_id: Number(a.character_id) } : {}))
10
+ const editView = (r) => { const e = r.edit || r; return clean({ id: String(e.id), status: e.status, title: e.title, target: e.target, seconds: e.seconds, format: e.format, tier: e.tier, aspect: e.aspect, chapters: e.chapter_count, shots: e.shot_count,
11
+ fit: e.fit, quote_total: e.quote?.total, frames_total: e.quote?.frames_total, plan_need: e.plan_need, errors: e.errors?.length ? e.errors : undefined, result: e.result, paused: e.run?.paused, need: e.run?.need, eta_at: e.run?.eta_at, progress: e.job?.progress }) }
12
+
13
+ export default [
14
+ op({ name: 'longvideo_list', group: 'longvideo', action: 'list', kind: 'read', summary: 'Long videos (edit studio projects)', description: 'Long video projects (15 s to 10 min) with status, plus your plan, balance and limits.', input: {},
15
+ run: async (c) => { const r = await edit(c, 'list'); return { edits: (r.edits || []).map(editView), plan: r.plan, balance: r.balance, limits: r.limits } } }),
16
+ op({ name: 'longvideo_get', group: 'longvideo', action: 'get', kind: 'read', summary: 'A long video: storyboard, quote, progress',
17
+ description: 'Full long video project: status, target fit (±5%), quote per chapter/shot, production progress and the editable storyboard (full=true includes it).',
18
+ input: { edit_id: ID('Edit id'), full: z.boolean().optional() }, positional: 'edit_id',
19
+ run: async (c, a) => { const r = await edit(c, 'get', { edit_id: Number(a.edit_id) }); return a.full ? r.edit : { ...editView(r), storyboard_chapters: (r.edit?.storyboard?.chapters || []).map((x) => x.title) } } }),
20
+ op({ name: 'longvideo_assist', group: 'longvideo', action: 'script', kind: 'spend', summary: 'Sam writes a script for a long video', description: 'Sam writes a spoken script sized for the target length. Charged by actual usage.',
21
+ input: { text: z.string().min(3).max(6000).describe('Topic or rough notes'), lang: z.string().min(2).max(5), seconds: SECONDS },
22
+ quote: async (c) => { const r = await c.call('ys-influencer-config'); return quote(c, [{ item: 'Sam script (estimate)', tokens: Math.max(100, Number(r.sam_estimate || 0)) }], { balance: n(r.balance), approximate: true, notes: 'Charged up front and settled to actual use.' }) },
23
+ run: (c, a) => edit(c, 'assist', { text: a.text, lang: a.lang, seconds: a.seconds }) }),
24
+ op({ name: 'longvideo_plan', group: 'longvideo', action: 'plan', kind: 'spend', summary: 'Plan a long video: Sam writes chapters, scenes and shots (editable storyboard)',
25
+ description: 'Sam plans a long video (15 s to 10 min) for your influencer as an editable storyboard: chapters, scenes, shots (talk, b-roll, scene, product, screen), continuity file, captions. Over 30 s or premium needs Basic, over 60 s needs Pro. Planning is charged by actual usage; production is a separate confirmed step.',
26
+ input: { influencer_id: ID('Influencer').optional(), character_id: ID('Character').optional(), edit_id: ID('Re-plan an existing project').optional(), topic: z.string().min(3).max(6000), seconds: SECONDS,
27
+ format: z.enum(['talk', 'mixed']).optional(), talk_share: z.number().int().min(0).max(100).optional().describe('Percent of talking shots (mixed)'), tier: z.enum(['economy', 'premium']).optional(), aspect: z.enum(['9:16', '16:9', '1:1']).optional(),
28
+ product_asset_ids: z.array(z.union([z.string(), z.number()])).max(4).optional(), screen_asset_ids: z.array(z.union([z.string(), z.number()])).max(4).optional(),
29
+ music: z.enum(['upbeat', 'lofi', 'cinematic', 'electronic', 'acoustic', 'luxury', 'none']).optional(), subtitles: z.boolean().optional(),
30
+ end_card_title: z.string().max(60).optional(), end_card_sub: z.string().max(80).optional(), notify_mail: z.boolean().optional(), negative_prompt: NEG },
31
+ prepare: (c, a) => { if (!a.edit_id && !a.influencer_id && !a.character_id) throw new Error('give influencer_id or character_id'); return {} },
32
+ quote: async (c, a) => { const m = await c.call('aistudio-operation-cost', { operation_key: 'image_draft' }); return quote(c, [{ item: 'planning (estimate, settled to actual use)', tokens: planEstimate(a.seconds, Number(m.multiplier) || 1) }], { balance: n(m.currentBalance), approximate: true, notes: 'Charged up front and settled to actual use; production is quoted separately after planning.' }) },
33
+ run: async (c, a) => { const r = await edit(c, 'plan', clean({ edit_id: n(a.edit_id), ...subject(a), topic: a.topic, seconds: a.seconds, format: a.format || 'mixed', weight: a.talk_share, tier: a.tier || 'economy', aspect: a.aspect || '9:16',
34
+ products: (a.product_asset_ids || []).map(Number), screens: (a.screen_asset_ids || []).map(Number), music: a.music === 'none' ? '' : (a.music || 'upbeat'), subtitles: a.subtitles !== false, notify_mail: a.notify_mail,
35
+ outro: a.end_card_title ? { on: true, title: a.end_card_title, sub: a.end_card_sub || '' } : undefined, negative: a.negative_prompt })); return { ...editView(r), charged: n(r.charged) } },
36
+ wait: (r) => ({ kind: 'edit', id: r.id }) }),
37
+ op({ name: 'longvideo_save', group: 'longvideo', action: 'save', kind: 'write', summary: 'Save an edited storyboard', description: 'Saves your edits to the storyboard (get it with longvideo_get full=true, change lines/visuals/shots, send it back). Free; shot lengths are refitted to the target.',
38
+ input: { edit_id: ID('Edit id'), storyboard: z.record(z.string(), z.any()) }, run: async (c, a) => editView(await edit(c, 'save', { edit_id: Number(a.edit_id), storyboard: a.storyboard })) }),
39
+ op({ name: 'longvideo_frames', group: 'longvideo', action: 'frames', kind: 'spend', summary: 'Preview frames for the storyboard', description: 'Makes the missing or changed preview frames of the storyboard (failed frames are refunded).',
40
+ input: { edit_id: ID('Edit id') }, positional: 'edit_id',
41
+ quote: async (c, a) => { const r = await edit(c, 'get', { edit_id: Number(a.edit_id) }); return quote(c, [{ item: 'preview frames', tokens: r.edit?.quote?.frames_total || 0 }], { balance: n(r.balance) }) },
42
+ run: async (c, a) => editView(await edit(c, 'frames', { edit_id: Number(a.edit_id) })),
43
+ wait: (r) => ({ kind: 'edit', id: r.id }) }),
44
+ op({ name: 'longvideo_produce', group: 'longvideo', action: 'produce', kind: 'spend', summary: 'Produce the long video',
45
+ description: 'Produces the planned long video. The quote is the total; it is charged chapter by chapter as production advances (music + chapter 1 now). If tokens run out between chapters, production pauses until you top up and resume.',
46
+ input: { edit_id: ID('Edit id') }, positional: 'edit_id',
47
+ prepare: async (c, a) => { const r = await edit(c, 'get', { edit_id: Number(a.edit_id) }); const e = r.edit; if (e?.errors?.length) throw new Error(`storyboard has errors: ${e.errors.map((x) => `${x.id}:${x.code}`).join(', ')}`); return { total: e?.quote?.total, chapters: e?.quote?.chapters, balance: r.balance } },
48
+ quote: (c, a, p) => quote(c, [{ item: 'production total', tokens: p.total }, ...Object.entries(p.chapters || {}).map(([k, v]) => ({ item: ` of which chapter ${k} (${v.seconds}s)`, tokens: 0 }))], { balance: n(p.balance), notes: 'Charged per chapter as production advances.' }),
49
+ run: async (c, a, ctx, p) => editView(await edit(c, 'produce', { edit_id: Number(a.edit_id), confirm_total: p.total })),
50
+ wait: (r) => ({ kind: 'edit', id: r.id }) }),
51
+ op({ name: 'longvideo_resume', group: 'longvideo', action: 'resume', kind: 'spend', summary: 'Resume a long video paused for balance', description: 'Continues a paused production (charges the next chapter).',
52
+ input: { edit_id: ID('Edit id') }, positional: 'edit_id',
53
+ quote: async (c, a) => { const r = await edit(c, 'get', { edit_id: Number(a.edit_id) }); return quote(c, [{ item: 'next chapter', tokens: r.edit?.run?.need || 0 }], { balance: n(r.balance) }) },
54
+ run: async (c, a) => editView(await edit(c, 'resume', { edit_id: Number(a.edit_id) })),
55
+ wait: (r) => ({ kind: 'edit', id: r.id }) }),
56
+ op({ name: 'longvideo_regen_shot', group: 'longvideo', action: 'regen', kind: 'spend', summary: 'Redo one shot of a long video', description: 'Re-makes one shot (optionally with a new frame) and re-assembles the video.',
57
+ input: { edit_id: ID('Edit id'), shot_id: z.string(), new_frame: z.boolean().optional() },
58
+ quote: async (c, a) => { const r = await edit(c, 'get', { edit_id: Number(a.edit_id) }); const s = r.edit?.quote?.shots?.[a.shot_id]; if (!s) throw new Error('shot not found'); return quote(c, [{ item: `shot ${a.shot_id}`, tokens: s.total }], { balance: n(r.balance) }) },
59
+ run: async (c, a) => editView(await edit(c, 'regen', { edit_id: Number(a.edit_id), shot_id: a.shot_id, new_frame: a.new_frame === false ? 0 : 1 })),
60
+ wait: (r) => ({ kind: 'edit', id: r.id }) }),
61
+ op({ name: 'longvideo_control', group: 'longvideo', action: 'control', kind: 'write', destructive: true, summary: 'Cancel, re-assemble or delete a long video',
62
+ description: 'cancel stops production (unstarted shots are refunded; finished chapters kept), assemble rebuilds the final video from ready shots, delete removes the project.',
63
+ input: { edit_id: ID('Edit id'), action: z.enum(['cancel', 'assemble', 'delete']) }, positional: 'edit_id',
64
+ run: async (c, a) => { const r = await edit(c, a.action, { edit_id: Number(a.edit_id) }); return a.action === 'delete' ? r : editView(r) } }),
65
+
66
+ // ── Otomasyon
67
+ op({ name: 'automation_estimate', group: 'automation', action: 'estimate', kind: 'read', summary: 'Estimate the monthly cost of an automation',
68
+ description: 'Estimates tokens per run and per month for an automation configuration and schedule.',
69
+ input: { sector: z.string().min(2).max(120), seconds: SECONDS.optional(), tier: z.enum(['economy', 'premium']).optional(), days: z.array(z.number().int().min(1).max(7)).optional(), times: z.array(z.string().regex(/^\d{2}:\d{2}$/)).max(3).optional(), influencer_id: ID('Influencer').optional() },
70
+ run: (c, a) => auto(c, 'estimate', clean({ config: clean({ sector: a.sector, seconds: a.seconds, tier: a.tier }), schedule: clean({ days: a.days || [1, 3, 5], times: a.times || ['10:00'] }), influencer_id: n(a.influencer_id) })) }),
71
+ op({ name: 'automation_save', group: 'automation', action: 'save', kind: 'spend', confirm: true, summary: 'Create or update an influencer automation (scheduled videos)',
72
+ description: 'Creates (or updates) an automation that makes videos on a schedule for your sector, with web research and verified sources: approval first (review) or automatic, a monthly token cap (100,000 to 100,000,000) that it never exceeds. Needs Basic (Pro for videos over 60 s). Saving authorizes spending up to the monthly cap, so it needs confirmation. Auto top-up and social publishing are set in the web app.',
73
+ input: { automation_id: ID('Existing automation (update)').optional(), influencer_id: ID('Influencer (new automation)').optional(), character_id: ID('Character').optional(), name: z.string().max(80).optional(),
74
+ sector: z.string().min(2).max(120), firm: z.string().max(200).optional(), product: z.string().max(200).optional(), audience: z.string().max(200).optional(), cta: z.string().max(200).optional(), link: z.string().max(200).optional(), guideline: z.string().max(1500).optional(),
75
+ seconds: SECONDS.optional(), format: z.enum(['talk', 'mixed']).optional(), tier: z.enum(['economy', 'premium']).optional(), aspect: z.enum(['9:16', '16:9', '1:1']).optional(),
76
+ template: z.enum(['auto', 'explainer', 'tips', 'story', 'myth', 'news', 'review', 'howto', 'qa']).optional(), approval: z.enum(['review', 'auto']).optional(), deliver_mail: z.boolean().optional(),
77
+ days: z.array(z.number().int().min(1).max(7)).min(1).describe('ISO weekdays, 1 = Monday'), times: z.array(z.string().regex(/^\d{2}:\d{2}$/)).min(1).max(3), timezone: z.string().optional().describe('e.g. Europe/Istanbul'),
78
+ monthly_cap: z.number().int().min(100000).max(100000000), negative_prompt: z.string().max(300).optional() },
79
+ prepare: (c, a) => ({ config: clean({ sector: a.sector, brand: clean({ firm: a.firm, product: a.product, audience: a.audience, cta: a.cta, link: a.link }), guideline: a.guideline, seconds: a.seconds, format: a.format, tier: a.tier, aspect: a.aspect,
80
+ template_mode: a.template && a.template !== 'auto' ? 'fixed' : 'auto', template: a.template && a.template !== 'auto' ? a.template : undefined, approval: a.approval || 'review', deliver_mail: a.deliver_mail, negative: a.negative_prompt }),
81
+ schedule: clean({ days: a.days, times: a.times, tz: a.timezone }) }),
82
+ quote: async (c, a, p) => { const e = await auto(c, 'estimate', clean({ config: p.config, schedule: p.schedule, influencer_id: n(a.influencer_id) }))
83
+ return quote(c, [{ item: `authorizes up to ${a.monthly_cap.toLocaleString('en-US')} tokens per month (estimate ${Number(e.per_month || 0).toLocaleString('en-US')}/month, ${Number(e.per_run || 0).toLocaleString('en-US')}/run, ${e.runs_per_month} runs)`, tokens: 0 }], { notes: `Nothing is charged now. Runs charge as they happen and never exceed the monthly cap.${e.plan_need ? ` Needs plan: ${e.plan_need}.` : ''}` }) },
84
+ run: (c, a, ctx, p) => auto(c, 'save', clean({ automation_id: n(a.automation_id), ...subject(a), name: a.name, config: p.config, schedule: p.schedule, budget: { monthly_cap: a.monthly_cap } })) }),
85
+ op({ name: 'automation_list', group: 'automation', action: 'list', kind: 'read', summary: 'Your automations', description: 'Automations with status, holds, next runs and this month\'s spending.', input: {},
86
+ run: async (c) => { const r = await auto(c, 'list'); return { automations: (r.automations || []).map((x) => clean({ id: String(x.id), name: x.name, status: x.status, hold: x.hold || undefined, next_run_at: x.next_run_at, month: x.month, counts: x.counts })), plan: r.meta?.plan, balance: r.meta?.balance } } }),
87
+ op({ name: 'automation_get', group: 'automation', action: 'get', kind: 'read', summary: 'An automation with its runs', description: 'Automation settings, schedule, budget, spending and the last 40 runs.',
88
+ input: { automation_id: ID('Automation id') }, positional: 'automation_id',
89
+ run: async (c, a) => { const r = await auto(c, 'get', { automation_id: Number(a.automation_id) }); return { automation: r.automation, runs: (r.runs || []).map((x) => clean({ id: String(x.id), status: x.status, wait: x.wait || undefined, title: x.title, topic: x.topic, quote_total: x.quote_total, slot_at: x.slot_at, result: x.result?.url, review_expires_at: x.review_expires_at })) } } }),
90
+ op({ name: 'automation_control', group: 'automation', action: 'control', kind: 'write', destructive: true, summary: 'Pause, resume, delete or retry an automation',
91
+ description: 'pause stops new runs, resume restarts the schedule, delete removes it (running productions finish), retry clears a hold now.',
92
+ input: { automation_id: ID('Automation id'), action: z.enum(['pause', 'resume', 'delete', 'retry']) }, positional: 'automation_id',
93
+ run: (c, a) => auto(c, a.action, { automation_id: Number(a.automation_id) }) }),
94
+ op({ name: 'automation_run_now', group: 'automation', action: 'run-now', kind: 'spend', summary: 'Start an automation run now', description: 'Starts a run immediately (research and planning are charged; production waits for approval if approval=review).',
95
+ input: { automation_id: ID('Automation id') }, positional: 'automation_id',
96
+ quote: async (c, a) => { const r = await auto(c, 'get', { automation_id: Number(a.automation_id) }); return quote(c, [{ item: 'one run (estimate)', tokens: r.automation?.month?.est_per_run || 0 }], { balance: n(r.meta?.balance), approximate: true }) },
97
+ run: (c, a) => auto(c, 'run_now', { automation_id: Number(a.automation_id) }) }),
98
+ op({ zeroRunsFree: true, name: 'automation_review', group: 'automation', action: 'review', kind: 'spend', summary: 'Approve or skip a run waiting for review',
99
+ description: 'approve produces the planned video of a run in review (charges production); skip drops it. Runs not reviewed within 7 days are skipped.',
100
+ input: { run_id: ID('Run id'), action: z.enum(['approve', 'skip']) },
101
+ quote: async (c, a) => { if (a.action === 'skip') return quote(c, [{ item: 'skip', tokens: 0 }]); const r = await auto(c, 'run', { run_id: Number(a.run_id) }); return quote(c, [{ item: 'production', tokens: r.run?.quote_total || 0 }], { balance: n(r.meta?.balance) }) },
102
+ run: (c, a) => auto(c, a.action, { run_id: Number(a.run_id) }),
103
+ wait: (r, a) => (a.action === 'approve' ? { kind: 'auto_run', id: a.run_id } : null) }),
104
+ op({ name: 'automation_run', group: 'automation', action: 'run', kind: 'read', summary: 'One automation run: research, storyboard, result', description: 'A run with its research summary and sources, title, status, quote and result video.',
105
+ input: { run_id: ID('Run id') }, positional: 'run_id', run: async (c, a) => { const r = await auto(c, 'run', { run_id: Number(a.run_id) }); return { run: r.run, edit: r.edit ? { id: r.edit.id, status: r.edit.status, quote_total: r.edit.quote?.total } : undefined } } }),
106
+ ]
@@ -0,0 +1,65 @@
1
+ // Maket: kat planı → yürünebilir 3B model. Yerel analiz (ücretsiz), projeler/planlar, fotoğraftan plan, foto-gerçekçi render.
2
+ // Döşeme/peyzaj tasarımı 3B motorun duvar analizine dayanır → yalnız uygulamada (CLI/MCP kapsamı dışı).
3
+ import { readFileSync, writeFileSync } from 'node:fs'
4
+ import { basename, extname } from 'node:path'
5
+ import { z, op, ID, NEG, n, clean, quote } from './_util.js'
6
+ import { analyzePlanText } from '../maket/analyze.js'
7
+ import { readLocal } from '../files.js'
8
+ import { SITE_URL } from '../constants.js'
9
+
10
+ const LANG6 = z.enum(['tr', 'en', 'de', 'fr', 'es', 'it'])
11
+ const day = (t) => (t ? new Date(Number(t) * 1000).toISOString().slice(0, 10) : undefined)
12
+
13
+ export default [
14
+ op({ name: 'maket_analyze', group: 'maket', action: 'analyze', kind: 'local', summary: 'Analyze a DXF or JSON floor plan locally (free)',
15
+ description: 'Analyzes a local DXF or JSON floor plan on this computer: walls, doors, windows, rooms, areas and layer roles. Free, nothing is uploaded.',
16
+ input: { path: z.string().min(1) }, positional: 'path',
17
+ run: (c, a) => analyzePlanText(readFileSync(a.path, 'utf8'), basename(a.path)) }),
18
+ op({ name: 'maket_projects', group: 'maket', action: 'projects', kind: 'read', summary: 'Maket projects (folders of floor plans)', description: 'Maket projects with plan counts.', input: {},
19
+ run: async (c) => { const r = await c.call('maket-list-projects'); return { projects: (r.items || []).map((p) => clean({ id: String(p.id), title: p.title, plans: n(p.plan_count), updated: day(p.updated_date_time) })) } } }),
20
+ op({ name: 'maket_create_project', group: 'maket', action: 'create', kind: 'write', summary: 'Create a Maket project', description: 'Creates a Maket project (a folder of floor plans, one per floor).',
21
+ input: { title: z.string().min(1).max(200), description: z.string().max(2000).optional() }, positional: 'title',
22
+ run: (c, a) => c.call('maket-create-project', { title: a.title, description: a.description || '' }) }),
23
+ op({ name: 'maket_plans', group: 'maket', action: 'plans', kind: 'read', summary: 'Floor plans in a Maket project', description: 'Floor plans of a project (floor index, source type).',
24
+ input: { project_id: ID('Project id') }, positional: 'project_id',
25
+ run: async (c, a) => { const r = await c.call('maket-plan-list', { projectId: Number(a.project_id) }); return { plans: (r.items || []).map((p) => clean({ id: String(p.id), title: p.title, floor: n(p.floor_index), source: p.source_kind, updated: day(p.updated_date_time), url: `${SITE_URL}/app/maket/${a.project_id}/${p.id}` })) } } }),
26
+ op({ name: 'maket_save', group: 'maket', action: 'save', kind: 'write', summary: 'Add a DXF/JSON floor plan to a project',
27
+ description: 'Uploads a local DXF or JSON floor plan into a Maket project (give project_id, or new_project to create one) and returns the 3D viewer link. Drawings are free to convert.',
28
+ input: { path: z.string().min(1), project_id: ID('Existing project').optional(), new_project: z.string().optional(), title: z.string().optional(), floor: z.number().int().optional().describe('0 = ground floor') },
29
+ positional: 'path',
30
+ run: async (c, a) => {
31
+ if (!a.project_id && !a.new_project) throw new Error('project_id or new_project is required: plans live inside a project')
32
+ const t = readFileSync(a.path, 'utf8'); const an = analyzePlanText(t, basename(a.path))
33
+ const pid = a.project_id ? Number(a.project_id) : Number((await c.call('maket-create-project', { title: a.new_project, description: '' })).id)
34
+ const r = await c.call('maket-plan-create', { projectId: pid, title: a.title || basename(a.path, extname(a.path)), floor_index: a.floor, source_kind: an.source_kind, source_text: t, settings: { overrides: {}, wallHeight: 2.9 }, stats: an.stats })
35
+ return { ...r, project_id: String(pid), url: `${SITE_URL}/app/maket/${pid}/${r.id}` }
36
+ } }),
37
+ op({ name: 'maket_get', group: 'maket', action: 'get', kind: 'read', summary: 'Download the source file of a floor plan', description: 'Saves the DXF/JSON source of a plan to a local file.',
38
+ input: { plan_id: ID('Plan id'), save_to: z.string().optional() }, positional: 'plan_id',
39
+ run: async (c, a) => { const r = await c.call('maket-plan-get', { planId: Number(a.plan_id) }); const p = r.plan; const file = a.save_to || `${p.title || 'plan'}.${p.source_kind === 'json' ? 'json' : 'dxf'}`; writeFileSync(file, p.source_text); return { id: String(p.id), title: p.title, saved: file } } }),
40
+ op({ name: 'maket_delete', group: 'maket', action: 'delete', kind: 'write', destructive: true, summary: 'Delete a Maket plan or a whole project',
41
+ description: 'Deletes one floor plan (plan_id) or a project with all its plans (project_id).', input: { plan_id: ID('Plan id').optional(), project_id: ID('Project id').optional() },
42
+ run: (c, a) => { if (a.plan_id) return c.call('maket-plan-delete', { planId: Number(a.plan_id) }); if (a.project_id) return c.call('maket-delete-project', { projectId: Number(a.project_id) }); throw new Error('give plan_id or project_id') } }),
43
+ op({ name: 'maket_image_to_plan', group: 'maket', action: 'from-image', kind: 'spend', summary: 'Floor plan from a photo or scan',
44
+ description: 'Recognises walls, doors, windows and rooms in a photo or scan of a floor plan (PNG, JPG, WebP) and saves it as a plan in a project. Charged by actual usage (typically 10,000-30,000 tokens; the estimate is charged first and the excess refunded; refunded if no plan is recognised). The same image within 10 minutes is free.',
45
+ input: { path: z.string().min(1), project_id: ID('Project').optional(), new_project: z.string().optional(), title: z.string().optional(), floor: z.number().int().optional(), lang: LANG6.optional(), hint: z.string().max(300).optional().describe('e.g. "the long wall is 12 m"') },
46
+ positional: 'path',
47
+ quote: (c) => quote(c, [{ item: 'plan recognition (typical 10,000-30,000)', tokens: 30000 }], { approximate: true, notes: 'Upper typical estimate. Charged by actual usage; excess refunded; refunded if no plan is recognised.' }),
48
+ run: async (c, a) => {
49
+ if (!a.project_id && !a.new_project) throw new Error('project_id or new_project is required')
50
+ const f = readLocal(a.path); if (!/^image\/(jpeg|png|webp)$/.test(f.mime)) throw new Error('use a PNG, JPG or WebP image')
51
+ const r = await c.call('maket-image-to-plan', clean({ image_base64: `data:${f.mime};base64,${f.buf.toString('base64')}`, mime: f.mime, lang: a.lang || 'en', hint: a.hint }))
52
+ const pid = a.project_id ? Number(a.project_id) : Number((await c.call('maket-create-project', { title: a.new_project, description: '' })).id)
53
+ const text = JSON.stringify(r.plan)
54
+ const an = analyzePlanText(text, `${basename(a.path)}.json`)
55
+ const p = await c.call('maket-plan-create', { projectId: pid, title: a.title || basename(a.path, extname(a.path)), floor_index: a.floor, source_kind: 'json', source_text: text, settings: { overrides: {}, wallHeight: 2.9 }, stats: an.stats })
56
+ return { plan_id: String(p.id), project_id: String(pid), charged: n(r.charged), rooms: an.stats?.roomCount, url: `${SITE_URL}/app/maket/${pid}/${p.id}` }
57
+ } }),
58
+ op({ name: 'maket_render', group: 'maket', action: 'render', kind: 'spend', summary: 'Photoreal render of a 3D view (day or dusk)',
59
+ description: 'Turns a screenshot of a Maket 3D view (PNG/JPG/WebP) into a photoreal render, by day or at dusk. Charged by actual usage after an estimate.',
60
+ input: { path: z.string().min(1).describe('Screenshot of the 3D view'), light: z.enum(['day', 'dusk']).optional(), style: z.string().max(60).optional(), plan_id: ID('Plan id').optional(), lang: LANG6.optional(), negative_prompt: NEG },
61
+ positional: 'path',
62
+ quote: async (c) => { const r = await c.call('maket-quote', {}); return quote(c, [{ item: 'render (estimate)', tokens: r.render }], { approximate: true }) },
63
+ run: (c, a) => { const f = readLocal(a.path); return c.call('maket-render-start', clean({ image_base64: `data:${f.mime};base64,${f.buf.toString('base64')}`, light: a.light || 'day', style: a.style, planId: a.plan_id ? String(a.plan_id) : undefined, lang: a.lang || 'en', negative_prompt: a.negative_prompt })) },
64
+ wait: (r) => ({ kind: 'maket_render', id: r.jobId }) }),
65
+ ]
@@ -0,0 +1,26 @@
1
+ // Sosyal medyada yayın (Instagram / TikTok). Sunucu bayrağı kapalıyken her işlem "yakında" döner.
2
+ import { z, op, ID, clean } from './_util.js'
3
+
4
+ const SOON = { available: false, message: 'Publishing to Instagram and TikTok is coming soon. Connect accounts in the web app when it opens: https://yesilcamstudios.com/app/account/social' }
5
+ async function social(c, body) {
6
+ try { return await c.call('ys-social', body) }
7
+ catch (e) { if (e.code === 'social_unavailable' || /invalid method/i.test(e.message)) return { ...SOON }; throw e }
8
+ }
9
+
10
+ export default [
11
+ op({ name: 'social_status', group: 'social', action: 'status', kind: 'read', summary: 'Social publishing: availability and connected accounts',
12
+ description: 'Shows whether publishing to Instagram / TikTok is open for this account, platform availability and connected accounts. Accounts are connected in the web app only.', input: {},
13
+ run: (c) => social(c, { op: 'config' }) }),
14
+ op({ name: 'social_publish', group: 'social', action: 'publish', kind: 'spend', confirm: true, summary: 'Publish or schedule a video to connected Instagram / TikTok accounts',
15
+ description: 'Publishes a library video (asset_id) to up to 6 connected accounts now or at a time (unix seconds, up to 60 days ahead), with caption and hashtags per account. Posting is public, so it needs confirmation. Returns "coming soon" while publishing is not open.',
16
+ input: { asset_id: ID('Library video asset id'), account_ids: z.array(z.union([z.string(), z.number()])).min(1).max(6), caption: z.string().max(2200), hashtags: z.array(z.string()).max(30).optional(),
17
+ when: z.union([z.literal('now'), z.number().int()]).optional(), fit: z.enum(['original', 'vertical']).optional(), tiktok_privacy: z.enum(['PUBLIC_TO_EVERYONE', 'MUTUAL_FOLLOW_FRIENDS', 'FOLLOWER_OF_CREATOR', 'SELF_ONLY']).optional() },
18
+ quote: async (c, a) => { const s = await social(c, { op: 'config' }); if (s.available === false) return { total: 0, items: [], notes: s.message, unavailable: true }; return { total: 0, items: [{ item: `public post to ${a.account_ids.length} account(s)`, tokens: 0 }], notes: 'Posting is free but public.' } },
19
+ run: async (c, a) => { const s = await social(c, { op: 'config' }); if (s.available === false) return s
20
+ const accounts = s.accounts || []
21
+ const targets = a.account_ids.map((id) => { const acc = accounts.find((x) => String(x.id) === String(id)); return clean({ account_id: Number(id), text: a.caption, hashtags: a.hashtags, options: acc?.platform === 'tiktok' ? { privacy: a.tiktok_privacy || 'SELF_ONLY', mode: 'inbox' } : { share_to_feed: true, ai_note: true } }) })
22
+ return social(c, clean({ op: 'publish', asset_id: Number(a.asset_id), source: 'library', when: a.when || 'now', fit: a.fit || 'original', targets })) },
23
+ wait: (r) => (r.posts?.[0]?.id ? { kind: 'social_post', id: r.posts[0].id } : null) }),
24
+ op({ name: 'social_posts', group: 'social', action: 'posts', kind: 'read', summary: 'Your scheduled and published posts', description: 'Social posts with status (scheduled, publishing, published, failed).',
25
+ input: { limit: z.number().int().min(1).max(50).optional() }, run: (c, a) => social(c, { op: 'posts', limit: a.limit || 20 }) }),
26
+ ]
@@ -0,0 +1,92 @@
1
+ // Website Studio (Montaj/Söküm videosundan kaydırmalı site), Website Promo (URL → tanıtım videosu), Montaj / Söküm (assemble).
2
+ import { z, op, ID, NEG, n, clean, quote } from './_util.js'
3
+ import { uploadAsset } from '../files.js'
4
+
5
+ const LANG6 = z.enum(['tr', 'en', 'de', 'fr', 'es', 'it'])
6
+ const ASM_FLOW = z.enum(['disassemble', 'assemble', 'disassemble_assemble', 'assemble_disassemble'])
7
+ const asmOptions = (a) => clean({ flow: a.flow, camera: a.camera, tier: a.tier, aspect: a.aspect, duration: a.duration, source: a.photo_asset_id || a.photo_path ? 'photo' : 'text', photo_mode: a.as_is ? 'asis' : undefined,
8
+ description: a.description, notes: a.notes, photo_asset_id: n(a.photo_asset_id), negative_prompt: a.negative_prompt })
9
+
10
+ const promoOptions = (a) => clean({ lang: a.lang, seconds: a.seconds, aspect: a.aspect, narration: a.narration, voice: a.voice, music: a.music_style === 'none' ? false : undefined, music_style: a.music_style, tier: a.tier, prompt: a.brief, focus_urls: a.focus_urls, negative_prompt: a.negative_prompt })
11
+
12
+ export default [
13
+ // ── Montaj / Söküm
14
+ op({ name: 'assemble_create', group: 'assemble', action: 'create', kind: 'spend', summary: 'Assemble / Disassemble: exploded-view product video',
15
+ description: 'Makes a product video that takes the object apart and/or puts it back together, from a text description or a product photo (as_is keeps your photo as the first frame unchanged; short edge at least 700 px). Camera static, orbit180 or orbit360; tier standard 720p, pro 1080p, 4k (Basic/Pro). Round trips only charge half (the reverse half is free).',
16
+ input: { description: z.string().max(600).optional().describe('Object description (text source)'), photo_asset_id: ID('Product photo asset id').optional(), photo_path: z.string().optional().describe('Local product photo'),
17
+ as_is: z.boolean().optional().describe('Use the photo exactly as it is'), flow: ASM_FLOW.optional(), camera: z.enum(['static', 'orbit180', 'orbit360']).optional(), tier: z.enum(['standard', 'pro', '4k']).optional(),
18
+ aspect: z.enum(['16:9', '9:16', '1:1']).optional(), duration: z.union([z.literal(5), z.literal(10), z.literal(20), z.literal(30), z.literal(40), z.literal(60)]).optional(), notes: z.string().max(400).optional(), negative_prompt: NEG },
19
+ prepare: async (c, a) => { if (!a.description && !a.photo_asset_id && !a.photo_path) throw new Error('give a description or a product photo'); return {} },
20
+ quote: async (c, a) => { const r = await c.call('aistudio-assemble-quote', { options: asmOptions({ ...a, photo_asset_id: a.photo_asset_id || (a.photo_path ? 1 : undefined) }) }); const p = r.price || {}
21
+ if (a.duration && r.durations && !r.durations.includes(Number(a.duration))) throw new Error(`duration must be one of ${r.durations.join(', ')} for this flow and camera`)
22
+ return quote(c, [{ item: `video ${p.generated_seconds}s`, tokens: p.video }, { item: 'keyframes', tokens: p.keyframes }, { item: 'director', tokens: p.director }], { balance: n(r.balance) }) },
23
+ run: async (c, a) => { const photo = a.photo_asset_id || (a.photo_path ? (await uploadAsset(c, a.photo_path, { kind: 'image' })).asset_id : undefined); return c.call('aistudio-assemble-create', { options: asmOptions({ ...a, photo_asset_id: photo }) }) },
24
+ wait: (r) => ({ kind: 'assemble', id: r.project_id }) }),
25
+ op({ name: 'assemble_list', group: 'assemble', action: 'list', kind: 'read', summary: 'Your Assemble / Disassemble projects', description: 'Assemble / Disassemble projects.',
26
+ input: { limit: z.number().int().min(1).max(50).optional() }, run: (c, a) => c.call('aistudio-assemble-list', { limit: a.limit || 12 }) }),
27
+ op({ name: 'assemble_get', group: 'assemble', action: 'get', kind: 'read', summary: 'An Assemble / Disassemble project', description: 'Status, result video, keyframes and versions.',
28
+ input: { project_id: ID('Project id') }, positional: 'project_id', run: (c, a) => c.call('aistudio-assemble-get', { project_id: Number(a.project_id) }) }),
29
+ op({ zeroRunsFree: true, name: 'assemble_regenerate', group: 'assemble', action: 'regenerate', kind: 'spend', summary: 'Re-render the video of an Assemble project with the same keyframes',
30
+ description: 'Re-renders only the video with the same keyframes. The first re-render of films up to 8 s is free.', input: { project_id: ID('Project id') }, positional: 'project_id',
31
+ quote: async (c, a) => { const r = await c.call('aistudio-assemble-get', { project_id: Number(a.project_id) }); const p = r.project || {}; return quote(c, [{ item: p.regen_available ? 'first re-render (free)' : 're-render', tokens: p.regen_available ? 0 : p.regen_cost }]) },
32
+ run: (c, a) => c.call('aistudio-assemble-regenerate', { project_id: Number(a.project_id) }),
33
+ wait: (r, a) => ({ kind: 'assemble', id: a.project_id }) }),
34
+ op({ name: 'assemble_delete', group: 'assemble', action: 'delete', kind: 'write', destructive: true, summary: 'Delete an Assemble project', description: 'Deletes an Assemble / Disassemble project.',
35
+ input: { project_id: ID('Project id') }, positional: 'project_id', run: (c, a) => c.call('aistudio-assemble-delete', { project_id: Number(a.project_id) }) }),
36
+ op({ name: 'photo_enhance', group: 'assemble', action: 'enhance', kind: 'spend', summary: 'Enhance a product photo (studio or lifestyle look)',
37
+ description: 'Cleans up a product photo for use as an Assemble source or a design image: studio, lifestyle, or keep (same scene, better light).',
38
+ input: { photo_asset_id: ID('Photo asset id').optional(), photo_path: z.string().optional(), style: z.enum(['studio', 'lifestyle', 'keep']).optional(), aspect: z.enum(['16:9', '9:16', '1:1']).optional(), negative_prompt: NEG },
39
+ quote: async (c) => { const r = await c.call('aistudio-assemble-enhance-quote'); return quote(c, [{ item: 'assemble_enhance', tokens: r.price?.total ?? r.price }], { balance: n(r.balance) }) },
40
+ run: async (c, a) => { const id = a.photo_asset_id || (await uploadAsset(c, a.photo_path, { kind: 'image' })).asset_id; return c.call('aistudio-assemble-enhance', clean({ photo_asset_id: Number(id), style: a.style || 'studio', aspect: a.aspect, negative_prompt: a.negative_prompt })) },
41
+ wait: (r) => ({ kind: 'enhance', id: r.job_id }) }),
42
+
43
+ // ── Website Studio
44
+ op({ name: 'website_create', group: 'website', action: 'create', kind: 'spend', summary: 'Website Studio: a scrolling product site from an Assemble / Disassemble video',
45
+ description: 'Builds a scroll-animated landing page from a ready Assemble / Disassemble project: texts written for your brand (optionally read from your current site), gallery from video frames or generated images, contact and social links, dark/light theme. Hosted privately (preview link) or publicly, or downloaded as a ZIP. Text edits later are free.',
46
+ input: { source_project_id: ID('Ready Assemble / Disassemble project id'), lang: LANG6.optional(), brand: z.string().max(60).optional(), product_name: z.string().max(80).optional(), headline: z.string().max(90).optional(),
47
+ texts: z.string().max(1500).optional().describe('Facts about the product'), site_url: z.string().url().optional(), read_site: z.boolean().optional().describe('Read brand, colours and text from site_url'),
48
+ email: z.string().optional(), phone: z.string().optional(), address: z.string().max(200).optional(), mode: z.enum(['dark', 'light', 'auto']).optional(), accent: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
49
+ image_mode: z.enum(['frames', 'generate']).optional(), image_count: z.number().int().min(1).max(4).optional(), image_asset_ids: z.array(z.union([z.string(), z.number()])).max(8).optional(), negative_prompt: NEG },
50
+ quote: async (c, a) => { const r = await c.call('ys-website-quote', clean({ image_mode: a.image_mode || 'frames', image_count: a.image_count, images: (a.image_asset_ids || []).map(Number) })); const p = r.price || {}
51
+ return quote(c, [{ item: 'build', tokens: p.build }, { item: 'writer', tokens: p.writer }, p.images && { item: `images x${p.image_count}`, tokens: p.images }], { balance: n(r.balance) }) },
52
+ run: (c, a) => c.call('ys-website-create', clean({ source_project_id: Number(a.source_project_id), lang: a.lang || 'en', crawl: a.read_site ? true : undefined, images: (a.image_asset_ids || []).map(Number), image_mode: a.image_mode || 'frames', image_count: a.image_count,
53
+ user: clean({ brand: a.brand, product_name: a.product_name, headline: a.headline, texts: a.texts, site_url: a.site_url, email: a.email, phone: a.phone, address: a.address, mode: a.mode, accent: a.accent }), negative_prompt: a.negative_prompt })),
54
+ wait: (r) => ({ kind: 'website', id: r.site_id }) }),
55
+ op({ name: 'website_list', group: 'website', action: 'list', kind: 'read', summary: 'Your Website Studio sites', description: 'Sites with status, visibility and links.', input: {},
56
+ run: (c) => c.call('ys-website-list', { limit: 60 }) }),
57
+ op({ name: 'website_get', group: 'website', action: 'get', kind: 'read', summary: 'A Website Studio site with its content', description: 'Site status, links (public, ZIP) and editable content.',
58
+ input: { site_id: ID('Site id') }, positional: 'site_id', run: (c, a) => c.call('ys-website-get', { site_id: Number(a.site_id) }) }),
59
+ op({ name: 'website_update', group: 'website', action: 'update', kind: 'write', summary: 'Edit the texts and theme of a site (free repack)',
60
+ description: 'Replaces the site content (hero, intro, features, story, FAQ, contact, SEO, theme with dark/light, accent and ink backdrop). Get the current content with website_get, change it, send it back. Free.',
61
+ input: { site_id: ID('Site id'), content: z.record(z.string(), z.any()).describe('Full content object (from website_get)') },
62
+ run: (c, a) => c.call('ys-website-update', { site_id: Number(a.site_id), content: a.content }),
63
+ wait: (r, a) => ({ kind: 'website', id: a.site_id }) }),
64
+ op({ name: 'website_manage', group: 'website', action: 'manage', kind: 'write', destructive: true, summary: 'Publish, unpublish, preview link or delete a site',
65
+ description: 'action=public publishes the site, private hides it, preview_link returns a 2-hour private preview link, delete removes the site.',
66
+ input: { site_id: ID('Site id'), action: z.enum(['public', 'private', 'preview_link', 'delete']) }, positional: 'site_id',
67
+ run: (c, a) => a.action === 'preview_link' ? c.call('ys-website-preview-link', { site_id: Number(a.site_id) }) : a.action === 'delete' ? c.call('ys-website-delete', { site_id: Number(a.site_id) }) : c.call('ys-website-visibility', { site_id: Number(a.site_id), visibility: a.action }) }),
68
+
69
+ // ── Website Promo
70
+ op({ name: 'promo_create', group: 'promo', action: 'create', kind: 'spend', summary: 'Website Promo: a narrated promo video of a website (step 1: script)',
71
+ description: 'Reads a website and writes a promo film script with screen-capture scenes (and generated b-roll on the cinema tier, Basic/Pro). This step charges the script; the script can be reviewed and edited, then produced with yesilcam_promo_produce (second confirmation).',
72
+ positional: 'url',
73
+ input: { url: z.string().url(), brief: z.string().max(3000).optional(), lang: z.enum(['tr', 'en', 'de', 'fr', 'es', 'it', 'pt', 'nl', 'pl', 'ru', 'ar', 'ja']).optional(), seconds: z.union([z.literal(15), z.literal(30), z.literal(45), z.literal(60), z.literal(90)]).optional(),
74
+ aspect: z.enum(['16:9', '9:16', '1:1', '4:5', '21:9']).optional(), narration: z.boolean().optional(), voice: z.enum(['warm_female', 'warm_male', 'energetic_female', 'energetic_male', 'calm_narrator', 'deep_trailer']).optional(),
75
+ music_style: z.enum(['uplifting_corporate', 'cinematic_epic', 'lofi_chill', 'electronic_pulse', 'acoustic_warm', 'luxury_minimal', 'none']).optional(), tier: z.enum(['standard', 'cinema']).optional(), focus_urls: z.array(z.string().url()).max(4).optional(), negative_prompt: NEG },
76
+ quote: async (c, a) => { const r = await c.call('sitefilm-quote', { options: promoOptions(a) }); return quote(c, [{ item: 'script', tokens: r.script_tokens }], { balance: n(r.balance), notes: `Production later (separate confirmation): ${Number(r.produce_tokens || 0).toLocaleString('en-US')} tokens.` }) },
77
+ run: (c, a) => c.call('sitefilm-create', { url: a.url, options: promoOptions(a) }),
78
+ wait: (r) => ({ kind: 'promo', id: r.project_id }) }),
79
+ op({ name: 'promo_list', group: 'promo', action: 'list', kind: 'read', summary: 'Your Website Promo videos', description: 'Website Promo projects.', input: {}, run: (c) => c.call('sitefilm-list', { limit: 50 }) }),
80
+ op({ name: 'promo_get', group: 'promo', action: 'get', kind: 'read', summary: 'A Website Promo project (script, status, video)', description: 'Status, script, versions and result video of a Website Promo project.',
81
+ input: { project_id: ID('Project id') }, positional: 'project_id', run: async (c, a) => { const r = await c.call('sitefilm-get', { project_id: Number(a.project_id) }); const p = r.project || r; if (p?.crawl) p.crawl = { brand: p.crawl.brand, pages: (p.crawl.pages || []).map((x) => x.url) }; return p } }),
82
+ op({ name: 'promo_script_save', group: 'promo', action: 'script', kind: 'write', summary: 'Save an edited Website Promo script', description: 'Saves an edited script (from promo_get) while the project is in review. Free.',
83
+ input: { project_id: ID('Project id'), script: z.record(z.string(), z.any()) }, run: (c, a) => c.call('sitefilm-save-script', { project_id: Number(a.project_id), script: a.script }) }),
84
+ op({ name: 'promo_produce', group: 'promo', action: 'produce', kind: 'spend', summary: 'Produce the Website Promo video (step 2)',
85
+ description: 'Records, narrates, scores and edits the reviewed script into the final promo video.', input: { project_id: ID('Project id') }, positional: 'project_id',
86
+ quote: async (c, a) => { const g = await c.call('sitefilm-get', { project_id: Number(a.project_id) }); const p = g.project || g; const r = await c.call('sitefilm-quote', { options: p.options || {} }); const broll = (p.script?.scenes || []).filter((s) => s.kind === 'broll').length
87
+ return quote(c, [{ item: 'production', tokens: r.produce_base_tokens ?? r.produce_tokens }, broll && { item: `b-roll x${broll}`, tokens: broll * Number(r.broll_tokens || 0) }], { balance: n(r.balance) }) },
88
+ run: (c, a) => c.call('sitefilm-produce', { project_id: Number(a.project_id) }),
89
+ wait: (r, a) => ({ kind: 'promo', id: a.project_id }) }),
90
+ op({ name: 'promo_delete', group: 'promo', action: 'delete', kind: 'write', destructive: true, summary: 'Delete a Website Promo project', description: 'Deletes a Website Promo project.',
91
+ input: { project_id: ID('Project id') }, positional: 'project_id', run: (c, a) => c.call('sitefilm-delete', { project_id: Number(a.project_id) }) }),
92
+ ]
@@ -0,0 +1,38 @@
1
+ // Tüm işlemler tek yerde: MCP araçları ve CLI komutları buradan türetilir (birebir eşlik).
2
+ import account from './ops/account.js'
3
+ import library from './ops/library.js'
4
+ import film from './ops/film.js'
5
+ import animation from './ops/animation.js'
6
+ import cinema from './ops/cinema.js'
7
+ import design from './ops/design.js'
8
+ import website from './ops/website.js'
9
+ import fashion from './ops/fashion.js'
10
+ import maket from './ops/maket.js'
11
+ import influencer from './ops/influencer.js'
12
+ import longvideo from './ops/longvideo.js'
13
+ import social from './ops/social.js'
14
+
15
+ export const OPS = [...account, ...library, ...film, ...animation, ...cinema, ...design, ...website, ...fashion, ...maket, ...influencer, ...longvideo, ...social]
16
+
17
+ /** Stüdyo kümeleri (yesilcam-mcp --studios ile süzme). */
18
+ export const STUDIO_OF = {
19
+ account: 'core', wait: 'core', jobs: 'core', library: 'core', voices: 'film', prompt: 'film',
20
+ image: 'image', video: 'film', characters: 'film', film: 'film',
21
+ animation: 'animation', inkmation: 'animation', cinema: 'cinema', design: 'design',
22
+ website: 'website', promo: 'website', assemble: 'website', fashion: 'fashion', maket: 'maket',
23
+ influencer: 'influencer', discover: 'influencer', longvideo: 'influencer', automation: 'influencer', social: 'influencer',
24
+ }
25
+ export const STUDIOS = [...new Set(Object.values(STUDIO_OF))]
26
+ export const studioOf = (o) => STUDIO_OF[o.group] || 'core'
27
+
28
+ export function selectOps({ studios, readOnly } = {}) {
29
+ const want = studios && studios.length ? new Set(['core', ...studios]) : null
30
+ return OPS.filter((o) => (!want || want.has(studioOf(o))) && (!readOnly || o.kind === 'read' || o.kind === 'local'))
31
+ }
32
+
33
+ /** Teklif + onay kuralı: jeton harcayan HER işlem onay ister (aylık ücretsiz hak da sınırlı bir haktır).
34
+ * Yalnız zeroRunsFree işaretli işlemler (ör. ücretsiz biçimde dışa aktarma, ilk kare onayı) teklif 0 iken onaysız çalışır. */
35
+ export const needsConfirm = (o, q) => o.kind === 'spend' && (o.confirm || !(o.zeroRunsFree && q && Number(q.total) === 0))
36
+
37
+ /** Ortak yürütücü (MCP ve CLI aynı yolu kullanır). */
38
+ export async function prepareOp(o, c, a) { return o.prepare ? (await o.prepare(c, a)) || {} : {} }
package/src/run.js ADDED
@@ -0,0 +1,36 @@
1
+ // MCP ve CLI'ın paylaştığı yürütme: hazırlık → (spend) teklif + onay kapısı → çalıştır → (isteğe bağlı) bekle + indir.
2
+ import { prepareOp, needsConfirm } from './registry.js'
3
+ import { waitFor } from './wait.js'
4
+ import { downloadUrl } from './files.js'
5
+
6
+ export const SHARED_KEYS = ['confirm', 'max_tokens', 'wait', 'max_wait_seconds', 'save_to']
7
+
8
+ /**
9
+ * execute(op, client, args, { confirmed, askConfirm, onProgress })
10
+ * → { status: 'quote', quote } (onay yok) | { status: 'insufficient', quote } | { status: 'done', result, quote?, final? }
11
+ * askConfirm(quote) → Promise<boolean> (CLI etkileşimli onayı); confirmed → önceden onaylı (MCP confirm=true, CLI --yes).
12
+ */
13
+ export async function execute(op, c, args, { confirmed = false, askConfirm, onProgress } = {}) {
14
+ const a = Object.fromEntries(Object.entries(args || {}).filter(([k]) => !SHARED_KEYS.includes(k)))
15
+ const p = await prepareOp(op, c, a)
16
+ let q
17
+ if (op.kind === 'spend') {
18
+ q = await op.quote(c, a, p)
19
+ if (q?.unavailable) return { status: 'unavailable', quote: q }
20
+ if (needsConfirm(op, q)) {
21
+ if (args.max_tokens != null && Number(q.total) > Number(args.max_tokens)) return { status: 'over_limit', quote: q, message: `Quote ${q.total} tokens is above max_tokens ${args.max_tokens}. Nothing was charged.` }
22
+ if (q.can_afford === false) return { status: 'insufficient', quote: q, message: 'Not enough tokens for this. Nothing was charged. Top up at https://yesilcamstudios.com/app/billing' }
23
+ const ok = confirmed || (askConfirm ? await askConfirm(q) : false)
24
+ if (!ok) return { status: 'quote', quote: q }
25
+ }
26
+ }
27
+ const result = await op.run(c, a, { onProgress }, p)
28
+ const out = { status: 'done', result, ...(q ? { quote: q } : {}) }
29
+ const w = op.wait ? op.wait(result || {}, a) : null
30
+ if (w && (args.wait || args.save_to)) {
31
+ const st = await waitFor(c, w.kind, w.id, { timeoutMs: (Number(args.max_wait_seconds) || 900) * 1000, onTick: onProgress })
32
+ if (args.save_to && st.ok && st.url) st.saved = await downloadUrl(c, st.url, args.save_to)
33
+ out.final = st
34
+ } else if (w) out.track = { kind: w.kind, id: String(w.id) }
35
+ return out
36
+ }
package/src/wait.js ADDED
@@ -0,0 +1,167 @@
1
+ // Uzun işler için ortak yoklama. Her "tür" bir durum ucunu okuyup ortak biçime çevirir:
2
+ // { kind, id, status, done, ok, needs_action?, progress, stage, url, poster, asset_id, error, tokens_charged, refunded }
3
+ const J = (v) => { if (v && typeof v === 'object') return v; try { return JSON.parse(v || '{}') } catch { return {} } }
4
+ const num = (v) => (v == null || v === '' ? null : Number(v))
5
+ const pick = (o, ...keys) => { for (const k of keys) { const v = k.split('.').reduce((a, p) => (a == null ? a : a[p]), o); if (v != null && v !== '') return v } return null }
6
+ const split2 = (id) => { const [a, b] = String(id).split(':'); return [a, b] }
7
+
8
+ const KINDS = {
9
+ job: {
10
+ label: 'Film Studio / Image Studio / Fashion job',
11
+ fetch: (c, id) => c.call('aistudio-job-status', { job_id: Number(id) }).then((r) => r.job),
12
+ norm: (j) => {
13
+ const ip = J(j?.input_params)
14
+ const awaiting = j?.status === 'waiting' && (ip.awaiting_approval == 1 || ip.awaiting_approval === true) && Boolean(ip.source_image_url)
15
+ return { status: awaiting ? 'awaiting_approval' : j?.status, done: awaiting || ['succeeded', 'failed', 'cancelled', 'canceled', 'refunded', 'done'].includes(j?.status),
16
+ ok: j?.status === 'succeeded' || j?.status === 'done', needs_action: awaiting ? 'Approve or reject the first frame: yesilcam_first_frame (approve|reject|regen)' : null,
17
+ progress: num(j?.render_progress), url: pick(j, 'result_asset.spaces_url'), poster: pick(j, 'result_asset.thumb_url'), asset_id: pick(j, 'result_asset.id', 'result_asset_id'),
18
+ error: j?.error_code || null, tokens_charged: num(j?.tokens_charged), refunded: Boolean(j?.refunded_at) }
19
+ },
20
+ },
21
+ influencer_job: {
22
+ label: 'AI Influencer video or create job',
23
+ fetch: (c, id) => c.call('ys-influencer-job', { job_id: Number(id) }).then((r) => r.job),
24
+ norm: (j) => ({ status: j?.status, done: ['ready', 'failed'].includes(j?.status), ok: j?.status === 'ready', progress: num(j?.progress), stage: j?.stage || null,
25
+ url: pick(j, 'result.url'), poster: pick(j, 'result.poster'), asset_id: pick(j, 'result.asset_id'), error: j?.error_code || null, tokens_charged: num(j?.tokens_charged), refunded: Boolean(j?.refunded) }),
26
+ },
27
+ influencer: {
28
+ label: 'AI Influencer character set',
29
+ fetch: (c, id) => c.call('ys-influencer-get', { influencer_id: Number(id) }).then((r) => r.influencer),
30
+ norm: (i) => ({ status: i?.status, done: ['ready', 'failed'].includes(i?.status), ok: i?.status === 'ready', progress: num(pick(i, 'job.progress')), stage: pick(i, 'job.stage'),
31
+ url: i?.cover_url || null, error: pick(i, 'job.error_code'), refunded: Boolean(pick(i, 'job.refunded')) }),
32
+ },
33
+ edit: {
34
+ label: 'Long video (edit studio)',
35
+ fetch: (c, id) => c.call('ys-influencer-edit', { op: 'get', edit_id: Number(id) }).then((r) => r.edit),
36
+ norm: (e) => ({ status: e?.status, done: !['planning', 'framing', 'producing'].includes(e?.status), ok: ['draft', 'ready', 'partial'].includes(e?.status),
37
+ needs_action: e?.status === 'paused' ? 'Paused (balance). Top up, then: yesilcam_longvideo_resume' : (e?.status === 'draft' ? 'Plan ready. Review/edit the storyboard, then quote and produce.' : null),
38
+ progress: num(pick(e, 'job.progress', 'plan_job.progress', 'frames_job.progress')), url: pick(e, 'result.url'), poster: e?.cover || null, error: pick(e, 'job.error_code', 'plan_job.error_code') }),
39
+ },
40
+ anim: {
41
+ label: 'Animation Studio project',
42
+ fetch: (c, id) => c.call('aistudio-anim-get', { project_id: Number(id) }),
43
+ norm: (p) => ({ status: p?.status, done: ['ready', 'failed'].includes(p?.status), ok: p?.status === 'ready', progress: num(p?.progress), stage: p?.stage || null, poster: p?.cover || null, error: p?.error_code || null, tokens_charged: num(p?.tokens_charged) }),
44
+ },
45
+ anim_render: {
46
+ label: 'Animation render (id = projectId:renderId)',
47
+ fetch: async (c, id) => { const [p, r] = split2(id); const x = await c.call('aistudio-anim-renders', { project_id: Number(p) }); return (x.renders || []).find((k) => String(k.id) === String(r)) || null },
48
+ norm: (r) => ({ status: r?.status, done: ['done', 'failed'].includes(r?.status), ok: r?.status === 'done', progress: num(r?.progress), url: r?.url || null, error: r?.error || null, tokens_charged: num(r?.tokens_charged) }),
49
+ },
50
+ motion: {
51
+ label: 'Animation › Custom (image + camera path)',
52
+ fetch: (c, id) => c.call('aistudio-motion-get', { job_id: Number(id) }).then((r) => r.job),
53
+ norm: (j) => ({ status: j?.status, done: ['ready', 'failed'].includes(j?.status), ok: j?.status === 'ready', progress: num(j?.progress), stage: j?.stage || null,
54
+ url: pick(j, 'result.url'), poster: pick(j, 'result.poster'), asset_id: pick(j, 'result.asset_id'), error: j?.error_code || null, refunded: Boolean(j?.refunded) }),
55
+ },
56
+ inkmation: {
57
+ label: 'Inkmation project (script + voice)',
58
+ fetch: (c, id) => c.call('aistudio-inkmation-get', { project_id: Number(id) }),
59
+ norm: (p) => ({ status: p?.status, done: p?.status === 'failed' || (p?.status === 'ready' && p?.audio_status !== 'voicing'), ok: p?.status === 'ready', stage: p?.audio_status ? `audio: ${p.audio_status}` : null, error: p?.error_code || p?.audio_error || null }),
60
+ },
61
+ inkmation_render: {
62
+ label: 'Inkmation render (id = projectId:renderId)',
63
+ fetch: async (c, id) => { const [p, r] = split2(id); const x = await c.call('aistudio-inkmation-renders', { project_id: Number(p) }); return (x.renders || []).find((k) => String(k.id) === String(r)) || null },
64
+ norm: (r) => ({ status: r?.status, done: ['done', 'failed'].includes(r?.status), ok: r?.status === 'done', progress: num(r?.progress), url: r?.url || null, error: r?.error || null }),
65
+ },
66
+ design: {
67
+ label: 'Design Studio design (generate / add detail)',
68
+ fetch: (c, id) => c.call('ys-design-get', { design_id: Number(id) }).then((r) => r.design),
69
+ norm: (d) => ({ status: d?.status, done: d?.status !== 'working', ok: d?.status === 'ready', progress: num(pick(d, 'job.progress')), stage: pick(d, 'job.stage'), poster: d?.thumb_url || null, error: pick(d, 'job.error_code'), tokens_charged: num(pick(d, 'job.tokens_charged')) }),
70
+ },
71
+ design_export: {
72
+ label: 'Design Studio export',
73
+ fetch: (c, id) => c.call('ys-design-export-get', { export_id: Number(id) }).then((r) => r.export),
74
+ norm: (x) => ({ status: x?.status, done: ['ready', 'failed'].includes(x?.status), ok: x?.status === 'ready', url: x?.url || null, asset_id: x?.asset_id || null, error: x?.error_code || null, tokens_charged: num(x?.tokens_charged) }),
75
+ },
76
+ website: {
77
+ label: 'Website Studio site',
78
+ fetch: (c, id) => c.call('ys-website-get', { site_id: Number(id) }).then((r) => r.site),
79
+ norm: (s) => ({ status: s?.status, done: ['ready', 'failed'].includes(s?.status), ok: s?.status === 'ready', progress: num(s?.progress), url: s?.public_url || null, poster: s?.cover_url || null, zip_url: s?.zip_url || null, error: s?.error_code || null }),
80
+ },
81
+ promo: {
82
+ label: 'Website Promo video',
83
+ fetch: (c, id) => c.call('sitefilm-get', { project_id: Number(id) }).then((r) => r.project || r),
84
+ norm: (p) => ({ status: p?.status, done: ['review', 'ready', 'failed'].includes(p?.status), ok: ['review', 'ready'].includes(p?.status),
85
+ needs_action: p?.status === 'review' ? 'Script ready for review. Edit (promo save-script) or produce (yesilcam_promo_produce).' : null,
86
+ url: pick(p, 'result.video_url'), poster: pick(p, 'result.poster_url'), error: p?.error_code || null }),
87
+ },
88
+ assemble: {
89
+ label: 'Assemble / Disassemble video',
90
+ fetch: (c, id) => c.call('aistudio-assemble-get', { project_id: Number(id) }).then((r) => r.project),
91
+ norm: (p) => ({ status: p?.status, done: ['ready', 'failed'].includes(p?.status), ok: p?.status === 'ready', progress: num(p?.progress), stage: p?.stage || null,
92
+ url: pick(p, 'result.video_url'), poster: pick(p, 'result.poster_url'), asset_id: pick(p, 'result.asset_id'), error: p?.error_code || null }),
93
+ },
94
+ enhance: {
95
+ label: 'Product photo enhance',
96
+ fetch: (c, id) => c.call('aistudio-assemble-enhance-get', { job_id: Number(id) }),
97
+ norm: (j) => ({ status: j?.status, done: ['ready', 'failed'].includes(j?.status), ok: j?.status === 'ready', progress: num(j?.progress), url: pick(j, 'result.url'), asset_id: pick(j, 'result.asset_id'), error: j?.error_code || null }),
98
+ },
99
+ maket_design: {
100
+ label: 'Maket furnishing / landscape design',
101
+ fetch: (c, id) => c.call('maket-design-get', { jobId: String(id) }),
102
+ norm: (j) => ({ status: j?.status, done: ['ready', 'failed'].includes(j?.status), ok: j?.status === 'ready', progress: num(j?.progress), error: j?.error || null, tokens_charged: num(j?.charged) }),
103
+ },
104
+ maket_render: {
105
+ label: 'Maket photoreal render',
106
+ fetch: (c, id) => c.call('maket-render-get', { jobId: String(id) }),
107
+ norm: (j) => ({ status: j?.status, done: ['ready', 'failed'].includes(j?.status), ok: j?.status === 'ready', progress: num(j?.progress), url: pick(j, 'result.url'), error: j?.error || null, tokens_charged: num(j?.charged) }),
108
+ },
109
+ flow: {
110
+ label: 'Film Studio flow / Story to Film',
111
+ fetch: (c, id) => c.call('aistudio-flow-get', { flow_id: Number(id) }),
112
+ norm: (r) => { const f = r?.flow || {}; return { status: f.status, done: !['directing', 'casting', 'storyboarding', 'producing', 'assembling'].includes(f.status), ok: ['ready', 'completed'].includes(f.status),
113
+ needs_action: f.status === 'ready' ? 'Storyboard ready. Estimate and produce: yesilcam_flow_produce' : null,
114
+ url: pick(r, 'final_asset.spaces_url'), poster: pick(r, 'final_asset.thumb_url'), asset_id: f.final_asset_id || null, error: f.error_code || null } },
115
+ },
116
+ bible: {
117
+ label: 'Character Bible',
118
+ fetch: (c, id) => c.call('aistudio-character-bible-get', { character_id: Number(id) }),
119
+ norm: (b) => ({ status: b?.status, done: b?.status !== 'generating', ok: b?.status === 'ready', poster: pick(b, 'character.thumb_url'), error: b?.bible_error || null }),
120
+ },
121
+ auto_run: {
122
+ label: 'Automation run',
123
+ fetch: (c, id) => c.call('ys-influencer-auto', { op: 'run', run_id: Number(id) }).then((r) => r.run),
124
+ norm: (r) => ({ status: r?.status, done: ['delivered', 'failed', 'skipped', 'canceled', 'review'].includes(r?.status), ok: ['delivered', 'review'].includes(r?.status),
125
+ needs_action: r?.status === 'review' ? 'Waiting for your approval: yesilcam_automation_review action=approve|skip' : null, url: pick(r, 'result.url'), poster: r?.cover || null, error: r?.error_code || null }),
126
+ },
127
+ social_post: {
128
+ label: 'Social media post',
129
+ fetch: (c, id) => c.call('ys-social', { op: 'post', post_id: Number(id) }).then((r) => r.post || r),
130
+ norm: (p) => ({ status: p?.status, done: ['published', 'failed', 'canceled'].includes(p?.status), ok: p?.status === 'published', url: p?.permalink || p?.url || null, error: p?.error_code || null }),
131
+ },
132
+ cinema_ai: {
133
+ label: 'Cinema Studio presenter / subtitles',
134
+ fetch: (c, id) => c.call('aistudio-cinema-ai-job', { job_id: String(id) }),
135
+ norm: (j) => ({ status: j?.status, done: ['done', 'failed'].includes(j?.status), ok: j?.status === 'done', url: pick(j, 'item.url'), asset_id: pick(j, 'item.asset_id'), error: j?.error_code || null, tokens_charged: num(j?.tokens_charged), refunded: Boolean(j?.refunded) }),
136
+ },
137
+ }
138
+ export const WAIT_KINDS = Object.keys(KINDS)
139
+ export const waitKindHelp = () => Object.entries(KINDS).map(([k, v]) => `${k}: ${v.label}`).join('; ')
140
+
141
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
142
+
143
+ /** Tek okuma. */
144
+ export async function checkOnce(c, kind, id) {
145
+ const K = KINDS[kind]; if (!K) throw new Error(`unknown kind: ${kind}`)
146
+ const raw = await K.fetch(c, id)
147
+ if (raw == null) return { kind, id: String(id), status: 'not_found', done: true, ok: false }
148
+ return { kind, id: String(id), ...K.norm(raw) }
149
+ }
150
+
151
+ /** Bitene (ya da işlem gerekene / süre dolana) kadar bekler. onTick(state) ilerleme için. */
152
+ export async function waitFor(c, kind, id, { intervalMs = 4000, timeoutMs = 20 * 60 * 1000, onTick, sleepImpl = sleep } = {}) {
153
+ const started = Date.now()
154
+ let errors = 0
155
+ for (;;) {
156
+ let st
157
+ try { st = await checkOnce(c, kind, id); errors = 0 }
158
+ catch (e) { if (++errors >= 4) throw e; await sleepImpl(intervalMs * errors); continue }
159
+ onTick?.(st)
160
+ if (st.done) return st
161
+ if (Date.now() - started > timeoutMs) return { ...st, timed_out: true, hint: `Still running. Check later: yesilcam wait ${kind} ${id}` }
162
+ await sleepImpl(intervalMs)
163
+ }
164
+ }
165
+
166
+ // Geriye uyum (0.2): jobs.js yerine
167
+ export async function waitForJob(c, jobId, opts = {}) { const s = await waitFor(c, 'job', jobId, opts); return s }