@jackwener/opencli 1.3.1 → 1.3.2
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/CHANGELOG.md +128 -0
- package/README.md +44 -5
- package/README.zh-CN.md +44 -5
- package/SKILL.md +317 -5
- package/TESTING.md +4 -4
- package/dist/browser/errors.d.ts +2 -1
- package/dist/browser/errors.js +9 -10
- package/dist/build-manifest.js +1 -3
- package/dist/cli-manifest.json +2573 -989
- package/dist/cli.js +42 -2
- package/dist/clis/bilibili/download.js +20 -65
- package/dist/clis/bilibili/utils.js +2 -1
- package/dist/clis/chaoxing/assignments.js +2 -1
- package/dist/clis/doubao/ask.d.ts +1 -0
- package/dist/clis/doubao/ask.js +35 -0
- package/dist/clis/doubao/common.d.ts +23 -0
- package/dist/clis/doubao/common.js +564 -0
- package/dist/clis/doubao/new.d.ts +1 -0
- package/dist/clis/doubao/new.js +20 -0
- package/dist/clis/doubao/read.d.ts +1 -0
- package/dist/clis/doubao/read.js +19 -0
- package/dist/clis/doubao/send.d.ts +1 -0
- package/dist/clis/doubao/send.js +22 -0
- package/dist/clis/doubao/status.d.ts +1 -0
- package/dist/clis/doubao/status.js +24 -0
- package/dist/clis/doubao-app/ask.d.ts +1 -0
- package/dist/clis/doubao-app/ask.js +53 -0
- package/dist/clis/doubao-app/common.d.ts +37 -0
- package/dist/clis/doubao-app/common.js +110 -0
- package/dist/clis/doubao-app/dump.d.ts +1 -0
- package/dist/clis/doubao-app/dump.js +24 -0
- package/dist/clis/doubao-app/new.d.ts +1 -0
- package/dist/clis/doubao-app/new.js +20 -0
- package/dist/clis/doubao-app/read.d.ts +1 -0
- package/dist/clis/doubao-app/read.js +18 -0
- package/dist/clis/doubao-app/screenshot.d.ts +1 -0
- package/dist/clis/doubao-app/screenshot.js +18 -0
- package/dist/clis/doubao-app/send.d.ts +1 -0
- package/dist/clis/doubao-app/send.js +27 -0
- package/dist/clis/doubao-app/status.d.ts +1 -0
- package/dist/clis/doubao-app/status.js +16 -0
- package/dist/clis/hackernews/ask.yaml +38 -0
- package/dist/clis/hackernews/best.yaml +38 -0
- package/dist/clis/hackernews/jobs.yaml +36 -0
- package/dist/clis/hackernews/new.yaml +38 -0
- package/dist/clis/hackernews/search.yaml +44 -0
- package/dist/clis/hackernews/show.yaml +38 -0
- package/dist/clis/hackernews/top.yaml +3 -1
- package/dist/clis/hackernews/user.yaml +25 -0
- package/dist/clis/twitter/download.js +13 -97
- package/dist/clis/twitter/thread.js +2 -1
- package/dist/clis/v2ex/member.yaml +29 -0
- package/dist/clis/v2ex/node.yaml +34 -0
- package/dist/clis/v2ex/nodes.yaml +31 -0
- package/dist/clis/v2ex/replies.yaml +32 -0
- package/dist/clis/v2ex/user.yaml +34 -0
- package/dist/clis/weibo/search.d.ts +1 -0
- package/dist/clis/weibo/search.js +73 -0
- package/dist/clis/weixin/download.d.ts +12 -0
- package/dist/clis/weixin/download.js +183 -0
- package/dist/clis/xiaohongshu/download.js +12 -60
- package/dist/clis/xiaohongshu/publish.d.ts +18 -0
- package/dist/clis/xiaohongshu/publish.js +352 -0
- package/dist/clis/xiaohongshu/search.js +47 -15
- package/dist/clis/xiaohongshu/search.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/search.test.js +114 -0
- package/dist/clis/yollomi/background.d.ts +4 -0
- package/dist/clis/yollomi/background.js +45 -0
- package/dist/clis/yollomi/edit.d.ts +5 -0
- package/dist/clis/yollomi/edit.js +56 -0
- package/dist/clis/yollomi/face-swap.d.ts +5 -0
- package/dist/clis/yollomi/face-swap.js +43 -0
- package/dist/clis/yollomi/generate.d.ts +9 -0
- package/dist/clis/yollomi/generate.js +100 -0
- package/dist/clis/yollomi/models.d.ts +1 -0
- package/dist/clis/yollomi/models.js +33 -0
- package/dist/clis/yollomi/object-remover.d.ts +4 -0
- package/dist/clis/yollomi/object-remover.js +42 -0
- package/dist/clis/yollomi/remove-bg.d.ts +4 -0
- package/dist/clis/yollomi/remove-bg.js +38 -0
- package/dist/clis/yollomi/restore.d.ts +4 -0
- package/dist/clis/yollomi/restore.js +38 -0
- package/dist/clis/yollomi/try-on.d.ts +4 -0
- package/dist/clis/yollomi/try-on.js +46 -0
- package/dist/clis/yollomi/upload.d.ts +7 -0
- package/dist/clis/yollomi/upload.js +71 -0
- package/dist/clis/yollomi/upscale.d.ts +4 -0
- package/dist/clis/yollomi/upscale.js +53 -0
- package/dist/clis/yollomi/utils.d.ts +45 -0
- package/dist/clis/yollomi/utils.js +180 -0
- package/dist/clis/yollomi/video.d.ts +5 -0
- package/dist/clis/yollomi/video.js +56 -0
- package/dist/clis/zhihu/download.d.ts +1 -5
- package/dist/clis/zhihu/download.js +20 -126
- package/dist/clis/zhihu/download.test.js +7 -5
- package/dist/clis/zhihu/question.js +2 -1
- package/dist/commanderAdapter.js +4 -6
- package/dist/daemon.js +5 -2
- package/dist/discovery.js +10 -10
- package/dist/download/article-download.d.ts +59 -0
- package/dist/download/article-download.js +178 -0
- package/dist/download/media-download.d.ts +49 -0
- package/dist/download/media-download.js +112 -0
- package/dist/errors.d.ts +23 -2
- package/dist/errors.js +58 -2
- package/dist/errors.test.d.ts +1 -0
- package/dist/errors.test.js +59 -0
- package/dist/execution.js +9 -10
- package/dist/explore.js +4 -2
- package/dist/external.d.ts +15 -0
- package/dist/external.js +48 -2
- package/dist/external.test.d.ts +1 -0
- package/dist/external.test.js +64 -0
- package/dist/main.js +10 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.js +45 -23
- package/dist/plugin.test.js +6 -1
- package/dist/record.d.ts +47 -0
- package/dist/record.js +545 -0
- package/dist/registry.d.ts +7 -2
- package/dist/registry.js +2 -6
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +10 -3
- package/dist/validate.js +1 -3
- package/docs/.vitepress/config.mts +1 -0
- package/docs/adapters/browser/doubao.md +35 -0
- package/docs/adapters/browser/hackernews.md +20 -4
- package/docs/adapters/browser/tiktok.md +1 -1
- package/docs/adapters/browser/v2ex.md +31 -10
- package/docs/adapters/browser/weibo.md +4 -0
- package/docs/adapters/browser/weixin.md +33 -0
- package/docs/adapters/browser/xiaohongshu.md +8 -6
- package/docs/adapters/browser/yollomi.md +69 -0
- package/docs/adapters/desktop/doubao-app.md +35 -0
- package/docs/adapters/index.md +16 -5
- package/docs/advanced/download.md +4 -0
- package/package.json +3 -1
- package/src/browser/errors.ts +17 -11
- package/src/build-manifest.ts +2 -3
- package/src/cli.ts +45 -2
- package/src/clis/bilibili/download.ts +25 -83
- package/src/clis/bilibili/utils.ts +2 -1
- package/src/clis/chaoxing/assignments.ts +2 -1
- package/src/clis/doubao/ask.ts +40 -0
- package/src/clis/doubao/common.ts +619 -0
- package/src/clis/doubao/new.ts +22 -0
- package/src/clis/doubao/read.ts +20 -0
- package/src/clis/doubao/send.ts +25 -0
- package/src/clis/doubao/status.ts +27 -0
- package/src/clis/doubao-app/ask.ts +60 -0
- package/src/clis/doubao-app/common.ts +116 -0
- package/src/clis/doubao-app/dump.ts +28 -0
- package/src/clis/doubao-app/new.ts +21 -0
- package/src/clis/doubao-app/read.ts +21 -0
- package/src/clis/doubao-app/screenshot.ts +19 -0
- package/src/clis/doubao-app/send.ts +30 -0
- package/src/clis/doubao-app/status.ts +17 -0
- package/src/clis/hackernews/ask.yaml +38 -0
- package/src/clis/hackernews/best.yaml +38 -0
- package/src/clis/hackernews/jobs.yaml +36 -0
- package/src/clis/hackernews/new.yaml +38 -0
- package/src/clis/hackernews/search.yaml +44 -0
- package/src/clis/hackernews/show.yaml +38 -0
- package/src/clis/hackernews/top.yaml +3 -1
- package/src/clis/hackernews/user.yaml +25 -0
- package/src/clis/twitter/download.ts +13 -111
- package/src/clis/twitter/thread.ts +2 -1
- package/src/clis/v2ex/member.yaml +29 -0
- package/src/clis/v2ex/node.yaml +34 -0
- package/src/clis/v2ex/nodes.yaml +31 -0
- package/src/clis/v2ex/replies.yaml +32 -0
- package/src/clis/v2ex/user.yaml +34 -0
- package/src/clis/weibo/search.ts +78 -0
- package/src/clis/weixin/download.ts +199 -0
- package/src/clis/xiaohongshu/download.ts +12 -71
- package/src/clis/xiaohongshu/publish.ts +392 -0
- package/src/clis/xiaohongshu/search.test.ts +134 -0
- package/src/clis/xiaohongshu/search.ts +49 -15
- package/src/clis/yollomi/background.ts +48 -0
- package/src/clis/yollomi/edit.ts +58 -0
- package/src/clis/yollomi/face-swap.ts +45 -0
- package/src/clis/yollomi/generate.ts +95 -0
- package/src/clis/yollomi/models.ts +38 -0
- package/src/clis/yollomi/object-remover.ts +44 -0
- package/src/clis/yollomi/remove-bg.ts +40 -0
- package/src/clis/yollomi/restore.ts +40 -0
- package/src/clis/yollomi/try-on.ts +48 -0
- package/src/clis/yollomi/upload.ts +78 -0
- package/src/clis/yollomi/upscale.ts +49 -0
- package/src/clis/yollomi/utils.ts +202 -0
- package/src/clis/yollomi/video.ts +61 -0
- package/src/clis/zhihu/download.test.ts +7 -5
- package/src/clis/zhihu/download.ts +23 -158
- package/src/clis/zhihu/question.ts +2 -1
- package/src/commanderAdapter.ts +4 -7
- package/src/daemon.ts +5 -2
- package/src/discovery.ts +26 -26
- package/src/download/article-download.ts +272 -0
- package/src/download/media-download.ts +178 -0
- package/src/errors.test.ts +79 -0
- package/src/errors.ts +92 -2
- package/src/execution.ts +14 -10
- package/src/explore.ts +4 -2
- package/src/external.test.ts +88 -0
- package/src/external.ts +56 -2
- package/src/generate.ts +2 -1
- package/src/main.ts +10 -0
- package/src/plugin.test.ts +7 -1
- package/src/plugin.ts +49 -25
- package/src/record.ts +617 -0
- package/src/registry.ts +9 -5
- package/src/runtime.ts +16 -4
- package/src/validate.ts +2 -3
- package/tests/e2e/browser-auth.test.ts +10 -1
- package/tests/e2e/browser-public.test.ts +13 -8
- package/tests/e2e/public-commands.test.ts +209 -21
- package/tests/smoke/api-health.test.ts +65 -6
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import './search.js';
|
|
4
|
+
function createPageMock(evaluateResults) {
|
|
5
|
+
const evaluate = vi.fn();
|
|
6
|
+
for (const result of evaluateResults) {
|
|
7
|
+
evaluate.mockResolvedValueOnce(result);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
evaluate,
|
|
12
|
+
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
typeText: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
18
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
20
|
+
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
newTab: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
24
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
25
|
+
scroll: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
29
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
30
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
describe('xiaohongshu search', () => {
|
|
34
|
+
it('throws a clear error when the search page is blocked by a login wall', async () => {
|
|
35
|
+
const cmd = getRegistry().get('xiaohongshu/search');
|
|
36
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
37
|
+
const page = createPageMock([
|
|
38
|
+
{
|
|
39
|
+
loginWall: true,
|
|
40
|
+
results: [],
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
await expect(cmd.func(page, { query: '特斯拉', limit: 5 })).rejects.toThrow('Xiaohongshu search results are blocked behind a login wall');
|
|
44
|
+
});
|
|
45
|
+
it('returns ranked results with search_result url and author_url preserved', async () => {
|
|
46
|
+
const cmd = getRegistry().get('xiaohongshu/search');
|
|
47
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
48
|
+
const detailUrl = 'https://www.xiaohongshu.com/search_result/68e90be80000000004022e66?xsec_token=test-token&xsec_source=';
|
|
49
|
+
const authorUrl = 'https://www.xiaohongshu.com/user/profile/635a9c720000000018028b40?xsec_token=user-token&xsec_source=pc_search';
|
|
50
|
+
const page = createPageMock([
|
|
51
|
+
{
|
|
52
|
+
loginWall: false,
|
|
53
|
+
results: [
|
|
54
|
+
{
|
|
55
|
+
title: '某鱼买FSD被坑了4万',
|
|
56
|
+
author: '随风',
|
|
57
|
+
likes: '261',
|
|
58
|
+
url: detailUrl,
|
|
59
|
+
author_url: authorUrl,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
const result = await cmd.func(page, { query: '特斯拉', limit: 1 });
|
|
65
|
+
// Should only do one goto (the search page itself), no per-note detail navigation
|
|
66
|
+
expect(page.goto.mock.calls).toHaveLength(1);
|
|
67
|
+
expect(result).toEqual([
|
|
68
|
+
{
|
|
69
|
+
rank: 1,
|
|
70
|
+
title: '某鱼买FSD被坑了4万',
|
|
71
|
+
author: '随风',
|
|
72
|
+
likes: '261',
|
|
73
|
+
url: detailUrl,
|
|
74
|
+
author_url: authorUrl,
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
it('filters out results with no title and respects the limit', async () => {
|
|
79
|
+
const cmd = getRegistry().get('xiaohongshu/search');
|
|
80
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
81
|
+
const page = createPageMock([
|
|
82
|
+
{
|
|
83
|
+
loginWall: false,
|
|
84
|
+
results: [
|
|
85
|
+
{
|
|
86
|
+
title: 'Result A',
|
|
87
|
+
author: 'UserA',
|
|
88
|
+
likes: '10',
|
|
89
|
+
url: 'https://www.xiaohongshu.com/search_result/aaa',
|
|
90
|
+
author_url: '',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
title: '',
|
|
94
|
+
author: 'UserB',
|
|
95
|
+
likes: '5',
|
|
96
|
+
url: 'https://www.xiaohongshu.com/search_result/bbb',
|
|
97
|
+
author_url: '',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
title: 'Result C',
|
|
101
|
+
author: 'UserC',
|
|
102
|
+
likes: '3',
|
|
103
|
+
url: 'https://www.xiaohongshu.com/search_result/ccc',
|
|
104
|
+
author_url: '',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
const result = (await cmd.func(page, { query: '测试', limit: 1 }));
|
|
110
|
+
// limit=1 should return only the first valid-titled result
|
|
111
|
+
expect(result).toHaveLength(1);
|
|
112
|
+
expect(result[0]).toMatchObject({ rank: 1, title: 'Result A' });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi AI background generator — POST /api/ai/ai-background-generator
|
|
3
|
+
*/
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { CliError } from '../../errors.js';
|
|
8
|
+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
|
|
9
|
+
cli({
|
|
10
|
+
site: 'yollomi',
|
|
11
|
+
name: 'background',
|
|
12
|
+
description: 'Generate AI background for a product/object image (5 credits)',
|
|
13
|
+
domain: YOLLOMI_DOMAIN,
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'image', positional: true, required: true, help: 'Image URL (upload via "opencli yollomi upload" first)' },
|
|
17
|
+
{ name: 'prompt', default: '', help: 'Background description (optional)' },
|
|
18
|
+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
|
|
19
|
+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['status', 'file', 'size', 'url'],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const imageUrl = kwargs.image;
|
|
24
|
+
const prompt = kwargs.prompt;
|
|
25
|
+
process.stderr.write(chalk.dim('Generating background...\n'));
|
|
26
|
+
const data = await yollomiPost(page, '/api/ai/ai-background-generator', {
|
|
27
|
+
images: [imageUrl],
|
|
28
|
+
prompt: prompt || undefined,
|
|
29
|
+
aspect_ratio: '1:1',
|
|
30
|
+
});
|
|
31
|
+
const url = data.image || (data.images?.[0]);
|
|
32
|
+
if (!url)
|
|
33
|
+
throw new CliError('EMPTY_RESPONSE', 'No result', 'Try a different image');
|
|
34
|
+
if (kwargs['no-download'])
|
|
35
|
+
return [{ status: 'generated', file: '-', size: '-', url }];
|
|
36
|
+
try {
|
|
37
|
+
const filename = `yollomi_bg_${Date.now()}.png`;
|
|
38
|
+
const { path: fp, size } = await downloadOutput(url, kwargs.output, filename);
|
|
39
|
+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }];
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [{ status: 'download-failed', file: '-', size: '-', url }];
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi image editing — POST /api/ai/qwen-image-edit
|
|
3
|
+
* Matches frontend workspace-generator.tsx for qwen-image-edit model.
|
|
4
|
+
*/
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
import { CliError } from '../../errors.js';
|
|
9
|
+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
|
|
10
|
+
cli({
|
|
11
|
+
site: 'yollomi',
|
|
12
|
+
name: 'edit',
|
|
13
|
+
description: 'Edit images with AI text prompts (Qwen image edit)',
|
|
14
|
+
domain: YOLLOMI_DOMAIN,
|
|
15
|
+
strategy: Strategy.COOKIE,
|
|
16
|
+
args: [
|
|
17
|
+
{ name: 'image', positional: true, required: true, help: 'Input image URL (upload via "opencli yollomi upload" first)' },
|
|
18
|
+
{ name: 'prompt', positional: true, required: true, help: 'Editing instruction (e.g. "Make it look vintage")' },
|
|
19
|
+
{ name: 'model', default: 'qwen-image-edit', choices: ['qwen-image-edit', 'qwen-image-edit-plus'], help: 'Edit model' },
|
|
20
|
+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
|
|
21
|
+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' },
|
|
22
|
+
],
|
|
23
|
+
columns: ['status', 'file', 'size', 'credits', 'url'],
|
|
24
|
+
func: async (page, kwargs) => {
|
|
25
|
+
const imageInput = kwargs.image;
|
|
26
|
+
const prompt = kwargs.prompt;
|
|
27
|
+
const modelId = kwargs.model;
|
|
28
|
+
let body;
|
|
29
|
+
if (modelId === 'qwen-image-edit-plus') {
|
|
30
|
+
body = { prompt, images: [imageInput] };
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
body = { image: imageInput, prompt, go_fast: true, output_format: 'png' };
|
|
34
|
+
}
|
|
35
|
+
const apiPath = modelId === 'qwen-image-edit-plus' ? '/api/ai/qwen-image-edit-plus' : '/api/ai/qwen-image-edit';
|
|
36
|
+
process.stderr.write(chalk.dim(`Editing with ${modelId}...\n`));
|
|
37
|
+
const data = await yollomiPost(page, apiPath, body);
|
|
38
|
+
const images = data.images || (data.image ? [data.image] : []);
|
|
39
|
+
if (!images.length)
|
|
40
|
+
throw new CliError('EMPTY_RESPONSE', 'No result', 'Try a different prompt');
|
|
41
|
+
const credits = data.remainingCredits;
|
|
42
|
+
const url = images[0];
|
|
43
|
+
if (kwargs['no-download'])
|
|
44
|
+
return [{ status: 'edited', file: '-', size: '-', credits: credits ?? '-', url }];
|
|
45
|
+
try {
|
|
46
|
+
const filename = `yollomi_edit_${Date.now()}.png`;
|
|
47
|
+
const { path: fp, size } = await downloadOutput(url, kwargs.output, filename);
|
|
48
|
+
if (credits !== undefined)
|
|
49
|
+
process.stderr.write(chalk.dim(`Credits remaining: ${credits}\n`));
|
|
50
|
+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), credits: credits ?? '-', url }];
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [{ status: 'download-failed', file: '-', size: '-', credits: credits ?? '-', url }];
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi face swap — POST /api/ai/face-swap
|
|
3
|
+
* Uses swap_image / input_image field names matching the frontend.
|
|
4
|
+
*/
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
import { CliError } from '../../errors.js';
|
|
9
|
+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
|
|
10
|
+
cli({
|
|
11
|
+
site: 'yollomi',
|
|
12
|
+
name: 'face-swap',
|
|
13
|
+
description: 'Swap faces between two photos (3 credits)',
|
|
14
|
+
domain: YOLLOMI_DOMAIN,
|
|
15
|
+
strategy: Strategy.COOKIE,
|
|
16
|
+
args: [
|
|
17
|
+
{ name: 'source', required: true, help: 'Source face image URL' },
|
|
18
|
+
{ name: 'target', required: true, help: 'Target photo URL' },
|
|
19
|
+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
|
|
20
|
+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' },
|
|
21
|
+
],
|
|
22
|
+
columns: ['status', 'file', 'size', 'url'],
|
|
23
|
+
func: async (page, kwargs) => {
|
|
24
|
+
process.stderr.write(chalk.dim('Swapping faces...\n'));
|
|
25
|
+
const data = await yollomiPost(page, '/api/ai/face-swap', {
|
|
26
|
+
swap_image: kwargs.source,
|
|
27
|
+
input_image: kwargs.target,
|
|
28
|
+
});
|
|
29
|
+
const url = data.image || (data.images?.[0]);
|
|
30
|
+
if (!url)
|
|
31
|
+
throw new CliError('EMPTY_RESPONSE', 'No result', 'Make sure both images contain clear faces');
|
|
32
|
+
if (kwargs['no-download'])
|
|
33
|
+
return [{ status: 'swapped', file: '-', size: '-', url }];
|
|
34
|
+
try {
|
|
35
|
+
const filename = `yollomi_faceswap_${Date.now()}.jpg`;
|
|
36
|
+
const { path: fp, size } = await downloadOutput(url, kwargs.output, filename);
|
|
37
|
+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }];
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [{ status: 'download-failed', file: '-', size: '-', url }];
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi text-to-image / image-to-image generation.
|
|
3
|
+
*
|
|
4
|
+
* Uses per-model routes exactly like the frontend:
|
|
5
|
+
* POST /api/ai/z-image-turbo { prompt, width, height, ... }
|
|
6
|
+
* POST /api/ai/nano-banana { prompt, aspect_ratio, ... }
|
|
7
|
+
* POST /api/ai/flux-2-pro { prompt, aspectRatio, imageUrl?, ... }
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi text-to-image / image-to-image generation.
|
|
3
|
+
*
|
|
4
|
+
* Uses per-model routes exactly like the frontend:
|
|
5
|
+
* POST /api/ai/z-image-turbo { prompt, width, height, ... }
|
|
6
|
+
* POST /api/ai/nano-banana { prompt, aspect_ratio, ... }
|
|
7
|
+
* POST /api/ai/flux-2-pro { prompt, aspectRatio, imageUrl?, ... }
|
|
8
|
+
*/
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { cli, Strategy } from '../../registry.js';
|
|
12
|
+
import { CliError } from '../../errors.js';
|
|
13
|
+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes, MODEL_ROUTES } from './utils.js';
|
|
14
|
+
function getDimensions(ratio) {
|
|
15
|
+
const map = {
|
|
16
|
+
'1:1': [1024, 1024], '16:9': [1344, 768], '9:16': [768, 1344],
|
|
17
|
+
'4:3': [1152, 896], '3:4': [896, 1152],
|
|
18
|
+
};
|
|
19
|
+
const [w, h] = map[ratio] || [1024, 1024];
|
|
20
|
+
return { width: w, height: h };
|
|
21
|
+
}
|
|
22
|
+
cli({
|
|
23
|
+
site: 'yollomi',
|
|
24
|
+
name: 'generate',
|
|
25
|
+
description: 'Generate images with AI (text-to-image or image-to-image)',
|
|
26
|
+
domain: YOLLOMI_DOMAIN,
|
|
27
|
+
strategy: Strategy.COOKIE,
|
|
28
|
+
args: [
|
|
29
|
+
{ name: 'prompt', positional: true, required: true, help: 'Text prompt describing the image' },
|
|
30
|
+
{ name: 'model', default: 'z-image-turbo', help: 'Model ID (z-image-turbo, flux-schnell, nano-banana, flux-2-pro, ...)' },
|
|
31
|
+
{ name: 'ratio', default: '1:1', choices: ['1:1', '16:9', '9:16', '4:3', '3:4'], help: 'Aspect ratio' },
|
|
32
|
+
{ name: 'image', help: 'Input image URL for image-to-image (upload via "opencli yollomi upload" first)' },
|
|
33
|
+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
|
|
34
|
+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URLs, skip download' },
|
|
35
|
+
],
|
|
36
|
+
columns: ['index', 'status', 'file', 'size', 'url'],
|
|
37
|
+
func: async (page, kwargs) => {
|
|
38
|
+
const prompt = kwargs.prompt;
|
|
39
|
+
const modelId = kwargs.model;
|
|
40
|
+
const ratio = kwargs.ratio;
|
|
41
|
+
const apiPath = MODEL_ROUTES[modelId];
|
|
42
|
+
if (!apiPath)
|
|
43
|
+
throw new CliError('INVALID_MODEL', `Unknown model: ${modelId}`, 'Run "opencli yollomi models --type image" to see available models');
|
|
44
|
+
let body;
|
|
45
|
+
if (modelId === 'z-image-turbo') {
|
|
46
|
+
const { width, height } = getDimensions(ratio);
|
|
47
|
+
body = { prompt, width, height, output_format: 'jpg', output_quality: 85, guidance_scale: 0, num_inference_steps: 8 };
|
|
48
|
+
}
|
|
49
|
+
else if (modelId === 'flux-2-pro') {
|
|
50
|
+
body = { prompt, aspectRatio: ratio, outputNumber: 1 };
|
|
51
|
+
if (kwargs.image)
|
|
52
|
+
body.imageUrl = kwargs.image;
|
|
53
|
+
}
|
|
54
|
+
else if (modelId === 'flux-kontext-pro') {
|
|
55
|
+
body = { prompt, output_format: 'jpg' };
|
|
56
|
+
if (kwargs.image)
|
|
57
|
+
body.imageUrl = kwargs.image;
|
|
58
|
+
if (ratio !== '1:1')
|
|
59
|
+
body.aspect_ratio = ratio;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
body = { prompt, aspect_ratio: ratio };
|
|
63
|
+
if (kwargs.image)
|
|
64
|
+
body.imageUrl = kwargs.image;
|
|
65
|
+
}
|
|
66
|
+
process.stderr.write(chalk.dim(`Generating with ${modelId}...\n`));
|
|
67
|
+
const data = await yollomiPost(page, apiPath, body);
|
|
68
|
+
const images = data.images || (data.image ? [data.image] : []);
|
|
69
|
+
if (!images.length)
|
|
70
|
+
throw new CliError('EMPTY_RESPONSE', 'No images returned', 'Try a different prompt or model');
|
|
71
|
+
const noDownload = kwargs['no-download'];
|
|
72
|
+
const outputDir = kwargs.output;
|
|
73
|
+
const results = [];
|
|
74
|
+
for (let i = 0; i < images.length; i++) {
|
|
75
|
+
const url = images[i];
|
|
76
|
+
if (noDownload) {
|
|
77
|
+
results.push({ index: i + 1, status: 'generated', file: '-', size: '-', url });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const urlPath = (() => { try {
|
|
82
|
+
return new URL(url).pathname;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return url;
|
|
86
|
+
} })();
|
|
87
|
+
const ext = urlPath.endsWith('.png') || urlPath.endsWith('.webp') ? urlPath.slice(urlPath.lastIndexOf('.')) : '.jpg';
|
|
88
|
+
const filename = `yollomi_${modelId}_${Date.now()}_${i + 1}${ext}`;
|
|
89
|
+
const { path: fp, size } = await downloadOutput(url, outputDir, filename);
|
|
90
|
+
results.push({ index: i + 1, status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
results.push({ index: i + 1, status: 'download-failed', file: '-', size: '-', url });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (data.remainingCredits !== undefined)
|
|
97
|
+
process.stderr.write(chalk.dim(`Credits remaining: ${data.remainingCredits}\n`));
|
|
98
|
+
return results;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { IMAGE_MODELS, VIDEO_MODELS, TOOL_MODELS } from './utils.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'yollomi',
|
|
5
|
+
name: 'models',
|
|
6
|
+
description: 'List available Yollomi AI models (image, video, tools)',
|
|
7
|
+
strategy: Strategy.PUBLIC,
|
|
8
|
+
browser: false,
|
|
9
|
+
args: [
|
|
10
|
+
{ name: 'type', default: 'all', choices: ['all', 'image', 'video', 'tool'], help: 'Filter by model type' },
|
|
11
|
+
],
|
|
12
|
+
columns: ['type', 'model', 'credits', 'description'],
|
|
13
|
+
func: async (_page, kwargs) => {
|
|
14
|
+
const filter = kwargs.type;
|
|
15
|
+
const rows = [];
|
|
16
|
+
if (filter === 'all' || filter === 'image') {
|
|
17
|
+
for (const [id, info] of Object.entries(IMAGE_MODELS)) {
|
|
18
|
+
rows.push({ type: 'image', model: id, credits: info.credits, description: info.description });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (filter === 'all' || filter === 'video') {
|
|
22
|
+
for (const [id, info] of Object.entries(VIDEO_MODELS)) {
|
|
23
|
+
rows.push({ type: 'video', model: id, credits: info.credits, description: info.description });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (filter === 'all' || filter === 'tool') {
|
|
27
|
+
for (const [id, info] of Object.entries(TOOL_MODELS)) {
|
|
28
|
+
rows.push({ type: 'tool', model: id, credits: info.credits, description: info.description });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return rows;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi object remover — POST /api/ai/object-remover
|
|
3
|
+
*/
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { CliError } from '../../errors.js';
|
|
8
|
+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
|
|
9
|
+
cli({
|
|
10
|
+
site: 'yollomi',
|
|
11
|
+
name: 'object-remover',
|
|
12
|
+
description: 'Remove unwanted objects from images (3 credits)',
|
|
13
|
+
domain: YOLLOMI_DOMAIN,
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'image', positional: true, required: true, help: 'Image URL' },
|
|
17
|
+
{ name: 'mask', positional: true, required: true, help: 'Mask image URL (white = area to remove)' },
|
|
18
|
+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
|
|
19
|
+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['status', 'file', 'size', 'url'],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
process.stderr.write(chalk.dim('Removing object...\n'));
|
|
24
|
+
const data = await yollomiPost(page, '/api/ai/object-remover', {
|
|
25
|
+
image: kwargs.image,
|
|
26
|
+
mask: kwargs.mask,
|
|
27
|
+
});
|
|
28
|
+
const url = data.image || (data.images?.[0]);
|
|
29
|
+
if (!url)
|
|
30
|
+
throw new CliError('EMPTY_RESPONSE', 'No result', 'Check image and mask');
|
|
31
|
+
if (kwargs['no-download'])
|
|
32
|
+
return [{ status: 'removed', file: '-', size: '-', url }];
|
|
33
|
+
try {
|
|
34
|
+
const filename = `yollomi_removed_${Date.now()}.png`;
|
|
35
|
+
const { path: fp, size } = await downloadOutput(url, kwargs.output, filename);
|
|
36
|
+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }];
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [{ status: 'download-failed', file: '-', size: '-', url }];
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi background removal — POST /api/ai/remove-bg (free, 0 credits)
|
|
3
|
+
*/
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { CliError } from '../../errors.js';
|
|
8
|
+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
|
|
9
|
+
cli({
|
|
10
|
+
site: 'yollomi',
|
|
11
|
+
name: 'remove-bg',
|
|
12
|
+
description: 'Remove image background with AI (free)',
|
|
13
|
+
domain: YOLLOMI_DOMAIN,
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'image', positional: true, required: true, help: 'Image URL to remove background from' },
|
|
17
|
+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
|
|
18
|
+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['status', 'file', 'size', 'url'],
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
process.stderr.write(chalk.dim('Removing background...\n'));
|
|
23
|
+
const data = await yollomiPost(page, '/api/ai/remove-bg', { imageUrl: kwargs.image });
|
|
24
|
+
const url = data.image || (data.images?.[0]);
|
|
25
|
+
if (!url)
|
|
26
|
+
throw new CliError('EMPTY_RESPONSE', 'No result', 'Check the input image URL');
|
|
27
|
+
if (kwargs['no-download'])
|
|
28
|
+
return [{ status: 'processed', file: '-', size: '-', url }];
|
|
29
|
+
try {
|
|
30
|
+
const filename = `yollomi_nobg_${Date.now()}.png`;
|
|
31
|
+
const { path: fp, size } = await downloadOutput(url, kwargs.output, filename);
|
|
32
|
+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }];
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [{ status: 'download-failed', file: '-', size: '-', url }];
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi photo restoration — POST /api/ai/photo-restoration
|
|
3
|
+
*/
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { CliError } from '../../errors.js';
|
|
8
|
+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
|
|
9
|
+
cli({
|
|
10
|
+
site: 'yollomi',
|
|
11
|
+
name: 'restore',
|
|
12
|
+
description: 'Restore old or damaged photos with AI (4 credits)',
|
|
13
|
+
domain: YOLLOMI_DOMAIN,
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'image', positional: true, required: true, help: 'Image URL to restore' },
|
|
17
|
+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
|
|
18
|
+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['status', 'file', 'size', 'url'],
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
process.stderr.write(chalk.dim('Restoring photo...\n'));
|
|
23
|
+
const data = await yollomiPost(page, '/api/ai/photo-restoration', { imageUrl: kwargs.image });
|
|
24
|
+
const url = data.image || (data.images?.[0]);
|
|
25
|
+
if (!url)
|
|
26
|
+
throw new CliError('EMPTY_RESPONSE', 'No result', 'Check the input image');
|
|
27
|
+
if (kwargs['no-download'])
|
|
28
|
+
return [{ status: 'restored', file: '-', size: '-', url }];
|
|
29
|
+
try {
|
|
30
|
+
const filename = `yollomi_restored_${Date.now()}.jpg`;
|
|
31
|
+
const { path: fp, size } = await downloadOutput(url, kwargs.output, filename);
|
|
32
|
+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }];
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [{ status: 'download-failed', file: '-', size: '-', url }];
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yollomi virtual try-on — POST /api/ai/virtual-try-on
|
|
3
|
+
*/
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { CliError } from '../../errors.js';
|
|
8
|
+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
|
|
9
|
+
cli({
|
|
10
|
+
site: 'yollomi',
|
|
11
|
+
name: 'try-on',
|
|
12
|
+
description: 'Virtual try-on — see how clothes look on a person (3 credits)',
|
|
13
|
+
domain: YOLLOMI_DOMAIN,
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'person', required: true, help: 'Person photo URL (upload via "opencli yollomi upload" first)' },
|
|
17
|
+
{ name: 'cloth', required: true, help: 'Clothing image URL' },
|
|
18
|
+
{ name: 'cloth-type', default: 'upper', choices: ['upper', 'lower', 'overall'], help: 'Clothing type' },
|
|
19
|
+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
|
|
20
|
+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' },
|
|
21
|
+
],
|
|
22
|
+
columns: ['status', 'file', 'size', 'url'],
|
|
23
|
+
func: async (page, kwargs) => {
|
|
24
|
+
process.stderr.write(chalk.dim('Processing virtual try-on...\n'));
|
|
25
|
+
const data = await yollomiPost(page, '/api/ai/virtual-try-on', {
|
|
26
|
+
person_image: kwargs.person,
|
|
27
|
+
cloth_image: kwargs.cloth,
|
|
28
|
+
cloth_type: kwargs['cloth-type'],
|
|
29
|
+
output_format: 'png',
|
|
30
|
+
output_quality: 100,
|
|
31
|
+
});
|
|
32
|
+
const url = data.image || (data.images?.[0]);
|
|
33
|
+
if (!url)
|
|
34
|
+
throw new CliError('EMPTY_RESPONSE', 'No result', 'Check both images have clear subjects');
|
|
35
|
+
if (kwargs['no-download'])
|
|
36
|
+
return [{ status: 'generated', file: '-', size: '-', url }];
|
|
37
|
+
try {
|
|
38
|
+
const filename = `yollomi_tryon_${Date.now()}.png`;
|
|
39
|
+
const { path: fp, size } = await downloadOutput(url, kwargs.output, filename);
|
|
40
|
+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }];
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [{ status: 'download-failed', file: '-', size: '-', url }];
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
});
|