@mmmbuto/nexuscrew 0.8.24 → 0.8.26
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 +764 -0
- package/README.md +38 -7
- package/frontend/dist/assets/index-DkIPrmX6.js +91 -0
- package/frontend/dist/assets/index-DqG-mDnV.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/doctor.js +35 -0
- package/lib/fleet/catalogs/alibaba-token-plan-qwen3.8.json +51 -0
- package/lib/fleet/managed.js +106 -10
- package/lib/fleet/routes.js +4 -1
- package/lib/mcp/server.js +80 -19
- package/lib/mcp/tools.js +57 -8
- package/package.json +3 -2
- package/skills/alibaba-token-media/SKILL.md +133 -0
- package/skills/alibaba-token-media/agents/openai.yaml +4 -0
- package/skills/alibaba-token-media/references/api-contract.md +97 -0
- package/skills/alibaba-token-media/scripts/alibaba_token_media.py +550 -0
- package/frontend/dist/assets/index-77r8nzQf.css +0 -32
- package/frontend/dist/assets/index-CU9OOtL-.js +0 -91
package/lib/mcp/server.js
CHANGED
|
@@ -25,7 +25,7 @@ const { loadConfig } = require('../config.js');
|
|
|
25
25
|
const { readTokenSafe } = require('../auth/token.js');
|
|
26
26
|
const { isValidSession } = require('../files/store.js');
|
|
27
27
|
const VERSION = require('../../package.json').version;
|
|
28
|
-
const { TOOLS } = require('./tools.js');
|
|
28
|
+
const { TOOLS, IDENTITY_CODE, IDENTITY_REMEDIATION } = require('./tools.js');
|
|
29
29
|
const cells = require('./cells.js');
|
|
30
30
|
|
|
31
31
|
// Versione protocollo di fallback se il client non ne dichiara una valida.
|
|
@@ -39,27 +39,83 @@ const METHOD_NOT_FOUND = -32601;
|
|
|
39
39
|
const INVALID_PARAMS = -32602;
|
|
40
40
|
|
|
41
41
|
// --- identita' cella mittente ------------------------------------------------
|
|
42
|
-
// Ordine (design §1): $TMUX presente -> `tmux display-message -p '#S'`
|
|
43
|
-
// sessione reale); fallback env NEXUSCREW_MCP_SESSION;
|
|
44
|
-
// che RICHIEDONO la sessione
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
// Ordine (design §1, INVARIATO): $TMUX presente -> `tmux display-message -p '#S'`
|
|
43
|
+
// (nome sessione reale); se fallisce/invalida -> fallback env NEXUSCREW_MCP_SESSION;
|
|
44
|
+
// altrimenti null. I tool che RICHIEDONO la sessione restano fail-closed.
|
|
45
|
+
// execFile argv diretto: mai shell.
|
|
46
|
+
//
|
|
47
|
+
// `resolveIdentity` rende OSSERVABILE la sorgente della risoluzione (P0):
|
|
48
|
+
// ritorna { session, source, code, envPresence, requiredEnvVars, remediation }
|
|
49
|
+
// senza cambiare la precedenza e senza esporre valori/segreti. `resolveSession`
|
|
50
|
+
// resta il wrapper pubblico Promise<string|null> invariato (compatibilita').
|
|
51
|
+
const IDENTITY_REQUIRED_ENV_VARS = Object.freeze(['TMUX', 'TMUX_PANE', 'NEXUSCREW_MCP_SESSION']);
|
|
52
|
+
|
|
53
|
+
function envPresenceOf(env) {
|
|
54
|
+
return {
|
|
55
|
+
TMUX: !!env.TMUX,
|
|
56
|
+
TMUX_PANE: !!env.TMUX_PANE,
|
|
57
|
+
NEXUSCREW_MCP_SESSION: !!(typeof env.NEXUSCREW_MCP_SESSION === 'string' && env.NEXUSCREW_MCP_SESSION.trim()),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveIdentity({ env, tmuxBin, execFileImpl }) {
|
|
62
|
+
const e = env || {};
|
|
63
|
+
const envPresence = envPresenceOf(e);
|
|
64
|
+
const fallbackRaw = e.NEXUSCREW_MCP_SESSION;
|
|
65
|
+
const fallbackPresent = typeof fallbackRaw === 'string' && fallbackRaw.trim().length > 0;
|
|
66
|
+
const tmuxPresent = !!e.TMUX;
|
|
67
|
+
|
|
68
|
+
// Prova il fallback NEXUSCREW_MCP_SESSION: ritorna la sessione normalizzata se
|
|
69
|
+
// valida, `false` se presente ma invalida, `null` se assente.
|
|
70
|
+
const tryFallback = () => {
|
|
71
|
+
if (!fallbackPresent) return null;
|
|
72
|
+
const s = fallbackRaw.trim();
|
|
73
|
+
return isValidSession(s) ? s : false;
|
|
50
74
|
};
|
|
51
|
-
|
|
75
|
+
|
|
76
|
+
// code quando NON identificati: INVALID se c'e' un segnale di identita'
|
|
77
|
+
// (TMUX o NEXUSCREW_MCP_SESSION presente), MISSING altrimenti.
|
|
78
|
+
const codeWhenMissing = () => ((tmuxPresent || fallbackPresent)
|
|
79
|
+
? IDENTITY_CODE.INVALID : IDENTITY_CODE.MISSING);
|
|
80
|
+
|
|
81
|
+
const ok = (session, source) => ({
|
|
82
|
+
session, source, code: IDENTITY_CODE.OK,
|
|
83
|
+
envPresence, requiredEnvVars: IDENTITY_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
|
|
84
|
+
});
|
|
85
|
+
const missing = () => ({
|
|
86
|
+
session: null, source: 'missing', code: codeWhenMissing(),
|
|
87
|
+
envPresence, requiredEnvVars: IDENTITY_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
|
|
88
|
+
});
|
|
89
|
+
|
|
52
90
|
return new Promise((resolve) => {
|
|
91
|
+
if (!tmuxPresent) {
|
|
92
|
+
const fb = tryFallback();
|
|
93
|
+
return resolve(typeof fb === 'string' ? ok(fb, 'NEXUSCREW_MCP_SESSION') : missing());
|
|
94
|
+
}
|
|
53
95
|
try {
|
|
54
96
|
execFileImpl(tmuxBin, ['display-message', '-p', '#S'], { timeout: 3000 }, (err, stdout) => {
|
|
55
|
-
if (err)
|
|
56
|
-
|
|
57
|
-
|
|
97
|
+
if (!err) {
|
|
98
|
+
const name = String(stdout || '').trim();
|
|
99
|
+
if (isValidSession(name)) return resolve(ok(name, 'tmux'));
|
|
100
|
+
}
|
|
101
|
+
// tmux fallito/invalido -> precedenza preservata: fallback env.
|
|
102
|
+
const fb = tryFallback();
|
|
103
|
+
resolve(typeof fb === 'string' ? ok(fb, 'NEXUSCREW_MCP_SESSION') : missing());
|
|
58
104
|
});
|
|
59
|
-
} catch (_) {
|
|
105
|
+
} catch (_) {
|
|
106
|
+
const fb = tryFallback();
|
|
107
|
+
resolve(typeof fb === 'string' ? ok(fb, 'NEXUSCREW_MCP_SESSION') : missing());
|
|
108
|
+
}
|
|
60
109
|
});
|
|
61
110
|
}
|
|
62
111
|
|
|
112
|
+
// Wrapper pubblico STORICO: stessi parametri, stesso return Promise<string|null>.
|
|
113
|
+
// Mantiene i test esistenti e ogni chiamante esterno che dipende solo dal nome
|
|
114
|
+
// della sessione (o null). La diagnostica source/code vive in resolveIdentity.
|
|
115
|
+
function resolveSession(opts) {
|
|
116
|
+
return resolveIdentity(opts).then((i) => i.session);
|
|
117
|
+
}
|
|
118
|
+
|
|
63
119
|
// --- server --------------------------------------------------------------------
|
|
64
120
|
function createMcpServer(opts = {}) {
|
|
65
121
|
const input = opts.input || process.stdin;
|
|
@@ -75,11 +131,15 @@ function createMcpServer(opts = {}) {
|
|
|
75
131
|
const baseUrl = `http://127.0.0.1:${cfg.port}`;
|
|
76
132
|
|
|
77
133
|
// Identita' risolta una volta e cacheata (la sessione tmux non cambia a runtime).
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
134
|
+
// Una sola risoluzione condivisa: `identity()` per la diagnostica completa
|
|
135
|
+
// (source/code/presence), `session()` estrae solo il nome per gli handler
|
|
136
|
+
// storici (compatibilita'). Nessuna API/token coinvolta qui.
|
|
137
|
+
let identityP = null;
|
|
138
|
+
const identity = () => {
|
|
139
|
+
if (!identityP) identityP = resolveIdentity({ env, tmuxBin: cfg.tmuxBin || 'tmux', execFileImpl });
|
|
140
|
+
return identityP;
|
|
82
141
|
};
|
|
142
|
+
const session = () => identity().then((i) => i.session);
|
|
83
143
|
|
|
84
144
|
// Token letto ad OGNI chiamata (rotazione-friendly), MAI incluso negli errori.
|
|
85
145
|
function readToken() {
|
|
@@ -113,6 +173,7 @@ function createMcpServer(opts = {}) {
|
|
|
113
173
|
|
|
114
174
|
const ctx = {
|
|
115
175
|
session,
|
|
176
|
+
identity,
|
|
116
177
|
api,
|
|
117
178
|
home: () => env.HOME || os.homedir(),
|
|
118
179
|
fileExists: (p) => { try { return require('node:fs').statSync(p).isFile(); } catch (_) { return false; } },
|
|
@@ -252,7 +313,7 @@ function startMcp(opts = {}) {
|
|
|
252
313
|
}
|
|
253
314
|
|
|
254
315
|
module.exports = {
|
|
255
|
-
createMcpServer, startMcp, resolveSession, TOOLS,
|
|
316
|
+
createMcpServer, startMcp, resolveSession, resolveIdentity, TOOLS,
|
|
256
317
|
parseCellTarget: cells.parseCellTarget,
|
|
257
318
|
normalizeCellPayload: cells.normalizeCellPayload,
|
|
258
319
|
readCellDirectory: cells.readCellDirectory,
|
package/lib/mcp/tools.js
CHANGED
|
@@ -27,10 +27,33 @@ function argString(args, key, { required = false, max = 4096 } = {}) {
|
|
|
27
27
|
return v;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// Codici stabili di identita' MCP (contratto P0). Valori EXACT, non sensibili,
|
|
31
|
+
// usati sia nella diagnostica read-only (`nc_identity`) sia nel messaggio umano
|
|
32
|
+
// dei tool identity-gated (isError=true preservato dal server). Non espongono
|
|
33
|
+
// valori/env/sessioni: solo la categoria del problema.
|
|
34
|
+
const IDENTITY_CODE = Object.freeze({
|
|
35
|
+
OK: 'OK',
|
|
36
|
+
MISSING: 'NEXUSCREW_MCP_IDENTITY_MISSING',
|
|
37
|
+
INVALID: 'NEXUSCREW_MCP_IDENTITY_INVALID',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Remediation senza segreti/valori: nomi soltanto, compatibile con
|
|
41
|
+
// `codex-vl mcp add --env-var` (allowlist di nomi, nessun valore persistito).
|
|
42
|
+
const IDENTITY_REMEDIATION =
|
|
43
|
+
'Nei client che ripuliscono l\'ambiente, allowlista i nomi delle variabili di identita nel server MCP stdio '
|
|
44
|
+
+ '(codex-vl mcp add ... --env-var NEXUSCREW_MCP_SESSION --env-var TMUX --env-var TMUX_PANE) '
|
|
45
|
+
+ 'oppure assicurati che il client MCP inoltri il contesto tmux al processo child: nessun valore '
|
|
46
|
+
+ 'viene copiato nella CLI o nel file di configurazione.';
|
|
47
|
+
|
|
48
|
+
function requireSession(session, tool, code = IDENTITY_CODE.MISSING) {
|
|
31
49
|
if (session) return session;
|
|
50
|
+
const stableCode = code === IDENTITY_CODE.INVALID ? IDENTITY_CODE.INVALID : IDENTITY_CODE.MISSING;
|
|
51
|
+
// Errore umano per il modello: messaggio chiaro + codice stabile fra parentesi
|
|
52
|
+
// quadre. isError=true e' impostato dal server (toolsCall); qui si propaga
|
|
53
|
+
// solo il testo. La regex storica /NEXUSCREW_MCP_SESSION/ resta soddisfatta.
|
|
32
54
|
throw new Error(
|
|
33
|
-
`${tool}: sessione tmux non identificata — serve $TMUX (dentro tmux) o
|
|
55
|
+
`${tool}: sessione tmux non identificata — serve $TMUX (dentro tmux) o `
|
|
56
|
+
+ `NEXUSCREW_MCP_SESSION [${stableCode}]`,
|
|
34
57
|
);
|
|
35
58
|
}
|
|
36
59
|
|
|
@@ -79,7 +102,8 @@ const TOOLS = [
|
|
|
79
102
|
if (args.options !== undefined && !Array.isArray(args.options)) {
|
|
80
103
|
throw new Error('options deve essere un array di stringhe');
|
|
81
104
|
}
|
|
82
|
-
const
|
|
105
|
+
const identity = await ctx.identity();
|
|
106
|
+
const session = requireSession(identity.session, 'nc_ask', identity.code);
|
|
83
107
|
const j = await ctx.api('POST', '/api/asks', {
|
|
84
108
|
question, ...(args.options ? { options: args.options } : {}), session,
|
|
85
109
|
});
|
|
@@ -109,7 +133,8 @@ const TOOLS = [
|
|
|
109
133
|
if (!ctx.fileExists(p)) throw new Error(`file inesistente: ${p}`);
|
|
110
134
|
const home = ctx.home();
|
|
111
135
|
if (p !== home && !p.startsWith(home + path.sep)) throw new Error('path fuori da HOME');
|
|
112
|
-
const
|
|
136
|
+
const identity = await ctx.identity();
|
|
137
|
+
const session = requireSession(identity.session, 'nc_send_file', identity.code);
|
|
113
138
|
const j = await ctx.api('POST', '/api/files/outbox', {
|
|
114
139
|
session, path: p, ...(caption ? { caption } : {}),
|
|
115
140
|
});
|
|
@@ -145,7 +170,8 @@ const TOOLS = [
|
|
|
145
170
|
inputSchema: { type: 'object', properties: {} },
|
|
146
171
|
annotations: { readOnlyHint: true },
|
|
147
172
|
async handler(_args, ctx) {
|
|
148
|
-
const
|
|
173
|
+
const identity = await ctx.identity();
|
|
174
|
+
const tmuxSession = requireSession(identity.session, 'nc_deck', identity.code);
|
|
149
175
|
const [config, topology, localDecks] = await Promise.all([
|
|
150
176
|
ctx.api('GET', '/api/config'), ctx.api('GET', '/api/topology'), ctx.api('GET', '/api/decks'),
|
|
151
177
|
]);
|
|
@@ -257,7 +283,8 @@ const TOOLS = [
|
|
|
257
283
|
if (code === 9 || code === 10 || code === 13) continue;
|
|
258
284
|
if (code < 32 || code === 127) throw new Error('message contiene caratteri di controllo non ammessi');
|
|
259
285
|
}
|
|
260
|
-
const
|
|
286
|
+
const identity = await ctx.identity();
|
|
287
|
+
const callerSession = requireSession(identity.session, 'nc_send_cell', identity.code);
|
|
261
288
|
const directory = await readCellDirectory(ctx, callerSession);
|
|
262
289
|
const sender = directory.cells.find((cell) => cell.self && cell.active);
|
|
263
290
|
if (!sender) throw new Error('nc_send_cell: la sessione chiamante non e\' una cella Fleet attiva locale');
|
|
@@ -290,11 +317,33 @@ const TOOLS = [
|
|
|
290
317
|
inputSchema: { type: 'object', properties: {} },
|
|
291
318
|
annotations: { readOnlyHint: true },
|
|
292
319
|
async handler(_args, ctx) {
|
|
293
|
-
const
|
|
320
|
+
const identity = await ctx.identity();
|
|
321
|
+
const session = requireSession(identity.session, 'nc_inbox', identity.code);
|
|
294
322
|
const j = await ctx.api('GET', `/api/files?session=${encodeURIComponent(session)}`);
|
|
295
323
|
return { inbox: Array.isArray(j.inbox) ? j.inbox : [] };
|
|
296
324
|
},
|
|
297
325
|
},
|
|
326
|
+
{
|
|
327
|
+
name: 'nc_identity',
|
|
328
|
+
description: 'Diagnostica read-only dell\'identita\' del chiamante MCP. Utilizzabile anche senza sessione tmux e senza token: restituisce SOLO dati non sensibili (presence delle env var, sorgente della risoluzione, codice stabile). Non chiama API HTTP e non legge il token. Usa questo tool quando gli altri tool nc_* falliscono con NEXUSCREW_MCP_IDENTITY_* per capire cosa manca.',
|
|
329
|
+
inputSchema: { type: 'object', properties: {} },
|
|
330
|
+
annotations: { readOnlyHint: true },
|
|
331
|
+
async handler(_args, ctx) {
|
|
332
|
+
const id = await ctx.identity();
|
|
333
|
+
// Output bounded e non sensibile: nessun valore/env, solo presence e codice.
|
|
334
|
+
// `session` solo se validata; `source` sempre fra i tre valori ammessi.
|
|
335
|
+
const out = {
|
|
336
|
+
identified: !!id.session,
|
|
337
|
+
source: id.source,
|
|
338
|
+
envPresence: id.envPresence,
|
|
339
|
+
requiredEnvVars: id.requiredEnvVars,
|
|
340
|
+
code: id.code,
|
|
341
|
+
remediation: id.remediation,
|
|
342
|
+
};
|
|
343
|
+
if (id.session) out.session = id.session;
|
|
344
|
+
return out;
|
|
345
|
+
},
|
|
346
|
+
},
|
|
298
347
|
];
|
|
299
348
|
|
|
300
|
-
module.exports = { TOOLS, argString, requireSession };
|
|
349
|
+
module.exports = { TOOLS, argString, requireSession, IDENTITY_CODE, IDENTITY_REMEDIATION };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmmbuto/nexuscrew",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.26",
|
|
4
4
|
"description": "Faithful browser tmux client — attach to live sessions over a real PTY, localhost-only, mobile-easy",
|
|
5
5
|
"main": "lib/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"skills/",
|
|
13
13
|
"frontend/dist/",
|
|
14
14
|
"frontend/index.html",
|
|
15
|
+
"CHANGELOG.md",
|
|
15
16
|
"LICENSE",
|
|
16
17
|
"README.md",
|
|
17
18
|
"NOTICE"
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
"scripts": {
|
|
20
21
|
"start": "node bin/nexuscrew.js serve",
|
|
21
22
|
"dev": "node bin/nexuscrew.js serve",
|
|
22
|
-
"build": "
|
|
23
|
+
"build": "npm --prefix frontend ci && npm --prefix frontend run build",
|
|
23
24
|
"test": "node tests/run-isolated.js && npm --prefix frontend test"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: alibaba-token-media
|
|
3
|
+
description: "Generate or edit images with Wan 2.7 and create videos with HappyHorse through an Alibaba Qwen Cloud Token Plan Personal subscription. Use when the user asks Claude Code, Codex, Codex-VL, or Pi to draw, generate, or edit an image with wan2.7-image or wan2.7-image-pro; create text-to-video or image-to-video media with happyhorse-1.1-t2v or happyhorse-1.1-i2v; inspect a submitted HappyHorse task; or validate the Token Plan media setup. Do not use this skill for chat, visual understanding, batch automation, backend services, or pay-as-you-go DashScope APIs."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Alibaba Token Media
|
|
7
|
+
|
|
8
|
+
Use the bundled, dependency-free CLI to access only the media models included
|
|
9
|
+
with the Lite guardrails of Alibaba Token Plan Personal. The same skill and Python script work
|
|
10
|
+
with Claude Code, Codex, Codex-VL, and Pi. Invoke the script directly from Pi;
|
|
11
|
+
do not assume that Pi provides a native MCP client.
|
|
12
|
+
|
|
13
|
+
Keep this workflow interactive. Every generation consumes plan Credits, and
|
|
14
|
+
the plan is not for unattended batch or backend workloads.
|
|
15
|
+
|
|
16
|
+
## Safety contract
|
|
17
|
+
|
|
18
|
+
- Read the credential only from `ALIBABA_CODE_API_KEY`. Never request it in
|
|
19
|
+
chat, pass it on the command line, print it, persist it, or send it through
|
|
20
|
+
an agent bus.
|
|
21
|
+
- Use only the fixed Token Plan host and exact model allowlist implemented by
|
|
22
|
+
the script. Never fall back to DashScope pay-as-you-go endpoints.
|
|
23
|
+
- Run `status` and then a `--dry-run` before the first real request in a
|
|
24
|
+
session. Dry runs never require or reveal the key.
|
|
25
|
+
- Show the user the normalized model, operation, size or duration, resolution,
|
|
26
|
+
and confirmation flags before spending Credits. A real image request needs
|
|
27
|
+
`--confirm-credit-use`; a real video request additionally needs
|
|
28
|
+
`--confirm-expensive`.
|
|
29
|
+
- Require `--confirm-high-cost` for Wan Pro, 2K or 4K images, 1080P video, or
|
|
30
|
+
video longer than five seconds. Keep Lite defaults at one 1K base image or
|
|
31
|
+
one 720P three-second video.
|
|
32
|
+
- Never batch, enable sequential image sets, retry a generation POST
|
|
33
|
+
automatically, or run more than one generation submit concurrently. A task
|
|
34
|
+
status GET may be retried, but never with a tight polling loop.
|
|
35
|
+
- Accept local input only as a regular, non-symlink JPEG, PNG, BMP, or WEBP
|
|
36
|
+
file below 20 MiB and under the current user's home. Image-to-video accepts
|
|
37
|
+
JPEG, PNG, or WEBP first frames, not BMP. Accept remote input only
|
|
38
|
+
over public HTTPS; reject localhost, private IP literals, credentials, and
|
|
39
|
+
non-standard ports.
|
|
40
|
+
- Download signed result URLs immediately. Never paste signed URLs into chat,
|
|
41
|
+
logs, source files, or an agent bus; they expire after 24 hours.
|
|
42
|
+
|
|
43
|
+
## Locate the CLI
|
|
44
|
+
|
|
45
|
+
Resolve `scripts/alibaba_token_media.py` relative to this `SKILL.md`, then run
|
|
46
|
+
it with Python 3:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python3 <skill-dir>/scripts/alibaba_token_media.py status
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Keep this packaged skill as the source of truth; do not copy the script into a
|
|
53
|
+
project before using it.
|
|
54
|
+
|
|
55
|
+
## Generate or edit an image
|
|
56
|
+
|
|
57
|
+
Start with a sanitized preview:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python3 <skill-dir>/scripts/alibaba_token_media.py image \
|
|
61
|
+
--prompt "A quiet alpine lake at sunrise" \
|
|
62
|
+
--dry-run
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
After explicit approval, repeat without `--dry-run` and add
|
|
66
|
+
`--confirm-credit-use`. For an edit, add one to nine `--image` values. Each
|
|
67
|
+
value may be a permitted local image under the current user's home or a public
|
|
68
|
+
HTTPS URL:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python3 <skill-dir>/scripts/alibaba_token_media.py image \
|
|
72
|
+
--prompt "Preserve the subject; replace the background with a night city" \
|
|
73
|
+
--image /absolute/path/input.png \
|
|
74
|
+
--confirm-credit-use
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use `--model wan2.7-image-pro --size 2K --confirm-high-cost` only after the
|
|
78
|
+
user selects that higher-cost variant. `4K` is allowed only for Pro
|
|
79
|
+
text-to-image with no input images. The script fixes `n=1` and disables image
|
|
80
|
+
sets. Image watermarking is off by default and enabled only with `--watermark`.
|
|
81
|
+
|
|
82
|
+
## Submit a video
|
|
83
|
+
|
|
84
|
+
Text-to-video Lite preview:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
python3 <skill-dir>/scripts/alibaba_token_media.py video-submit \
|
|
88
|
+
--prompt "A paper model city slowly lights up at dusk" \
|
|
89
|
+
--dry-run
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For a real request, add both `--confirm-credit-use` and
|
|
93
|
+
`--confirm-expensive`. For image-to-video, select `happyhorse-1.1-i2v` and
|
|
94
|
+
supply exactly one first-frame image with `--image`. Do not supply `--ratio`
|
|
95
|
+
for image-to-video; its aspect ratio follows the input. Video watermarking is
|
|
96
|
+
on by default and can be disabled explicitly with `--no-watermark`.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python3 <skill-dir>/scripts/alibaba_token_media.py video-submit \
|
|
100
|
+
--model happyhorse-1.1-i2v \
|
|
101
|
+
--image /absolute/path/first-frame.png \
|
|
102
|
+
--prompt "The camera slowly moves forward" \
|
|
103
|
+
--confirm-credit-use --confirm-expensive
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Return the task ID after a real submit. Do not hide submission success behind
|
|
107
|
+
polling.
|
|
108
|
+
|
|
109
|
+
## Check and download a video
|
|
110
|
+
|
|
111
|
+
Query a task once:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python3 <skill-dir>/scripts/alibaba_token_media.py video-status <task-id>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
When it is `SUCCEEDED`, repeat with `--download`. If recurring monitoring is
|
|
118
|
+
requested and the client exposes a loop facility, use an interval of at least
|
|
119
|
+
five minutes. Do not implement a private tight polling loop.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python3 <skill-dir>/scripts/alibaba_token_media.py video-status <task-id> \
|
|
123
|
+
--download
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Default downloads go to
|
|
127
|
+
`~/Downloads/alibaba-token-plan/YYYY-MM-DD/` with unique names and no
|
|
128
|
+
overwrite. An explicit `--output` must remain under the current user's home.
|
|
129
|
+
|
|
130
|
+
## Reference
|
|
131
|
+
|
|
132
|
+
Read [references/api-contract.md](references/api-contract.md) before changing
|
|
133
|
+
payloads, limits, endpoints, or model handling.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Alibaba Token Plan media API contract
|
|
2
|
+
|
|
3
|
+
Verified against the official Qwen Cloud documentation on 2026-07-19.
|
|
4
|
+
|
|
5
|
+
## Fixed subscription surface
|
|
6
|
+
|
|
7
|
+
- Host: `https://token-plan.ap-southeast-1.maas.aliyuncs.com`
|
|
8
|
+
- Credential environment variable: `ALIBABA_CODE_API_KEY`
|
|
9
|
+
- Authorization: `Bearer <token>`
|
|
10
|
+
- Supported invocation surfaces: Claude Code, Codex, Codex-VL, and Pi through
|
|
11
|
+
the packaged skill and CLI. Pi is not assumed to support MCP natively.
|
|
12
|
+
- Token Plan Personal Lite guardrails: interactive use only, one submit at a
|
|
13
|
+
time, no batch generation, and no automatic retry of a generation POST.
|
|
14
|
+
- Never substitute the DashScope pay-as-you-go host.
|
|
15
|
+
|
|
16
|
+
The Token Plan documentation says coding tools cannot configure media models
|
|
17
|
+
as chat models. They must use a skill, command, agent, or equivalent extension.
|
|
18
|
+
The Personal plan is intended for interactive programming and agent-tool
|
|
19
|
+
work, not unattended scripts, backend services, or batch processing.
|
|
20
|
+
|
|
21
|
+
## Wan 2.7 image generation and editing
|
|
22
|
+
|
|
23
|
+
Synchronous endpoint:
|
|
24
|
+
|
|
25
|
+
`POST /api/v1/services/aigc/multimodal-generation/generation`
|
|
26
|
+
|
|
27
|
+
Allowed plan models:
|
|
28
|
+
|
|
29
|
+
- `wan2.7-image`
|
|
30
|
+
- `wan2.7-image-pro`
|
|
31
|
+
|
|
32
|
+
Request shape:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"model": "wan2.7-image",
|
|
37
|
+
"input": {
|
|
38
|
+
"messages": [{
|
|
39
|
+
"role": "user",
|
|
40
|
+
"content": [{"text": "prompt"}, {"image": "data-or-https-url"}]
|
|
41
|
+
}]
|
|
42
|
+
},
|
|
43
|
+
"parameters": {
|
|
44
|
+
"n": 1,
|
|
45
|
+
"size": "1K",
|
|
46
|
+
"enable_sequential": false,
|
|
47
|
+
"watermark": false,
|
|
48
|
+
"thinking_mode": true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The content array requires exactly one text object and accepts zero to nine
|
|
54
|
+
image objects. Prompt maximum is 5,000 characters. Inputs may be JPEG, PNG,
|
|
55
|
+
BMP, or WEBP and are limited to 20 MiB each. `wan2.7-image` supports 1K and 2K;
|
|
56
|
+
Pro also supports 4K for text-to-image only. The output image URL is under
|
|
57
|
+
`output.choices[*].message.content[*].image` and expires after 24 hours.
|
|
58
|
+
|
|
59
|
+
## HappyHorse video generation
|
|
60
|
+
|
|
61
|
+
Submit endpoint:
|
|
62
|
+
|
|
63
|
+
`POST /api/v1/services/aigc/video-generation/video-synthesis`
|
|
64
|
+
|
|
65
|
+
Required submission header: `X-DashScope-Async: enable`.
|
|
66
|
+
|
|
67
|
+
Allowed plan models:
|
|
68
|
+
|
|
69
|
+
- `happyhorse-1.1-t2v`
|
|
70
|
+
- `happyhorse-1.1-i2v`
|
|
71
|
+
|
|
72
|
+
Text-to-video input contains `prompt`; parameters contain `resolution`,
|
|
73
|
+
`ratio`, and `duration`. Image-to-video input additionally contains exactly one
|
|
74
|
+
`media` item with `type: first_frame` and an HTTPS or Base64 image in `url`;
|
|
75
|
+
its parameters omit `ratio` because output follows the first frame. The CLI
|
|
76
|
+
enables video watermarking by default; image watermarking is disabled by
|
|
77
|
+
default. Image-to-video first frames accept JPEG, PNG, or WEBP, not BMP.
|
|
78
|
+
|
|
79
|
+
Duration is an integer from 3 through 15 seconds. Resolution is 720P or 1080P.
|
|
80
|
+
Text-to-video ratios are 16:9, 9:16, 1:1, 4:3, 3:4, 4:5, 5:4, 9:21, or 21:9.
|
|
81
|
+
The response task ID is `output.task_id`.
|
|
82
|
+
|
|
83
|
+
Status endpoint:
|
|
84
|
+
|
|
85
|
+
`GET /api/v1/tasks/{task_id}`
|
|
86
|
+
|
|
87
|
+
Terminal states are `SUCCEEDED`, `FAILED`, `CANCELED`, and `UNKNOWN`. On
|
|
88
|
+
success, `output.video_url` is an H.264 MP4 signed URL valid for 24 hours.
|
|
89
|
+
|
|
90
|
+
## Official sources
|
|
91
|
+
|
|
92
|
+
- https://docs.qwencloud.com/token-plan/best-practices/integrate-multimodal-gen
|
|
93
|
+
- https://docs.qwencloud.com/token-plan/personal/token-plan-personal-overview
|
|
94
|
+
- https://docs.qwencloud.com/api-reference/image-generation/wan27-image-gen-edit/synchronous
|
|
95
|
+
- https://docs.qwencloud.com/api-reference/video-generation/happyhorse-text-to-video/create-task
|
|
96
|
+
- https://docs.qwencloud.com/api-reference/video-generation/happyhorse-image-to-video/create-task
|
|
97
|
+
- https://docs.qwencloud.com/api-reference/video-generation/happyhorse-text-to-video/query-result
|