@remixmate/cli 0.9.15 → 0.9.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/capabilities.d.ts +5 -1
- package/dist/cli.js +12 -0
- package/dist/doctor.js +21 -0
- package/dist/manifest.json +13 -13
- package/dist/project/commands.d.ts +19 -0
- package/dist/project/commands.js +156 -0
- package/dist/project/host.d.ts +77 -0
- package/dist/project/host.js +106 -0
- package/dist/project/resolve.d.ts +43 -0
- package/dist/project/resolve.js +65 -0
- package/dist/project/store.d.ts +31 -0
- package/dist/project/store.js +97 -0
- package/dist/project/take.d.ts +46 -0
- package/dist/project/take.js +102 -0
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +2 -0
- package/dist/runner.js +59 -0
- package/dist/skill-schema.d.ts +14 -0
- package/dist/skill-schema.js +15 -0
- package/package.json +1 -1
- package/skills/export-jianying/version.json +1 -1
- package/skills/gen-digital-human/skill.json +78 -11
- package/skills/gen-digital-human/version.json +1 -1
- package/skills/gen-image/SKILL.md +40 -18
- package/skills/gen-image/skill.json +71 -12
- package/skills/gen-image/version.json +1 -1
- package/skills/gen-script/SKILL.md +17 -21
- package/skills/gen-script/version.json +1 -1
- package/skills/gen-video/SKILL.md +5 -3
- package/skills/gen-video/skill.json +77 -13
- package/skills/gen-video/version.json +1 -1
- package/skills/gen-voice/SKILL.md +1 -1
- package/skills/gen-voice/skill.json +34 -8
- package/skills/gen-voice/version.json +1 -1
- package/skills/prepare-video-assets/skill.json +43 -9
- package/skills/prepare-video-assets/version.json +1 -1
- package/skills/render-video/skill.json +43 -8
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/version.json +1 -1
- package/skills/video-parser/skill.json +29 -7
- package/skills/video-parser/version.json +1 -1
- package/skills/web-record/SKILL.md +131 -133
- package/skills/web-record/skill.json +137 -33
- package/skills/web-screenshot/SKILL.md +93 -96
- package/skills/web-screenshot/skill.json +66 -16
- package/skills/web-screenshot/version.json +1 -1
package/README.md
CHANGED
|
@@ -296,7 +296,7 @@ remixmate template-registry --list-templates
|
|
|
296
296
|
### Quick start
|
|
297
297
|
|
|
298
298
|
```
|
|
299
|
-
@skills/gen-image/SKILL.md Generate an image of a panda, 9:16, using gemini
|
|
299
|
+
@skills/gen-image/SKILL.md Generate an image of a panda, 9:16, using gemini, Chinese-painting style + follow this doc strictly
|
|
300
300
|
@skills/gen-video/SKILL.md Generate a video of a panda running in a bamboo forest, 9:16, 6 seconds, using veo + follow this doc strictly
|
|
301
301
|
@skills/gen-voice/SKILL.md Generate a voiceover introducing panda habits, around 100 words + follow this doc strictly
|
|
302
302
|
@skills/gen-digital-human/SKILL.md Get the digital human list + follow this doc strictly
|
package/README.zh-CN.md
CHANGED
|
@@ -232,7 +232,7 @@ remixmate template-registry --list-templates
|
|
|
232
232
|
### 快速体验
|
|
233
233
|
|
|
234
234
|
```
|
|
235
|
-
@skills/gen-image/SKILL.md 生成一张熊猫的图片,9:16,调用gemini
|
|
235
|
+
@skills/gen-image/SKILL.md 生成一张熊猫的图片,9:16,调用gemini,国画风 + 严格按该文档执行
|
|
236
236
|
@skills/gen-video/SKILL.md 生成一段熊猫在竹林奔跑的视频,9:16,长度6秒,调用veo + 严格按该文档执行
|
|
237
237
|
@skills/gen-voice/SKILL.md 生成一段语音,介绍熊猫的习性,大概100字左右 + 严格按该文档执行
|
|
238
238
|
@skills/gen-digital-human/SKILL.md 获取数字人列表 + 严格按该文档执行
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -19,8 +19,12 @@ export interface ModelConstraints {
|
|
|
19
19
|
durationDefault?: number;
|
|
20
20
|
refImageMax?: number;
|
|
21
21
|
sizes?: string[];
|
|
22
|
-
/** aspect-ratio → [width, height] pixel preset (Seedream).
|
|
22
|
+
/** aspect-ratio → [width, height] pixel preset (Seedream). Per model: the same
|
|
23
|
+
* ratio maps to different pixels on different Seedream variants. */
|
|
23
24
|
sizePresets?: Record<string, [number, number]>;
|
|
25
|
+
/** total-pixel bounds (width × height); the backend rescales sizes outside them. */
|
|
26
|
+
pixelMin?: number;
|
|
27
|
+
pixelMax?: number;
|
|
24
28
|
}
|
|
25
29
|
export interface ModelDescriptor {
|
|
26
30
|
id: string;
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { loadSkills, SKILLS_DIR } from './registry.js';
|
|
|
15
15
|
import { parseArgv } from './argv.js';
|
|
16
16
|
import { runSkill } from './runner.js';
|
|
17
17
|
import { runAuthCommand } from './auth/commands.js';
|
|
18
|
+
import { HOST_VERB, PROJECT_VERB, runHostCommand, runProjectCommand } from './project/commands.js';
|
|
18
19
|
import { runExec } from './exec.js';
|
|
19
20
|
import { authStatusLine, runDoctor } from './doctor.js';
|
|
20
21
|
import { padDisplay } from './text.js';
|
|
@@ -56,6 +57,10 @@ function printUsage() {
|
|
|
56
57
|
process.stdout.write(' remixmate logout Remove stored credentials\n');
|
|
57
58
|
process.stdout.write(' remixmate whoami Show the current identity\n');
|
|
58
59
|
process.stdout.write(' remixmate exec -- <cmd> [args...] Run a command with credentials injected\n');
|
|
60
|
+
process.stdout.write(' remixmate project list List your projects\n');
|
|
61
|
+
process.stdout.write(' remixmate project use <prj> [--global] Bind output to a project (this dir, or globally)\n');
|
|
62
|
+
process.stdout.write(' remixmate project show Show the current project binding\n');
|
|
63
|
+
process.stdout.write(' remixmate host probe Diagnose host detection and take grouping\n');
|
|
59
64
|
process.stdout.write(' remixmate <name> [--flag value ...] Invoke a skill\n');
|
|
60
65
|
process.stdout.write(' remixmate <name> --help Show skill-specific help\n');
|
|
61
66
|
process.stdout.write('\nAvailable skills:\n');
|
|
@@ -132,6 +137,13 @@ async function main() {
|
|
|
132
137
|
const code = await runAuthCommand(argv[0], argv.slice(1));
|
|
133
138
|
process.exit(code);
|
|
134
139
|
}
|
|
140
|
+
// Project / host verbs, like the auth verbs, are resolved before skill dispatch.
|
|
141
|
+
if (argv[0] === PROJECT_VERB) {
|
|
142
|
+
process.exit(await runProjectCommand(argv.slice(1)));
|
|
143
|
+
}
|
|
144
|
+
if (argv[0] === HOST_VERB) {
|
|
145
|
+
process.exit(await runHostCommand(argv.slice(1)));
|
|
146
|
+
}
|
|
135
147
|
if (argv[0] === 'exec') {
|
|
136
148
|
// Raw argv (not parsed) — everything after `--` belongs to the child command.
|
|
137
149
|
const code = await runExec(argv.slice(1));
|
package/dist/doctor.js
CHANGED
|
@@ -17,6 +17,8 @@ import { resolveApiBaseUrl } from './http.js';
|
|
|
17
17
|
import { EXIT } from './errors.js';
|
|
18
18
|
import { resolvePrivToken } from './auth/resolve.js';
|
|
19
19
|
import { listCredentials } from './auth/credential-store.js';
|
|
20
|
+
import { resolveTakeKey } from './project/host.js';
|
|
21
|
+
import { readBinding } from './project/store.js';
|
|
20
22
|
import { canAutoAuth, isHeadless } from './auth/environment.js';
|
|
21
23
|
import { padDisplay } from './text.js';
|
|
22
24
|
/**
|
|
@@ -98,6 +100,25 @@ export async function runDoctor(opts = {}) {
|
|
|
98
100
|
detail: playwright ?? '当前解释器下未安装 —— 仅 web-screenshot / web-record 需要,首次运行会自动装',
|
|
99
101
|
});
|
|
100
102
|
checks.push({ name: '后端地址', status: 'ok', detail: apiBaseUrl });
|
|
103
|
+
// Where will this machine's renders land, and will repeated renders group?
|
|
104
|
+
// Both were previously invisible — the first sign of trouble was a finished
|
|
105
|
+
// video that appeared nowhere in the web app.
|
|
106
|
+
const binding = await readBinding(apiBaseUrl);
|
|
107
|
+
checks.push({
|
|
108
|
+
name: '项目归属',
|
|
109
|
+
status: 'ok',
|
|
110
|
+
detail: binding
|
|
111
|
+
? `${binding.projectId}(${binding.scope === 'path' ? '目录绑定' : '全局绑定'})`
|
|
112
|
+
: '未绑定 → 产物落进默认项目「未分类」(remixmate project use <prj> 可改)',
|
|
113
|
+
});
|
|
114
|
+
const take = resolveTakeKey(undefined);
|
|
115
|
+
checks.push({
|
|
116
|
+
name: 'take 分组',
|
|
117
|
+
status: take.source === 'none' ? 'warn' : 'ok',
|
|
118
|
+
detail: take.source === 'none'
|
|
119
|
+
? '宿主未提供会话 ID —— 每次渲染各自成为一个 take。可 export REMIXMATE_TAKE_KEY 手动分组,详见 remixmate host probe'
|
|
120
|
+
: `${take.key}(宿主:${take.host || 'unknown'},来源:${take.source})`,
|
|
121
|
+
});
|
|
101
122
|
const resolved = await resolvePrivToken({ apiBaseUrl });
|
|
102
123
|
const stored = await listCredentials();
|
|
103
124
|
if (resolved) {
|
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"version": "0.9.17",
|
|
4
|
+
"generatedAt": "2026-08-29T15:20:08.872Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "export-jianying",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"tier": "atomic",
|
|
55
55
|
"category": "asset",
|
|
56
56
|
"title": "AI Image Generation",
|
|
57
|
-
"summary": "AI image generation: produce an image from a text prompt. Supports Seedream and Gemini
|
|
57
|
+
"summary": "AI image generation: produce an image from a text prompt. Supports the Seedream family (including a high-fidelity 'pro' variant) and Gemini, plus image-to-image with reference images.",
|
|
58
58
|
"triggers": [
|
|
59
59
|
"AI image generation, text-to-image, \"draw me ...\", \"generate an image of ...\"",
|
|
60
60
|
"Image-to-image, reference image, style transfer, image variation",
|
|
@@ -247,11 +247,11 @@
|
|
|
247
247
|
"title": "Web Page Recording",
|
|
248
248
|
"summary": "Drive a headless browser (Playwright Python) to RECORD any URL to a video, then (by default) transcode to mp4, grab a cover frame, upload to VOD and return a playable CDN URL. Modes: fixed-duration recording, condition-triggered stop (element appears / disappears), auto-scroll from top to bottom, custom storyboards, and parameterized templates. Storyboard scenes: highlight / focus / zoom / scroll / virtual-cursor click / type / hover / caption / title-card / arrow / numbered sequence / redact / code-line highlight. For still images (png/jpg) use the web_screenshot tool instead. Use this tool whenever the user wants a video / recording / screencast of a web page: record a page, scroll-through video, page-from-top-to-bottom clip, demo of clicks/typing/hover, storyboard / multi-scene intro video, or a template-based clip.",
|
|
249
249
|
"triggers": [
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
254
|
-
"
|
|
250
|
+
"Screen recording, record a web page, record a video, capture an interaction, produce a webm, screencast",
|
|
251
|
+
"Scroll recording, a video that pans the page from top to bottom",
|
|
252
|
+
"Multi-scene video, storyboard video, timeline-sequenced clips",
|
|
253
|
+
"Animated demos of clicks / typing / hover (virtual cursor + ripple / typewriter / triggering tooltips)",
|
|
254
|
+
"One-command template clips, numbering and highlighting several regions in sequence and recording it"
|
|
255
255
|
],
|
|
256
256
|
"entry": {
|
|
257
257
|
"type": "python",
|
|
@@ -275,11 +275,11 @@
|
|
|
275
275
|
"title": "Web Page Screenshot",
|
|
276
276
|
"summary": "Drive a headless browser (Playwright Python) to capture any URL to a local STILL IMAGE (png/jpg): full-page / viewport / element / region screenshots, with device emulation, waiting, hide/mask/redact, and static annotations (highlight / arrow / caption / numbered sequence / redact). Images only — for video / recording / scroll-through / storyboard clips (webm) use the web_record tool instead.",
|
|
277
277
|
"triggers": [
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
278
|
+
"Web page screenshot, screen capture, full-page screenshot, long screenshot",
|
|
279
|
+
"Capture a specific element / region, partial screenshot, focus on an area",
|
|
280
|
+
"Hide or mask elements before capturing, annotate a still screenshot (highlight box / arrow / label)",
|
|
281
|
+
"Highlight lines L5-L20 of a file and screenshot it",
|
|
282
|
+
"Mobile / device-emulated screenshot, capture with cookies / a logged-in session"
|
|
283
283
|
],
|
|
284
284
|
"entry": {
|
|
285
285
|
"type": "python",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate project ...` and `remixmate host probe`.
|
|
3
|
+
*
|
|
4
|
+
* `project use` is the low-friction way to keep CLI output out of 「未分类」:
|
|
5
|
+
* say it once per repo (or once globally) instead of passing --project on every
|
|
6
|
+
* call — an agent won't pass a flag it wasn't told about.
|
|
7
|
+
*
|
|
8
|
+
* `host probe` exists so onboarding an unknown agent shell is a minute's work
|
|
9
|
+
* rather than a reverse-engineering session: run it inside the new host, read
|
|
10
|
+
* off the session variable, add one row to HOST_PROVIDERS.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PROJECT_VERB = "project";
|
|
13
|
+
export declare const HOST_VERB = "host";
|
|
14
|
+
export declare function runProjectCommand(argv: string[]): Promise<number>;
|
|
15
|
+
/**
|
|
16
|
+
* Report what the CLI can see about its host. Prints variable NAMES only —
|
|
17
|
+
* an environment dump is a routine way to leak tokens into a chat transcript.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runHostCommand(argv: string[]): Promise<number>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remixmate project ...` and `remixmate host probe`.
|
|
3
|
+
*
|
|
4
|
+
* `project use` is the low-friction way to keep CLI output out of 「未分类」:
|
|
5
|
+
* say it once per repo (or once globally) instead of passing --project on every
|
|
6
|
+
* call — an agent won't pass a flag it wasn't told about.
|
|
7
|
+
*
|
|
8
|
+
* `host probe` exists so onboarding an unknown agent shell is a minute's work
|
|
9
|
+
* rather than a reverse-engineering session: run it inside the new host, read
|
|
10
|
+
* off the session variable, add one row to HOST_PROVIDERS.
|
|
11
|
+
*/
|
|
12
|
+
import { EXIT, SkillError } from '../errors.js';
|
|
13
|
+
import { resolveApiBaseUrl, resolveHttpContext } from '../http.js';
|
|
14
|
+
import { detectHostLabel, probeSessionVarNames, resolveTakeKey } from './host.js';
|
|
15
|
+
import { fetchDefaultProject, listProjects } from './resolve.js';
|
|
16
|
+
import { clearBinding, readBinding, writeBinding, PROJECTS_FILE } from './store.js';
|
|
17
|
+
export const PROJECT_VERB = 'project';
|
|
18
|
+
export const HOST_VERB = 'host';
|
|
19
|
+
function projectLabel(p) {
|
|
20
|
+
return p.displayName || p.fullName || p.sourceUrl || '(未命名)';
|
|
21
|
+
}
|
|
22
|
+
async function ctxFor(sub) {
|
|
23
|
+
return await resolveHttpContext(`project-${sub}`);
|
|
24
|
+
}
|
|
25
|
+
async function cmdList() {
|
|
26
|
+
const ctx = await ctxFor('list');
|
|
27
|
+
const projects = await listProjects(ctx);
|
|
28
|
+
if (projects.length === 0) {
|
|
29
|
+
process.stdout.write('(还没有项目。渲染一次即可自动创建「未分类」)\n');
|
|
30
|
+
return EXIT.OK;
|
|
31
|
+
}
|
|
32
|
+
const bound = await readBinding(ctx.apiBaseUrl);
|
|
33
|
+
for (const p of projects) {
|
|
34
|
+
const marks = [p.isDefault ? '默认' : '', bound?.projectId === p.id ? '← 当前绑定' : '']
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join(' ');
|
|
37
|
+
process.stdout.write(` ${p.id.padEnd(20)} ${projectLabel(p)}${marks ? ` ${marks}` : ''}\n`);
|
|
38
|
+
}
|
|
39
|
+
return EXIT.OK;
|
|
40
|
+
}
|
|
41
|
+
async function cmdUse(args) {
|
|
42
|
+
const global = args.includes('--global');
|
|
43
|
+
const projectId = args.find((a) => !a.startsWith('-'));
|
|
44
|
+
if (!projectId) {
|
|
45
|
+
process.stderr.write('用法: remixmate project use <prj_xxx> [--global]\n');
|
|
46
|
+
return EXIT.USAGE;
|
|
47
|
+
}
|
|
48
|
+
const ctx = await ctxFor('use');
|
|
49
|
+
// Verify before persisting: a typo'd uid would otherwise sit in the config and
|
|
50
|
+
// silently send every later render to the default project.
|
|
51
|
+
const projects = await listProjects(ctx);
|
|
52
|
+
const match = projects.find((p) => p.id === projectId);
|
|
53
|
+
if (!match) {
|
|
54
|
+
process.stderr.write(`❌ 项目不存在或不属于当前账号: ${projectId}\n 用 remixmate project list 查看可用项目。\n`);
|
|
55
|
+
return EXIT.USAGE;
|
|
56
|
+
}
|
|
57
|
+
await writeBinding(ctx.apiBaseUrl, projectId, global ? 'default' : 'path');
|
|
58
|
+
const scopeText = global ? '全局默认' : `当前目录 (${process.cwd()})`;
|
|
59
|
+
process.stdout.write(`✅ 已绑定到 ${projectLabel(match)} [${projectId}]\n 作用域: ${scopeText}\n`);
|
|
60
|
+
if (!global) {
|
|
61
|
+
// Not a hypothetical: WorkBuddy spawns ~/WorkBuddy/<timestamp> per conversation,
|
|
62
|
+
// so a path binding made there is gone by the next conversation.
|
|
63
|
+
process.stdout.write(' 提示: 若宿主每次会话都换工作目录,请改用 --global。\n');
|
|
64
|
+
}
|
|
65
|
+
return EXIT.OK;
|
|
66
|
+
}
|
|
67
|
+
async function cmdUnuse(args) {
|
|
68
|
+
const global = args.includes('--global');
|
|
69
|
+
const apiBaseUrl = resolveApiBaseUrl();
|
|
70
|
+
const removed = await clearBinding(apiBaseUrl, global ? 'default' : 'path');
|
|
71
|
+
process.stdout.write(removed ? '✅ 绑定已清除\n' : 'ℹ️ 没有可清除的绑定\n');
|
|
72
|
+
return EXIT.OK;
|
|
73
|
+
}
|
|
74
|
+
async function cmdShow() {
|
|
75
|
+
const apiBaseUrl = resolveApiBaseUrl();
|
|
76
|
+
const bound = await readBinding(apiBaseUrl);
|
|
77
|
+
process.stdout.write(`后端: ${apiBaseUrl}\n`);
|
|
78
|
+
process.stdout.write(`配置文件: ${PROJECTS_FILE}\n`);
|
|
79
|
+
if (bound) {
|
|
80
|
+
const scope = bound.scope === 'path' ? `目录绑定 (${process.cwd()})` : '全局默认绑定';
|
|
81
|
+
process.stdout.write(`绑定项目: ${bound.projectId} [${scope}]\n`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
process.stdout.write('绑定项目: (无) — 产物会落进默认项目「未分类」\n');
|
|
85
|
+
}
|
|
86
|
+
const envOverride = (process.env.REMIXMATE_PROJECT_ID ?? '').trim();
|
|
87
|
+
if (envOverride) {
|
|
88
|
+
process.stdout.write(`⚠️ REMIXMATE_PROJECT_ID=${envOverride} 会覆盖上面的绑定\n`);
|
|
89
|
+
}
|
|
90
|
+
return EXIT.OK;
|
|
91
|
+
}
|
|
92
|
+
async function cmdDefault() {
|
|
93
|
+
const ctx = await ctxFor('default');
|
|
94
|
+
const p = await fetchDefaultProject(ctx);
|
|
95
|
+
process.stdout.write(`${p.id} ${projectLabel(p)}\n`);
|
|
96
|
+
return EXIT.OK;
|
|
97
|
+
}
|
|
98
|
+
export async function runProjectCommand(argv) {
|
|
99
|
+
const sub = argv[0] ?? 'show';
|
|
100
|
+
try {
|
|
101
|
+
switch (sub) {
|
|
102
|
+
case 'list':
|
|
103
|
+
return await cmdList();
|
|
104
|
+
case 'use':
|
|
105
|
+
return await cmdUse(argv.slice(1));
|
|
106
|
+
case 'unuse':
|
|
107
|
+
return await cmdUnuse(argv.slice(1));
|
|
108
|
+
case 'show':
|
|
109
|
+
return await cmdShow();
|
|
110
|
+
case 'default':
|
|
111
|
+
return await cmdDefault();
|
|
112
|
+
default:
|
|
113
|
+
process.stderr.write(`❌ 未知子命令: ${sub}\n用法: remixmate project <list|use|unuse|show|default>\n`);
|
|
114
|
+
return EXIT.USAGE;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (err instanceof SkillError) {
|
|
119
|
+
process.stderr.write(err.message + '\n');
|
|
120
|
+
return err.exitCode;
|
|
121
|
+
}
|
|
122
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
123
|
+
return EXIT.ERROR;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Report what the CLI can see about its host. Prints variable NAMES only —
|
|
128
|
+
* an environment dump is a routine way to leak tokens into a chat transcript.
|
|
129
|
+
*/
|
|
130
|
+
export async function runHostCommand(argv) {
|
|
131
|
+
const sub = argv[0] ?? 'probe';
|
|
132
|
+
if (sub !== 'probe') {
|
|
133
|
+
process.stderr.write(`❌ 未知子命令: ${sub}\n用法: remixmate host probe\n`);
|
|
134
|
+
return EXIT.USAGE;
|
|
135
|
+
}
|
|
136
|
+
const host = detectHostLabel();
|
|
137
|
+
const take = resolveTakeKey(undefined);
|
|
138
|
+
process.stdout.write(`识别宿主: ${host || '(未识别)'}\n`);
|
|
139
|
+
process.stdout.write(`分组键: ${take.key || '(无 — 每次渲染会各自成为一个 take)'}\n`);
|
|
140
|
+
process.stdout.write(`来源: ${take.source}\n`);
|
|
141
|
+
const names = probeSessionVarNames();
|
|
142
|
+
process.stdout.write('\n环境里疑似会话标识的变量(仅名称,不含取值):\n');
|
|
143
|
+
if (names.length === 0) {
|
|
144
|
+
process.stdout.write(' (无)\n');
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
for (const n of names)
|
|
148
|
+
process.stdout.write(` ${n}\n`);
|
|
149
|
+
}
|
|
150
|
+
if (!host && names.length > 0) {
|
|
151
|
+
process.stdout.write('\n宿主未被识别。若上面某个变量在整个会话期间保持不变,可以:\n' +
|
|
152
|
+
' 1) 临时方案: export REMIXMATE_TAKE_KEY="$该变量"\n' +
|
|
153
|
+
' 2) 长期方案: 在 src/project/host.ts 的 HOST_PROVIDERS 里加一行\n');
|
|
154
|
+
}
|
|
155
|
+
return EXIT.OK;
|
|
156
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host detection — which agent shell is this CLI running inside, and what is
|
|
3
|
+
* its conversation id?
|
|
4
|
+
*
|
|
5
|
+
* The id is used as the *grouping key* for takes: every render fired from the
|
|
6
|
+
* same host conversation lands in one VideoProject as successive takes. The
|
|
7
|
+
* creative loop ("generate → watch → shorten the intro → re-render") spans
|
|
8
|
+
* multiple skill invocations and multiple render jobs, so the conversation —
|
|
9
|
+
* not the render job — is its natural boundary.
|
|
10
|
+
*
|
|
11
|
+
* Everything here reads the **child process environment only**. We deliberately
|
|
12
|
+
* do not speak any host's private IPC (Codex exposes its thread id through an
|
|
13
|
+
* App Server JSON-RPC channel, and finding "the current thread" there means
|
|
14
|
+
* matching on cwd + status + recency — a heuristic, not an identity lookup,
|
|
15
|
+
* which would silently group takes wrong). A host that hands us nothing simply
|
|
16
|
+
* degrades one rung; see resolveTakeKey().
|
|
17
|
+
*
|
|
18
|
+
* See docs/cli-project-binding-design.md §5.1 and appendix A.
|
|
19
|
+
*/
|
|
20
|
+
export interface HostSessionProvider {
|
|
21
|
+
/** Host identifier; becomes the namespace of the grouping key. */
|
|
22
|
+
host: string;
|
|
23
|
+
/** First provider whose detect() passes wins — ORDER MATTERS, see below. */
|
|
24
|
+
detect: (env: NodeJS.ProcessEnv) => boolean;
|
|
25
|
+
/** Session-level id candidates, most-preferred first. */
|
|
26
|
+
sessionVars: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Adding a host is a data-only change: append a row, touch nothing else.
|
|
30
|
+
* Run `remixmate host probe` inside the new host to discover its variable names.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ ORDER IS LOAD-BEARING. CodeBuddy also sets `CLAUDE_SESSION_ID` (same value
|
|
33
|
+
* as its own id), while real Claude Code uses the longer `CLAUDE_CODE_*` prefix.
|
|
34
|
+
* Detecting Claude Code first — or matching on a bare `CLAUDE_` prefix — would
|
|
35
|
+
* mislabel every CodeBuddy session.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ Prefer the ROOT session over a child/sub-agent one. Claude Code exposes
|
|
38
|
+
* both (`CLAUDE_CODE_HOST_SESSION_ID` alongside a per-subagent
|
|
39
|
+
* `CLAUDE_CODE_SESSION_ID`); grouping must follow the root so a render fired
|
|
40
|
+
* from a subagent still joins the main take.
|
|
41
|
+
*/
|
|
42
|
+
export declare const HOST_PROVIDERS: HostSessionProvider[];
|
|
43
|
+
export interface HostSession {
|
|
44
|
+
host: string;
|
|
45
|
+
sessionId: string;
|
|
46
|
+
}
|
|
47
|
+
/** Identify the host and its root session id, or null when unrecognised. */
|
|
48
|
+
export declare function detectHostSession(env?: NodeJS.ProcessEnv): HostSession | null;
|
|
49
|
+
/** Host label for provenance, even when no session id could be read. */
|
|
50
|
+
export declare function detectHostLabel(env?: NodeJS.ProcessEnv): string;
|
|
51
|
+
export interface TakeKey {
|
|
52
|
+
/** `<host>:<sessionId>`, or '' when nothing groups this invocation. */
|
|
53
|
+
key: string;
|
|
54
|
+
host: string;
|
|
55
|
+
/** Which rung produced the key — surfaced by `doctor` / `host probe`. */
|
|
56
|
+
source: 'host' | 'env-override' | 'render-job' | 'none';
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the take grouping key.
|
|
60
|
+
*
|
|
61
|
+
* 1. a recognised host's root session id → "<host>:<id>"
|
|
62
|
+
* 2. REMIXMATE_TAKE_KEY → "custom:<value>"
|
|
63
|
+
* 3. the render job id → "job:<id>"
|
|
64
|
+
*
|
|
65
|
+
* Rung 2 is the escape hatch that matters: an unsupported host (or a user on
|
|
66
|
+
* Codex) can export one variable and get correct grouping without waiting for a
|
|
67
|
+
* CLI release. Rung 3 keeps the feature working everywhere, at the cost of
|
|
68
|
+
* splitting one video's iterations across takes — acceptable as a floor, not as
|
|
69
|
+
* a default.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveTakeKey(jobId: number | undefined, env?: NodeJS.ProcessEnv): TakeKey;
|
|
72
|
+
/**
|
|
73
|
+
* Environment variable names that look like a session/thread identifier.
|
|
74
|
+
* Used by `remixmate host probe` to onboard an unknown host in a minute.
|
|
75
|
+
* Returns NAMES ONLY — values may carry tokens and are never printed.
|
|
76
|
+
*/
|
|
77
|
+
export declare function probeSessionVarNames(env?: NodeJS.ProcessEnv): string[];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host detection — which agent shell is this CLI running inside, and what is
|
|
3
|
+
* its conversation id?
|
|
4
|
+
*
|
|
5
|
+
* The id is used as the *grouping key* for takes: every render fired from the
|
|
6
|
+
* same host conversation lands in one VideoProject as successive takes. The
|
|
7
|
+
* creative loop ("generate → watch → shorten the intro → re-render") spans
|
|
8
|
+
* multiple skill invocations and multiple render jobs, so the conversation —
|
|
9
|
+
* not the render job — is its natural boundary.
|
|
10
|
+
*
|
|
11
|
+
* Everything here reads the **child process environment only**. We deliberately
|
|
12
|
+
* do not speak any host's private IPC (Codex exposes its thread id through an
|
|
13
|
+
* App Server JSON-RPC channel, and finding "the current thread" there means
|
|
14
|
+
* matching on cwd + status + recency — a heuristic, not an identity lookup,
|
|
15
|
+
* which would silently group takes wrong). A host that hands us nothing simply
|
|
16
|
+
* degrades one rung; see resolveTakeKey().
|
|
17
|
+
*
|
|
18
|
+
* See docs/cli-project-binding-design.md §5.1 and appendix A.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Adding a host is a data-only change: append a row, touch nothing else.
|
|
22
|
+
* Run `remixmate host probe` inside the new host to discover its variable names.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ ORDER IS LOAD-BEARING. CodeBuddy also sets `CLAUDE_SESSION_ID` (same value
|
|
25
|
+
* as its own id), while real Claude Code uses the longer `CLAUDE_CODE_*` prefix.
|
|
26
|
+
* Detecting Claude Code first — or matching on a bare `CLAUDE_` prefix — would
|
|
27
|
+
* mislabel every CodeBuddy session.
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ Prefer the ROOT session over a child/sub-agent one. Claude Code exposes
|
|
30
|
+
* both (`CLAUDE_CODE_HOST_SESSION_ID` alongside a per-subagent
|
|
31
|
+
* `CLAUDE_CODE_SESSION_ID`); grouping must follow the root so a render fired
|
|
32
|
+
* from a subagent still joins the main take.
|
|
33
|
+
*/
|
|
34
|
+
export const HOST_PROVIDERS = [
|
|
35
|
+
{
|
|
36
|
+
host: 'codebuddy',
|
|
37
|
+
detect: (e) => !!e.CODEBUDDY_SESSION_ID || e.CODEBUDDY_HOST != null,
|
|
38
|
+
sessionVars: ['CODEBUDDY_SESSION_ID'],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
host: 'claude-code',
|
|
42
|
+
detect: (e) => e.CLAUDECODE === '1' || !!e.CLAUDE_CODE_SESSION_ID,
|
|
43
|
+
sessionVars: ['CLAUDE_CODE_HOST_SESSION_ID', 'CLAUDE_CODE_SESSION_ID'],
|
|
44
|
+
},
|
|
45
|
+
// Untested placeholders — verify with `remixmate host probe` before trusting.
|
|
46
|
+
{ host: 'qoder', detect: (e) => !!e.QODER_SESSION_ID, sessionVars: ['QODER_SESSION_ID'] },
|
|
47
|
+
{ host: 'trae', detect: (e) => !!e.TRAE_SESSION_ID, sessionVars: ['TRAE_SESSION_ID'] },
|
|
48
|
+
];
|
|
49
|
+
/** Identify the host and its root session id, or null when unrecognised. */
|
|
50
|
+
export function detectHostSession(env = process.env) {
|
|
51
|
+
for (const provider of HOST_PROVIDERS) {
|
|
52
|
+
if (!provider.detect(env))
|
|
53
|
+
continue;
|
|
54
|
+
for (const name of provider.sessionVars) {
|
|
55
|
+
const value = (env[name] ?? '').trim();
|
|
56
|
+
if (value)
|
|
57
|
+
return { host: provider.host, sessionId: value };
|
|
58
|
+
}
|
|
59
|
+
// Recognised the host but it gave us no id — don't fall through to a later
|
|
60
|
+
// provider, that would mislabel. Report the host, leave grouping to a lower rung.
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/** Host label for provenance, even when no session id could be read. */
|
|
66
|
+
export function detectHostLabel(env = process.env) {
|
|
67
|
+
return HOST_PROVIDERS.find((p) => p.detect(env))?.host ?? '';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the take grouping key.
|
|
71
|
+
*
|
|
72
|
+
* 1. a recognised host's root session id → "<host>:<id>"
|
|
73
|
+
* 2. REMIXMATE_TAKE_KEY → "custom:<value>"
|
|
74
|
+
* 3. the render job id → "job:<id>"
|
|
75
|
+
*
|
|
76
|
+
* Rung 2 is the escape hatch that matters: an unsupported host (or a user on
|
|
77
|
+
* Codex) can export one variable and get correct grouping without waiting for a
|
|
78
|
+
* CLI release. Rung 3 keeps the feature working everywhere, at the cost of
|
|
79
|
+
* splitting one video's iterations across takes — acceptable as a floor, not as
|
|
80
|
+
* a default.
|
|
81
|
+
*/
|
|
82
|
+
export function resolveTakeKey(jobId, env = process.env) {
|
|
83
|
+
const host = detectHostLabel(env);
|
|
84
|
+
const detected = detectHostSession(env);
|
|
85
|
+
if (detected) {
|
|
86
|
+
return { key: `${detected.host}:${detected.sessionId}`, host: detected.host, source: 'host' };
|
|
87
|
+
}
|
|
88
|
+
const override = (env.REMIXMATE_TAKE_KEY ?? '').trim();
|
|
89
|
+
if (override)
|
|
90
|
+
return { key: `custom:${override}`, host, source: 'env-override' };
|
|
91
|
+
if (jobId && jobId > 0)
|
|
92
|
+
return { key: `job:${jobId}`, host, source: 'render-job' };
|
|
93
|
+
return { key: '', host, source: 'none' };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Environment variable names that look like a session/thread identifier.
|
|
97
|
+
* Used by `remixmate host probe` to onboard an unknown host in a minute.
|
|
98
|
+
* Returns NAMES ONLY — values may carry tokens and are never printed.
|
|
99
|
+
*/
|
|
100
|
+
export function probeSessionVarNames(env = process.env) {
|
|
101
|
+
const interesting = /(SESSION|THREAD|CONVERSATION|CHAT|TASK)/i;
|
|
102
|
+
const secretish = /(TOKEN|KEY|SECRET|AUTH|PASSWORD|COOKIE|CREDENTIAL)/i;
|
|
103
|
+
return Object.keys(env)
|
|
104
|
+
.filter((name) => interesting.test(name) && !secretish.test(name))
|
|
105
|
+
.sort();
|
|
106
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project resolution ladder — which Project does this invocation's output
|
|
3
|
+
* belong to? First rung that matches wins.
|
|
4
|
+
*
|
|
5
|
+
* 0. CONVERSATION_ID already set → do nothing at all (cloud ab-agent path)
|
|
6
|
+
* 1. --project prj_xxx → explicit flag
|
|
7
|
+
* 2. REMIXMATE_PROJECT_ID → explicit env
|
|
8
|
+
* 3. ~/.config/remixmate/projects.json → `project use` binding (byPath → default)
|
|
9
|
+
* 4. the default project 「未分类」 → lazily created server-side
|
|
10
|
+
*
|
|
11
|
+
* Only "explicit" and "fallback" — there is deliberately no inference step in
|
|
12
|
+
* between. Deriving a project from the git remote reads well until you notice
|
|
13
|
+
* it *creates* one on every unfamiliar repo (`/project/create` is an upsert),
|
|
14
|
+
* and that its rules have to keep growing for private remotes, non-GitHub
|
|
15
|
+
* remotes, monorepos and repos with no origin — each misfire silently filing
|
|
16
|
+
* output under a project the user never made.
|
|
17
|
+
*
|
|
18
|
+
* See docs/cli-project-binding-design.md §3.
|
|
19
|
+
*/
|
|
20
|
+
import { type HttpContext } from '../http.js';
|
|
21
|
+
export interface ResolvedProject {
|
|
22
|
+
projectId: string;
|
|
23
|
+
source: 'flag' | 'env' | 'binding-path' | 'binding-default' | 'default-project';
|
|
24
|
+
}
|
|
25
|
+
/** Minimal shape of ab-api's ProjectVO — only what the ladder needs. */
|
|
26
|
+
export interface ProjectSummary {
|
|
27
|
+
id: string;
|
|
28
|
+
displayName?: string;
|
|
29
|
+
fullName?: string;
|
|
30
|
+
sourceUrl?: string;
|
|
31
|
+
isDefault?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** POST /project/default — get-or-lazily-create 「未分类」. */
|
|
34
|
+
export declare function fetchDefaultProject(ctx: HttpContext): Promise<ProjectSummary>;
|
|
35
|
+
/** POST /project/list — for `remixmate project list`. */
|
|
36
|
+
export declare function listProjects(ctx: HttpContext): Promise<ProjectSummary[]>;
|
|
37
|
+
export declare function resolveProject(ctx: HttpContext, opts?: {
|
|
38
|
+
flagProjectId?: string;
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
cwd?: string;
|
|
41
|
+
}): Promise<ResolvedProject>;
|
|
42
|
+
/** Human-readable label for `doctor` / `project show`. */
|
|
43
|
+
export declare function describeProjectSource(source: ResolvedProject['source']): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project resolution ladder — which Project does this invocation's output
|
|
3
|
+
* belong to? First rung that matches wins.
|
|
4
|
+
*
|
|
5
|
+
* 0. CONVERSATION_ID already set → do nothing at all (cloud ab-agent path)
|
|
6
|
+
* 1. --project prj_xxx → explicit flag
|
|
7
|
+
* 2. REMIXMATE_PROJECT_ID → explicit env
|
|
8
|
+
* 3. ~/.config/remixmate/projects.json → `project use` binding (byPath → default)
|
|
9
|
+
* 4. the default project 「未分类」 → lazily created server-side
|
|
10
|
+
*
|
|
11
|
+
* Only "explicit" and "fallback" — there is deliberately no inference step in
|
|
12
|
+
* between. Deriving a project from the git remote reads well until you notice
|
|
13
|
+
* it *creates* one on every unfamiliar repo (`/project/create` is an upsert),
|
|
14
|
+
* and that its rules have to keep growing for private remotes, non-GitHub
|
|
15
|
+
* remotes, monorepos and repos with no origin — each misfire silently filing
|
|
16
|
+
* output under a project the user never made.
|
|
17
|
+
*
|
|
18
|
+
* See docs/cli-project-binding-design.md §3.
|
|
19
|
+
*/
|
|
20
|
+
import { mmPost } from '../http.js';
|
|
21
|
+
import { readBinding } from './store.js';
|
|
22
|
+
/** POST /project/default — get-or-lazily-create 「未分类」. */
|
|
23
|
+
export async function fetchDefaultProject(ctx) {
|
|
24
|
+
return await mmPost(ctx, '/project/default', {});
|
|
25
|
+
}
|
|
26
|
+
/** POST /project/list — for `remixmate project list`. */
|
|
27
|
+
export async function listProjects(ctx) {
|
|
28
|
+
const data = await mmPost(ctx, '/project/list', {});
|
|
29
|
+
if (Array.isArray(data))
|
|
30
|
+
return data;
|
|
31
|
+
return data?.list ?? [];
|
|
32
|
+
}
|
|
33
|
+
export async function resolveProject(ctx, opts = {}) {
|
|
34
|
+
const env = opts.env ?? process.env;
|
|
35
|
+
const flag = (opts.flagProjectId ?? '').trim();
|
|
36
|
+
if (flag)
|
|
37
|
+
return { projectId: flag, source: 'flag' };
|
|
38
|
+
const fromEnv = (env.REMIXMATE_PROJECT_ID ?? '').trim();
|
|
39
|
+
if (fromEnv)
|
|
40
|
+
return { projectId: fromEnv, source: 'env' };
|
|
41
|
+
const bound = await readBinding(ctx.apiBaseUrl, opts.cwd);
|
|
42
|
+
if (bound) {
|
|
43
|
+
return {
|
|
44
|
+
projectId: bound.projectId,
|
|
45
|
+
source: bound.scope === 'path' ? 'binding-path' : 'binding-default',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const fallback = await fetchDefaultProject(ctx);
|
|
49
|
+
return { projectId: fallback.id, source: 'default-project' };
|
|
50
|
+
}
|
|
51
|
+
/** Human-readable label for `doctor` / `project show`. */
|
|
52
|
+
export function describeProjectSource(source) {
|
|
53
|
+
switch (source) {
|
|
54
|
+
case 'flag':
|
|
55
|
+
return '--project 参数';
|
|
56
|
+
case 'env':
|
|
57
|
+
return 'REMIXMATE_PROJECT_ID 环境变量';
|
|
58
|
+
case 'binding-path':
|
|
59
|
+
return `${process.cwd()} 的目录绑定`;
|
|
60
|
+
case 'binding-default':
|
|
61
|
+
return '全局默认绑定';
|
|
62
|
+
case 'default-project':
|
|
63
|
+
return '默认项目「未分类」';
|
|
64
|
+
}
|
|
65
|
+
}
|