@petercjl/seedaudiocli 0.0.0-bootstrap.0 → 0.1.1

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,3 +1,66 @@
1
- # Seed Audio CLI bootstrap
1
+ # @petercjl/seedaudiocli
2
2
 
3
- This placeholder establishes the package for npm Trusted Publishing. It contains no executable, credentials, or functional release. Use the stable release after it becomes available.
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.
4
+
5
+ ## Install and configure
6
+
7
+ ```bash
8
+ npm install -g @petercjl/seedaudiocli
9
+ seedaudiocli setup
10
+ ```
11
+
12
+ On first use, provide the path to the configuration file supplied by your administrator. For an Agent or noninteractive terminal:
13
+
14
+ ```bash
15
+ seedaudiocli config import "/path/to/seedaudio-config.json"
16
+ seedaudiocli auth status
17
+ seedaudiocli doctor --json
18
+ ```
19
+
20
+ Copy `seedaudiocli.config.example.json`, fill `SEEDAUDIO_API_KEY`, and deliver the file privately to the authorized recipient. A `.env` file using the same field names is also accepted. The CLI parses data; it does not execute configuration as a script. The package includes only blank examples. Each recipient with the file can use the key. Per-user service keys allow independent revocation and limits.
21
+
22
+ Credentials persist outside the npm installation:
23
+
24
+ - macOS/Linux: `~/.config/seedaudiocli/seedaudiocli.env`
25
+ - Windows: `%APPDATA%/seedaudiocli/seedaudiocli.env`
26
+ - Explicit override: `SEEDAUDIO_CONFIG_DIR`
27
+
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
+
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`.
31
+
32
+ ## Generate and recover
33
+
34
+ ```bash
35
+ seedaudiocli generate text --prompt "Generate five seconds of instrumental piano." --dry-run
36
+ seedaudiocli generate text --prompt "Generate five seconds of instrumental piano." --wait --download ./audio
37
+ seedaudiocli task wait TASK_ID --download ./audio
38
+ ```
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.
41
+
42
+ ## Agent installation
43
+
44
+ ```bash
45
+ seedaudiocli skill source --json
46
+ seedaudiocli skill install --agent codex
47
+ seedaudiocli skill status --agent codex --json
48
+ seedaudiocli skill install --agent sealseek --root "/actual/skills-directory"
49
+ ```
50
+
51
+ SealSeek's discovery directory is explicit (`--root` or `SEALSEEK_SKILLS_DIR`). Unknown Agents and unmanaged targets are refused. Managed copies have ownership/hash records; updates back up prior contents, preserve customized UI metadata and refuse local edits. In-place synchronization supports directory handles held open by an Agent. Failed synchronization reports the recovery backup.
52
+
53
+ ## Update and rollback
54
+
55
+ ```bash
56
+ npm install -g @petercjl/seedaudiocli@latest
57
+ seedaudiocli skill update --agent codex
58
+ ```
59
+
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.
61
+
62
+ 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
+
64
+ ## Development and evidence
65
+
66
+ 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.
@@ -0,0 +1,6 @@
1
+ {
2
+ "agent": "codex",
3
+ "skill": "skills/seedaudiocli",
4
+ "status": "implemented",
5
+ "installation": "seedaudiocli skill install --agent codex"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "agent": "sealseek",
3
+ "skill": "skills/seedaudiocli",
4
+ "status": "implemented",
5
+ "installation": "seedaudiocli skill install --agent sealseek --root SKILLS_DIRECTORY"
6
+ }
@@ -0,0 +1,67 @@
1
+ {
2
+ "schema_version": "1.0.0",
3
+ "package": "@petercjl/seedaudiocli",
4
+ "runtime": {
5
+ "node": ">=22",
6
+ "python": ">=3.9",
7
+ "python_override": "SEEDAUDIO_PYTHON"
8
+ },
9
+ "commands": [
10
+ "version",
11
+ "capabilities",
12
+ "doctor",
13
+ "setup",
14
+ "config import",
15
+ "auth status",
16
+ "models list",
17
+ "generate text",
18
+ "generate audio-ref",
19
+ "generate image-ref",
20
+ "task get",
21
+ "task wait",
22
+ "task url",
23
+ "task jobs",
24
+ "payload validate",
25
+ "payload submit",
26
+ "skill source",
27
+ "skill status",
28
+ "skill install",
29
+ "skill update",
30
+ "update instructions"
31
+ ],
32
+ "config": {
33
+ "format": [
34
+ "json",
35
+ "env"
36
+ ],
37
+ "fields": [
38
+ "SEEDAUDIO_API_KEY",
39
+ "SEEDAUDIO_BASE_URL",
40
+ "SEEDAUDIO_MODEL",
41
+ "SEEDAUDIO_DEFAULT_FORMAT",
42
+ "SEEDAUDIO_DEFAULT_SAMPLE_RATE"
43
+ ],
44
+ "precedence": [
45
+ "environment",
46
+ "saved config",
47
+ "defaults"
48
+ ],
49
+ "import": "seedaudiocli config import FILE [--replace]",
50
+ "side_effects": "Local file write, backup on replacement; no remote call",
51
+ "missing": "CONFIG_REQUIRED",
52
+ "secrets_in_output": false
53
+ },
54
+ "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"
59
+ },
60
+ "generation": {
61
+ "provider": "EvoLink",
62
+ "model": "doubao-seed-audio-1-0",
63
+ "paid": true,
64
+ "submit_retry": false,
65
+ "recovery": "Use saved task ID; unknown submit outcome requires inspection before resubmission"
66
+ }
67
+ }
package/npm/cli.mjs ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { root, pkg, fail, findPython, runPython } from './runtime.mjs';
5
+ import { source, targetPath, status, install } from './skills.mjs';
6
+
7
+ const args = process.argv.slice(2);
8
+ const print = value => console.log(JSON.stringify(value, null, 2));
9
+ const flag = name => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
10
+ try {
11
+ if (['version', '--version'].includes(args[0])) console.log(`seedaudiocli ${pkg.version}`);
12
+ else if (args[0] === 'capabilities') print(JSON.parse(fs.readFileSync(path.join(root, 'capabilities.json'), 'utf8')));
13
+ else if (args[0] === 'doctor') {
14
+ const python = findPython();
15
+ const auth = runPython(['auth', 'status'], { encoding: 'utf8', stdio: 'pipe' });
16
+ let config;
17
+ try { config = JSON.parse(auth.stdout); } catch { throw fail('RUNTIME_FAILED', 'Python CLI did not return JSON.'); }
18
+ print({ ok: auth.status === 0, version: pkg.version, python, skill_source: source, config });
19
+ process.exitCode = auth.status === 0 ? 0 : 1;
20
+ } else if (args[0] === 'skill') {
21
+ if (args[1] === 'source') print({ ok: true, source, version: pkg.version });
22
+ else {
23
+ const target = targetPath(flag('--agent') || 'codex', flag('--root'));
24
+ if (args[1] === 'status') print({ ok: true, ...status(target), record: undefined });
25
+ else if (['install', 'update'].includes(args[1])) print(install(target));
26
+ else throw fail('INVALID_COMMAND', 'Use skill source|status|install|update.');
27
+ }
28
+ } 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.' });
30
+ } 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).');
32
+ process.exitCode = runPython(['--help']).status || 0;
33
+ } else {
34
+ const result = runPython(args);
35
+ process.exitCode = result.status ?? 1;
36
+ }
37
+ } catch (e) {
38
+ print({ ok: false, code: e.code || 'LOCAL_OPERATION_FAILED', error: e.code ? e.message : 'Local operation failed.' });
39
+ process.exitCode = 1;
40
+ }
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+ export const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
9
+ export const fail = (code, message) => Object.assign(new Error(message), { code });
10
+ export function pythonCandidates(env = process.env) {
11
+ if (env.SEEDAUDIO_PYTHON) return [[env.SEEDAUDIO_PYTHON]];
12
+ const result = [];
13
+ // Discover bundled Agent runtimes within declared runtime directories, never through credentials.
14
+ const bases = [env.CODEX_HOME || path.join(os.homedir(), '.codex'), env.SEALSEEK_HOME, env.SEEDAUDIO_RUNTIME_ROOT].filter(Boolean);
15
+ function visit(dir, depth) {
16
+ if (depth < 0) return;
17
+ for (const item of fs.readdirSync(dir, { withFileTypes: true, throwIfNoEntry: false }) || []) {
18
+ const file = path.join(dir, item.name);
19
+ if ((item.isFile() || item.isSymbolicLink()) && /^(python3?(\.exe)?)$/.test(item.name)) result.push([file]);
20
+ else if (item.isDirectory()) visit(file, depth - 1);
21
+ }
22
+ }
23
+ for (const base of bases) {
24
+ for (const sub of ['runtimes', 'runtime', 'dependencies', 'python']) {
25
+ try { visit(path.join(base, sub), 4); } catch { /* missing/inaccessible candidate */ }
26
+ }
27
+ }
28
+ if (process.platform === 'win32') result.push(['py', '-3'], ['python'], ['python3']);
29
+ else result.push(['python3'], ['python'], ['/opt/homebrew/bin/python3'], ['/usr/local/bin/python3']);
30
+ return result;
31
+ }
32
+ export function findPython(env = process.env) {
33
+ for (const command of pythonCandidates(env)) {
34
+ const probe = spawnSync(command[0], [...command.slice(1), '-c', 'import sys,json,ssl,urllib.request,pathlib; assert sys.version_info >= (3,9); print(json.dumps({"executable":sys.executable,"version":sys.version.split()[0]}))'], { encoding: 'utf8', timeout: 5000, env, windowsHide: true });
35
+ if (probe.status === 0) {
36
+ try { return { command, ...JSON.parse(probe.stdout.trim()) }; } catch { /* invalid runtime */ }
37
+ }
38
+ }
39
+ throw fail('PYTHON_REQUIRED', 'Install Python 3.9+ with SSL support, or set SEEDAUDIO_PYTHON to its executable. Then run seedaudiocli doctor --json.');
40
+ }
41
+ export function runPython(args, options = {}) {
42
+ const env = { ...process.env, ...options.env, PYTHONUTF8: '1', PYTHONIOENCODING: 'utf-8' };
43
+ const runtime = findPython(env);
44
+ // Resolve only the bundled implementation; user PYTHONPATH cannot shadow it.
45
+ const boot = 'import sys; sys.path.insert(0,sys.argv.pop(1)); from seedaudiocli.cli import main; sys.exit(main())';
46
+ return spawnSync(runtime.command[0], [...runtime.command.slice(1), '-c', boot, root, ...args], { stdio: 'inherit', windowsHide: true, ...options, env });
47
+ }
package/npm/skills.mjs ADDED
@@ -0,0 +1,82 @@
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 { root, pkg, fail } from './runtime.mjs';
6
+
7
+ export const source = path.join(root, 'skills', 'seedaudiocli');
8
+ const marker = '.seedaudiocli-managed.json';
9
+ const metadata = 'agents/openai.yaml';
10
+ function exists(file) { try { return fs.lstatSync(file); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } }
11
+ export function inventory(dir) {
12
+ const result = {};
13
+ function walk(at, prefix = '') {
14
+ for (const entry of fs.readdirSync(at, { withFileTypes: true })) {
15
+ if (entry.name === marker) continue;
16
+ const relative = prefix + entry.name;
17
+ if (entry.isSymbolicLink()) throw fail('SKILL_UNSAFE_TARGET', 'Skill files must not contain symlinks.');
18
+ if (entry.isDirectory()) walk(path.join(at, entry.name), relative + '/');
19
+ else result[relative] = crypto.createHash('sha256').update(fs.readFileSync(path.join(at, entry.name))).digest('hex');
20
+ }
21
+ }
22
+ walk(dir);
23
+ return result;
24
+ }
25
+ export function targetPath(agent, targetRoot) {
26
+ if (!['codex', 'sealseek'].includes(agent)) throw fail('INVALID_AGENT', 'Choose --agent codex or sealseek.');
27
+ let base = targetRoot;
28
+ if (!base && agent === 'codex') base = path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'skills');
29
+ if (!base && agent === 'sealseek') base = process.env.SEALSEEK_SKILLS_DIR;
30
+ if (!base) throw fail('AGENT_PATH_REQUIRED', 'Provide the actual SealSeek skills directory through --root or SEALSEEK_SKILLS_DIR.');
31
+ return path.join(path.resolve(base), 'seedaudiocli');
32
+ }
33
+ export function status(target) {
34
+ const st = exists(target);
35
+ if (!st) return { installed: false, managed: false, current: false, target };
36
+ if (st.isSymbolicLink() || !st.isDirectory()) return { installed: true, managed: false, current: false, target };
37
+ let record;
38
+ try { record = JSON.parse(fs.readFileSync(path.join(target, marker), 'utf8')); } catch { return { installed: true, managed: false, current: false, target }; }
39
+ if (record.package !== pkg.name || !record.files || typeof record.files !== 'object') return { installed: true, managed: false, current: false, target };
40
+ if (Object.keys(record.files).some(file => !file || path.isAbsolute(file) || file.includes('\\') || file.split('/').some(part => ['.', '..', ''].includes(part)))) throw fail('SKILL_UNSAFE_TARGET', 'Invalid managed-file path in Skill ownership record.');
41
+ const files = inventory(target);
42
+ const edited = Object.entries(record.files).some(([file, hash]) => file !== metadata && files[file] !== hash);
43
+ const expected = inventory(source);
44
+ const current = !edited && Object.entries(expected).every(([file, hash]) => file === metadata || files[file] === hash);
45
+ return { installed: true, managed: true, current, edited, target, version: record.version, record };
46
+ }
47
+ export function install(target) {
48
+ const state = status(target);
49
+ 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
+ if (state.edited) throw fail('SKILL_EDITED', 'Managed Skill files contain local edits; preserve and reconcile those edits before updating.');
51
+ if (state.current && state.version === pkg.version) return { ...state, record: undefined, ok: true, changed: false };
52
+ const expected = inventory(source);
53
+ for (const file of Object.keys(expected)) {
54
+ if (state.installed && exists(path.join(target, file)) && !(file in state.record.files) && file !== metadata) throw fail('SKILL_UNMANAGED_FILE', 'A new bundled file would overwrite a user-owned file.');
55
+ }
56
+ fs.mkdirSync(path.dirname(target), { recursive: true });
57
+ const lock = target + '.install-lock';
58
+ let lockfd;
59
+ try { lockfd = fs.openSync(lock, 'wx', 0o600); } catch (e) { if (e.code === 'EEXIST') throw fail('SKILL_BUSY', 'Another Skill installation holds the lock.'); throw e; }
60
+ let backup;
61
+ try {
62
+ if (state.installed) {
63
+ backup = target + '.backup-' + crypto.randomUUID();
64
+ fs.cpSync(target, backup, { recursive: true, errorOnExist: true, force: false });
65
+ } else fs.mkdirSync(target);
66
+ // In-place updates support hosts holding a directory handle open. Backup is recoverable.
67
+ for (const file of Object.keys(expected)) {
68
+ if (file === metadata && exists(path.join(target, file))) continue;
69
+ fs.mkdirSync(path.dirname(path.join(target, file)), { recursive: true });
70
+ fs.copyFileSync(path.join(source, file), path.join(target, file));
71
+ }
72
+ for (const file of Object.keys(state.record?.files || {})) {
73
+ if (file !== metadata && !(file in expected)) fs.unlinkSync(path.join(target, file));
74
+ }
75
+ const actual = inventory(target);
76
+ if (Object.entries(expected).some(([file, hash]) => file !== metadata && actual[file] !== hash)) throw fail('SKILL_VERIFY_FAILED', 'Installed Skill hashes differ from the package.');
77
+ fs.writeFileSync(path.join(target, marker), JSON.stringify({ package: pkg.name, version: pkg.version, files: expected }, null, 2));
78
+ return { ok: true, installed: true, managed: true, current: true, changed: true, target, version: pkg.version, backup: backup || null };
79
+ } catch (e) {
80
+ throw fail('SKILL_INSTALL_FAILED', `Skill installation failed; ${backup ? 'restore from ' + backup : 'inspect the partial target at ' + target}.`);
81
+ } finally { fs.closeSync(lockfd); fs.unlinkSync(lock); }
82
+ }
package/package.json CHANGED
@@ -1,10 +1,29 @@
1
1
  {
2
2
  "name": "@petercjl/seedaudiocli",
3
- "version": "0.0.0-bootstrap.0",
4
- "description": "Bootstrap placeholder for configuring npm Trusted Publishing.",
3
+ "version": "0.1.1",
4
+ "description": "Seed Audio CLI with portable Agent skill and persistent user credentials",
5
+ "type": "module",
6
+ "bin": {
7
+ "seedaudiocli": "npm/cli.mjs"
8
+ },
5
9
  "files": [
10
+ "npm/",
11
+ "seedaudiocli/*.py",
12
+ "skills/",
13
+ "capabilities.json",
14
+ "adapters/",
15
+ "seedaudiocli.env.example",
16
+ "seedaudiocli.config.example.json",
6
17
  "README.md"
7
18
  ],
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "scripts": {
23
+ "check": "node --check npm/cli.mjs && node --check npm/runtime.mjs && node --check npm/skills.mjs",
24
+ "test": "node --test test/*.test.mjs",
25
+ "prepack": "npm run check && npm test"
26
+ },
8
27
  "publishConfig": {
9
28
  "access": "public"
10
29
  },
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())