@petercjl/seedaudiocli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @petercjl/seedaudiocli
2
2
 
3
- EvoLink Seed Audio CLI with a portable companion Skill. Requires Node.js 22+ and Python 3.9+ with SSL; no Python third-party packages are needed.
3
+ Official Volcengine Seed Audio CLI with a portable companion Skill. Requires Node.js 22+ and Python 3.9+ with SSL; no Python third-party packages are needed.
4
4
 
5
5
  ## Install and configure
6
6
 
@@ -27,7 +27,7 @@ Credentials persist outside the npm installation:
27
27
 
28
28
  macOS/Linux files use mode 0600. Windows relies on the user's profile directory ACL; administrators should keep that directory private. Import validates local structure and does not make a paid API call. `auth status` reports local availability, not remote key validity. Environment variables override saved configuration. Network errors do not clear credentials.
29
29
 
30
- An existing configuration requires `--replace`; replacement creates a recoverable sibling backup before atomic write. Backups contain credentials too. Keep them private. Package uninstall/reinstall preserves configuration and task history. Existing env configurations remain supported. Legacy `config init --import-key-file FILE` remains available for older note-format files; new distribution uses `config import`.
30
+ An existing configuration requires `--replace`; replacement creates a recoverable sibling backup before atomic write. Backups contain credentials too. Keep them private. Package uninstall/reinstall preserves configuration and task history. Official env configurations are supported. To migrate an existing installation, import the new official configuration with `--replace`. Relay endpoints and relay model IDs are rejected. Obtain a Doubao Speech API key with Seed Audio access; an Ark API key is a different credential.
31
31
 
32
32
  ## Generate and recover
33
33
 
@@ -37,7 +37,13 @@ seedaudiocli generate text --prompt "Generate five seconds of instrumental piano
37
37
  seedaudiocli task wait TASK_ID --download ./audio
38
38
  ```
39
39
 
40
- Actual generation can incur provider charges. Save the returned task ID and recover it instead of submitting again. Unknown submission outcomes require provider/history inspection. Reference modes and parameter limits are documented by `generate --help` and the bundled Skill.
40
+ Actual generation can incur provider charges. The official API returns audio synchronously. The CLI records a local ID before its single POST, then saves the response and audio privately. `task get`, `task wait` and `task download` recover saved local results without network calls. There is no remote task polling. A timeout or interrupted submission is marked unknown; inspect its local record and provider billing evidence before explicitly authorizing another attempt with `--force`. A repeated successful request reuses the cached result.
41
+
42
+ The fixed endpoint is `https://openspeech.bytedance.com/api/v3/tts/create`, using `X-Api-Key` and model `seed-audio-1.0`. Prompts accept 1–3000 characters. Use `--audio-file` for local recordings or `--audio-reference` for preset speakers/public URLs (up to three total); address them with `@音频1` through `@音频3`. Use one `--image-file` or `--image-url` for image guidance. Local files are encoded directly in the official request. References are limited to 10 MiB each; audio must be at most 30 seconds. Audio and image modes are exclusive. `--speech-rate` and `--loudness-rate` are integers from -50 to 100 (0 unchanged), and `--pitch-rate` is -12 to 12. `--subtitle` saves returned subtitle data next to the audio.
43
+
44
+ Outputs support mp3, wav, pcm and ogg_opus. Select compatible sample rates with the CLI help: mp3 excludes 40000 Hz and ogg_opus requires 48000 Hz. Output files are never silently overwritten. Old history remains on disk; official records are stored separately.
45
+
46
+ Protocol reference: [Volcengine Seed Audio HTTP documentation](https://docs.volcengine.com/docs/DoubaoVoice/audio-generation-http?lang=zh).
41
47
 
42
48
  ## Agent installation
43
49
 
@@ -57,10 +63,14 @@ npm install -g @petercjl/seedaudiocli@latest
57
63
  seedaudiocli skill update --agent codex
58
64
  ```
59
65
 
60
- Run `skill update` for each installed Agent, including the configured SealSeek root. `update instructions` prints these operations and the current version's rollback command. Updates are explicit. Review compatibility before a major-version update. For rollback, install the prior exact npm version and synchronize Skills again. Credentials and history remain unchanged; offline or registry errors require retrying the package update when connectivity returns.
66
+ Global installs check and install stable latest before every run and synchronize previously registered managed Skills. Use `update check` to inspect availability and `update install` to apply it immediately. For a deliberate rollback, set `SEEDAUDIO_AUTO_UPDATE=0`, install the prior exact npm version and synchronize Skills. Configuration and history remain unchanged.
61
67
 
62
68
  Python discovery checks an explicit `SEEDAUDIO_PYTHON`, configured Agent runtime directories and platform commands. `SEEDAUDIO_RUNTIME_ROOT` can point to a managed runtime root. A missing or broken runtime produces `PYTHON_REQUIRED` with installation guidance.
63
69
 
64
70
  ## Development and evidence
65
71
 
66
72
  Run `npm test` for isolated CLI, configuration and lifecycle tests, and `npm pack --dry-run` to inspect published contents. Tests use synthetic keys and do not submit paid jobs. GitHub QA covers macOS, Windows and Linux package entry points. Host Skill discovery and paid audio output require separate runtime evidence; adapter status remains `implemented` until those checks are recorded. Publishing uses the committed GitHub Actions OIDC workflow after QA. Public distribution does not include credentials.
73
+
74
+ ## Automatic updates
75
+
76
+ Global npm installs check stable latest before every command, install newer releases and sync managed Skills before running the requested command once. No TTL cache is used. Offline checks keep the installed version; updates back up the package for rollback. Configuration and task history remain outside the package. Use `seedaudiocli update check` or `seedaudiocli update install`. For a deliberately pinned run set `SEEDAUDIO_AUTO_UPDATE=0`. Development checkouts are Git-managed; taobao-ai-ops owns suite component updates.
package/capabilities.json CHANGED
@@ -20,6 +20,7 @@
20
20
  "task get",
21
21
  "task wait",
22
22
  "task url",
23
+ "task download",
23
24
  "task jobs",
24
25
  "payload validate",
25
26
  "payload submit",
@@ -27,7 +28,8 @@
27
28
  "skill status",
28
29
  "skill install",
29
30
  "skill update",
30
- "update instructions"
31
+ "update check",
32
+ "update install"
31
33
  ],
32
34
  "config": {
33
35
  "format": [
@@ -49,19 +51,30 @@
49
51
  "import": "seedaudiocli config import FILE [--replace]",
50
52
  "side_effects": "Local file write, backup on replacement; no remote call",
51
53
  "missing": "CONFIG_REQUIRED",
52
- "secrets_in_output": false
54
+ "secrets_in_output": false,
55
+ "migration": "Import official configuration with --replace; backup is automatic; relay endpoints/models are rejected."
53
56
  },
54
57
  "update": {
55
- "trigger": "explicit",
56
- "instructions": "npm install -g @petercjl/seedaudiocli@latest; then skill update for each installed Agent",
57
- "compatibility": "Review release notes before major updates",
58
- "configuration": "Outside package; preserved"
58
+ "trigger": "before-every-run",
59
+ "channel": "latest",
60
+ "automatic_install": true,
61
+ "configuration": "Outside package; preserved",
62
+ "offline": "keep-current",
63
+ "source_checkout": "git-managed",
64
+ "suite_install": "suite-managed",
65
+ "opt_out": "SEEDAUDIO_AUTO_UPDATE=0",
66
+ "managed_skills": "synchronized with backups; edited content preserved"
59
67
  },
60
68
  "generation": {
61
- "provider": "EvoLink",
62
- "model": "doubao-seed-audio-1-0",
69
+ "provider": "Volcengine",
70
+ "endpoint": "https://openspeech.bytedance.com/api/v3/tts/create",
71
+ "authentication": "X-Api-Key (Doubao Speech)",
72
+ "model": "seed-audio-1.0",
73
+ "mode": "synchronous",
63
74
  "paid": true,
64
75
  "submit_retry": false,
65
- "recovery": "Use saved task ID; unknown submit outcome requires inspection before resubmission"
76
+ "local_references": true,
77
+ "remote_polling": false,
78
+ "recovery": "Persist local record before POST; save response and audio; export locally by record ID. Unknown outcomes require inspection and explicit authorization before --force."
66
79
  }
67
80
  }
package/npm/cli.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
+ import { beforeRun, checkUpdate, installUpdate } from './update.mjs';
3
4
  import path from 'node:path';
4
5
  import { root, pkg, fail, findPython, runPython } from './runtime.mjs';
5
6
  import { source, targetPath, status, install } from './skills.mjs';
@@ -8,7 +9,8 @@ const args = process.argv.slice(2);
8
9
  const print = value => console.log(JSON.stringify(value, null, 2));
9
10
  const flag = name => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
10
11
  try {
11
- if (['version', '--version'].includes(args[0])) console.log(`seedaudiocli ${pkg.version}`);
12
+ if (await beforeRun(args)) { /* child completed */ }
13
+ else if (['version', '--version'].includes(args[0])) console.log(`seedaudiocli ${pkg.version}`);
12
14
  else if (args[0] === 'capabilities') print(JSON.parse(fs.readFileSync(path.join(root, 'capabilities.json'), 'utf8')));
13
15
  else if (args[0] === 'doctor') {
14
16
  const python = findPython();
@@ -26,9 +28,11 @@ try {
26
28
  else throw fail('INVALID_COMMAND', 'Use skill source|status|install|update.');
27
29
  }
28
30
  } else if (args[0] === 'update') {
29
- print({ ok: true, policy: 'explicit', installed_version: pkg.version, package_command: `npm install -g ${pkg.name}@latest`, skill_command: 'seedaudiocli skill update --agent AGENT [--root SKILLS_DIRECTORY]', rollback: `npm install -g ${pkg.name}@${pkg.version}`, note: 'Run the package command, then update each previously installed Skill. Configuration and task history remain in user directories. Registry/network failures preserve the last saved configuration.' });
31
+ const check = await checkUpdate();
32
+ if (args[1] === 'install' && check.updateAvailable) print(installUpdate(check));
33
+ else print({ ...check, policy: 'before-every-run', opt_out: 'SEEDAUDIO_AUTO_UPDATE=0' });
30
34
  } else if (!args.length || ['--help', '-h'].includes(args[0])) {
31
- console.log('seedaudiocli: setup | config import FILE [--replace] | auth status | generate | task | payload\nPackage: version | capabilities --json | doctor --json | skill source/status/install/update [--agent codex|sealseek] [--root SKILLS_DIRECTORY] | update instructions\nRuntime: Node.js 22+, Python 3.9+ (SEEDAUDIO_PYTHON optional).');
35
+ console.log('seedaudiocli: setup | config import FILE [--replace] | auth status | generate | task | payload\nPackage: version | capabilities --json | doctor --json | skill source/status/install/update [--agent codex|sealseek] [--root SKILLS_DIRECTORY] | update check/install\nRuntime: Node.js 22+, Python 3.9+ (SEEDAUDIO_PYTHON optional).');
32
36
  process.exitCode = runPython(['--help']).status || 0;
33
37
  } else {
34
38
  const result = runPython(args);
package/npm/skills.mjs CHANGED
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import crypto from 'node:crypto';
5
+ import { stateRoot } from './update.mjs';
5
6
  import { root, pkg, fail } from './runtime.mjs';
6
7
 
7
8
  export const source = path.join(root, 'skills', 'seedaudiocli');
@@ -46,6 +47,10 @@ export function status(target) {
46
47
  }
47
48
  export function install(target) {
48
49
  const state = status(target);
50
+ const registry = path.join(stateRoot(), 'skill-targets');
51
+ fs.mkdirSync(registry, { recursive: true });
52
+ const registration = path.join(registry, crypto.createHash('sha256').update(target).digest('hex') + '.json');
53
+ if (!exists(registration)) fs.writeFileSync(registration, JSON.stringify({ target }), { flag: 'wx', mode: 0o600 });
49
54
  if (state.installed && !state.managed) throw fail('SKILL_UNMANAGED', 'An existing user-owned Skill occupies this target; preserve it and choose a new root or explicitly migrate it.');
50
55
  if (state.edited) throw fail('SKILL_EDITED', 'Managed Skill files contain local edits; preserve and reconcile those edits before updating.');
51
56
  if (state.current && state.version === pkg.version) return { ...state, record: undefined, ok: true, changed: false };
@@ -60,7 +65,8 @@ export function install(target) {
60
65
  let backup;
61
66
  try {
62
67
  if (state.installed) {
63
- backup = target + '.backup-' + crypto.randomUUID();
68
+ backup = path.join(stateRoot(), 'skill-backups', crypto.randomUUID());
69
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
64
70
  fs.cpSync(target, backup, { recursive: true, errorOnExist: true, force: false });
65
71
  } else fs.mkdirSync(target);
66
72
  // In-place updates support hosts holding a directory handle open. Backup is recoverable.
package/npm/update.mjs ADDED
@@ -0,0 +1,101 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { root, pkg, fail } from './runtime.mjs';
7
+
8
+ export const stateRoot = () => process.env.SEEDAUDIO_STATE_DIR || (process.platform === 'win32' ? path.join(process.env.LOCALAPPDATA || os.homedir(), 'seedaudiocli') : path.join(os.homedir(), '.local', 'state', 'seedaudiocli'));
9
+ export function newer(a, b) {
10
+ if (!/^\d+\.\d+\.\d+$/.test(a)) return false;
11
+ const x=a.split('.').map(Number), y=b.split('.').map(Number);
12
+ for(let i=0;i<3;i++) { if(x[i]!==y[i]) return x[i]>y[i]; }
13
+ return false;
14
+ }
15
+ export function globalPrefix(location=root, platform=process.platform) {
16
+ const modules=path.dirname(path.dirname(location));
17
+ if(path.basename(modules)!=='node_modules' || fs.existsSync(path.join(location,'.git'))) return null;
18
+ if(platform==='win32') return path.dirname(modules);
19
+ return path.basename(path.dirname(modules))==='lib' ? path.dirname(path.dirname(modules)) : null;
20
+ }
21
+ export async function checkUpdate(registry=fetch) {
22
+ try {
23
+ const r=await registry(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}/latest`,{signal:AbortSignal.timeout(8000)});
24
+ if(!r.ok) throw Error('Registry unavailable');
25
+ const data=await r.json();
26
+ if(data.name!==pkg.name || !/^\d+\.\d+\.\d+$/.test(data.version)) throw Error('Invalid stable package');
27
+ return {ok:true,current:pkg.version,available:data.version,updateAvailable:newer(data.version,pkg.version),tag:'latest'};
28
+ } catch { return {ok:false,code:'REGISTRY_UNAVAILABLE',current:pkg.version,updateAvailable:false,offline:'keep-current'}; }
29
+ }
30
+ function npm(args) {
31
+ let cli=process.env.npm_execpath;
32
+ if(!cli?.endsWith('npm-cli.js')) {
33
+ const found=spawnSync(process.platform==='win32'?'where':'which',[process.platform==='win32'?'npm.cmd':'npm'],{encoding:'utf8',timeout:10000});
34
+ const launcher=found.stdout?.trim().split(/\r?\n/)[0];
35
+ cli=launcher && (process.platform==='win32' ? path.join(path.dirname(launcher),'node_modules','npm','bin','npm-cli.js') : fs.realpathSync(launcher));
36
+ }
37
+ if(!cli || !fs.existsSync(cli)) throw fail('NPM_UNAVAILABLE','npm is required for updates.');
38
+ const r=spawnSync(process.execPath,[cli,...args],{encoding:'utf8',timeout:180000});
39
+ if(r.status!==0) throw fail('UPDATE_FAILED','npm update failed; check npm connectivity and installation permissions.');
40
+ return r.stdout;
41
+ }
42
+ export function installUpdate(check, services={}) {
43
+ const prefix=services.prefix || globalPrefix();
44
+ if(!prefix) throw fail('UPDATE_OWNER_REQUIRED','This installation is managed by its source checkout or parent package.');
45
+ const state=stateRoot(); fs.mkdirSync(state,{recursive:true});
46
+ const lock=path.join(state,'update.lock');
47
+ try { fs.mkdirSync(lock); } catch { throw fail('UPDATE_BUSY','Another update is running. Retry this command after it finishes.'); }
48
+ const run=services.npm || npm, spawn=services.spawn || spawnSync;
49
+ const backup=path.join(state,'package-backups',crypto.randomUUID());
50
+ const targets=[];
51
+ try {
52
+ // Recheck the live package under the lock; another process may have updated it.
53
+ const live=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));
54
+ if(!newer(check.available,live.version)) return {ok:true,changed:false,version:live.version};
55
+ fs.mkdirSync(backup,{recursive:true});
56
+ const archive=JSON.parse(run(['pack',root,'--ignore-scripts','--pack-destination',backup,'--json']))[0];
57
+ const registered=path.join(state,'skill-targets');
58
+ const known=[path.join(process.env.CODEX_HOME || path.join(os.homedir(),'.codex'),'skills','seedaudiocli')];
59
+ for(const f of (fs.existsSync(registered) ? fs.readdirSync(registered) : [])) {
60
+ try { known.push(JSON.parse(fs.readFileSync(path.join(registered,f),'utf8')).target); } catch { /* invalid registration */ }
61
+ }
62
+ for(const target of new Set(known)) {
63
+ try { if(JSON.parse(fs.readFileSync(path.join(target,'.seedaudiocli-managed.json'),'utf8')).package===pkg.name) targets.push(target); } catch { /* unmanaged or absent */ }
64
+ }
65
+ const entry=path.join(root,'npm','cli.mjs');
66
+ const sync=target=>spawn(process.execPath,[entry,'skill','update','--root',path.dirname(target)],{encoding:'utf8',timeout:30000,env:{...process.env,SEEDAUDIO_AUTO_UPDATE:'0'}});
67
+ let mutated=false;
68
+ try {
69
+ mutated=true;
70
+ run(['install','--global','--prefix',prefix,'--ignore-scripts','--no-audit','--no-fund',`${pkg.name}@${check.available}`]);
71
+ const version=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8')).version;
72
+ if(version!==check.available) throw fail('UPDATE_VERIFY_FAILED','Installed package version mismatch.');
73
+ for(const target of targets) if(sync(target).status!==0) throw fail('SKILL_SYNC_FAILED','A managed Skill needs reconciliation.');
74
+ return {ok:true,changed:true,from:pkg.version,version,backup:path.join(backup,archive.filename)};
75
+ } catch(e) {
76
+ if(mutated) {
77
+ try { run(['install','--global','--prefix',prefix,'--ignore-scripts',path.join(backup,archive.filename)]); }
78
+ catch { throw fail('ROLLBACK_REQUIRED','Automatic recovery failed; restore the package archive before running commands.'); }
79
+ for(const target of targets) if(sync(target).status!==0) throw fail('ROLLBACK_SYNC_REQUIRED','Package restored; reconcile edited Skill files before updating.');
80
+ }
81
+ throw e;
82
+ }
83
+ } finally { fs.rmdirSync(lock); }
84
+ }
85
+ export async function beforeRun(args, services={}) {
86
+ if(process.env.SEEDAUDIO_AUTO_UPDATE==='0' || !globalPrefix() || args[0]==='update') return false;
87
+ const check=await (services.check || checkUpdate)();
88
+ if(!check.ok) console.error('[seedaudiocli] Update check unavailable; using installed version.');
89
+ if(!check.updateAvailable) return false;
90
+ try {
91
+ const result=(services.install || installUpdate)(check);
92
+ console.error(`[seedaudiocli] Using latest ${result.version}.`);
93
+ } catch(e) {
94
+ if(['UPDATE_BUSY','ROLLBACK_SYNC_REQUIRED','ROLLBACK_REQUIRED'].includes(e.code)) throw e;
95
+ console.error(`[seedaudiocli] ${e.code || 'UPDATE_FAILED'}; using restored installed version.`);
96
+ }
97
+ // Delegate once only after update/rollback; never replay a business request.
98
+ const r=spawnSync(process.execPath,[path.join(root,'npm','cli.mjs'),...args],{stdio:'inherit',env:{...process.env,SEEDAUDIO_AUTO_UPDATE:'0'}});
99
+ process.exitCode=r.status??1;
100
+ return true;
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@petercjl/seedaudiocli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Seed Audio CLI with portable Agent skill and persistent user credentials",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1,9 @@
1
- __version__ = "0.1.0"
1
+ """Version follows the canonical npm package metadata."""
2
+ import json
3
+ from pathlib import Path
4
+
5
+ try:
6
+ __version__ = json.loads((Path(__file__).resolve().parent.parent / "package.json").read_text(encoding="utf-8"))["version"]
7
+ except (OSError, ValueError, KeyError):
8
+ from importlib.metadata import version
9
+ __version__ = version("seedaudiocli")