@sogni-ai/sogni-creative-agent-skill 3.1.1 → 3.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 +19 -2
- package/SKILL.md +141 -45
- package/generated/creative-agent-runtime.mjs +2 -2
- package/llm.txt +8 -0
- package/package.json +6 -5
- package/skill-package.json +1 -1
- package/sogni-agent.mjs +277 -16
- package/update-check.mjs +303 -0
- package/version.mjs +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ With this skill, an agent can:
|
|
|
42
42
|
- [Requirements](#requirements)
|
|
43
43
|
- [Installation](#installation)
|
|
44
44
|
- [Node CLI (default)](#node-cli-default)
|
|
45
|
+
- [Claude Code plugin](#claude-code-plugin)
|
|
45
46
|
- [OpenClaw plugin](#openclaw-plugin)
|
|
46
47
|
- [Hermes Agent / Manus / other frameworks](#hermes-agent--manus--other-frameworks)
|
|
47
48
|
- [Manual install from source](#manual-install-from-source)
|
|
@@ -111,6 +112,17 @@ sogni-agent --version
|
|
|
111
112
|
|
|
112
113
|
Then point your agent/runtime at this repository's [`SKILL.md`](./SKILL.md). When an install request is ambiguous, install the CLI and skill source together — that's the supported default.
|
|
113
114
|
|
|
115
|
+
### Claude Code plugin
|
|
116
|
+
|
|
117
|
+
The Claude Code plugin shells out to the `sogni-agent` CLI installed above, so both steps are required. From inside Claude Code, register the marketplace and install the plugin:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
/plugin marketplace add Sogni-AI/sogni-creative-agent-skill
|
|
121
|
+
/plugin install sogni-creative-agent@sogni
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The first command registers a `sogni` marketplace with one plugin entry (`sogni-creative-agent`) backed by a lean Claude-Code-focused [`plugin-skills/sogni-creative-agent/SKILL.md`](./plugin-skills/sogni-creative-agent/SKILL.md); the second installs the plugin into Claude Code. The full skill spec still lives at the repository root [`SKILL.md`](./SKILL.md).
|
|
125
|
+
|
|
114
126
|
### OpenClaw plugin
|
|
115
127
|
|
|
116
128
|
For the published plugin:
|
|
@@ -275,6 +287,10 @@ sogni-agent --api-chat --task-profile reasoning --no-thinking \
|
|
|
275
287
|
"Plan a concise multi-step product launch workflow"
|
|
276
288
|
sogni-agent --list-api-models
|
|
277
289
|
|
|
290
|
+
# Durable hosted chat run with SSE progress events
|
|
291
|
+
SOGNI_SKILL_USE_SDK_TRANSPORT=1 sogni-agent --durable-chat \
|
|
292
|
+
"Create a product launch storyboard and render the first hero image"
|
|
293
|
+
|
|
278
294
|
# Durable hosted workflow (/v1/creative-agent/workflows)
|
|
279
295
|
sogni-agent --api-workflow \
|
|
280
296
|
--video-prompt "The camera slowly pushes in as the sketch comes alive" \
|
|
@@ -299,7 +315,7 @@ sogni-agent --get-replay run_abc123 --json
|
|
|
299
315
|
|
|
300
316
|
# Opt in to SDK transport for hosted operations (durable workflows + chat).
|
|
301
317
|
# Validates restEndpoint/socketEndpoint via the skill's SSRF guard, then
|
|
302
|
-
# calls
|
|
318
|
+
# calls the SDK workflow/chat methods directly.
|
|
303
319
|
# Falls back to the legacy SSRF-validated fetch path when the env is unset.
|
|
304
320
|
export SOGNI_SKILL_USE_SDK_TRANSPORT=1
|
|
305
321
|
sogni-agent --api-workflow storyboard-video "10s neon city flyover"
|
|
@@ -508,6 +524,7 @@ Hosted API modes require `SOGNI_API_KEY`.
|
|
|
508
524
|
|
|
509
525
|
- **`--api-chat`** targets `/v1/chat/completions` with Sogni creative-agent tools — best for text-first natural-language workflows. The CLI sanitizes prompt-injection markers before forwarding messages and can use the current server-side creative-agent media tools, including video extension, segment replacement, overlays, subtitles, stitch/orbit/dance composition, and generated artifact indexing. Tune with `--api-tools creative-agent|creative-tools|none`, `--no-api-tool-execution`, `--llm-model`, and `--system`.
|
|
510
526
|
- **Sogni Intelligence controls** include `--task-profile general|coding|reasoning`, `--max-tokens`, and `--thinking` / `--no-thinking`, which forward to `/v1/chat/completions` as `task_profile`, `max_tokens`, and `chat_template_kwargs.enable_thinking`. Use `--list-api-models` or `--get-api-model <id>` to inspect `/v1/models`.
|
|
527
|
+
- **`--durable-chat`** starts a hosted `/v1/chat/runs` record through the SDK transport. Set `SOGNI_SKILL_USE_SDK_TRANSPORT=1` before using it. The CLI streams assistant deltas and de-duplicated per-job progress / ETA / result lines from hosted run events.
|
|
511
528
|
- **`--api-workflow`** targets `/v1/creative-agent/workflows` for durable, async workflow records with event streaming and cancellation. Requests carry `input.steps` plus snake_case controls such as `token_type`, `media_references`, `max_estimated_capacity_units`, and `confirm_cost`.
|
|
512
529
|
- **`--workflow-input`** forwards exact durable workflow JSON (`{ title?, steps: [...] }`). Use this when you need exact multi-step behavior such as repeated `replace_video_segment` steps with `replacementStartSeconds` / `replacementEndSeconds` for interleaved video slices.
|
|
513
530
|
- **`--api-workflow storyboard-video`** generates a storyline, creates a single GPT Image 2 storyboard sheet, then passes that artifact into Seedance as the video reference. The `-Q fast|hq|pro` preset maps to GPT Image 2 low/medium/high quality for that storyboard sheet.
|
|
@@ -601,7 +618,7 @@ With both repos checked out as siblings, refresh the generated runtime before pu
|
|
|
601
618
|
npm run sync:creative-agent-runtime
|
|
602
619
|
```
|
|
603
620
|
|
|
604
|
-
Reusable workflow rules should
|
|
621
|
+
Reusable workflow rules should come from the shared Sogni runtime before they are synced into this public package. Keep storyboard planning, tool argument validation, prompt linting, media-routing decisions, chat-run progress extraction, and repair/control behavior aligned with the hosted `/v1/chat/completions` and `/v1/creative-agent/workflows` APIs. Prefer typed helpers exported by `@sogni-ai/sogni-intelligence-client` or the generated runtime over new skill-local regex guards.
|
|
605
622
|
|
|
606
623
|
Public-skill regex should stay limited to CLI argument/fact extraction such as file paths, URLs, extensions, dimensions, durations, and explicit positions. Hosted-style decisions such as latest-video continuation, uploaded-video modification, image-selection waits, stitch-after-batch state, and repair/control routing belong upstream in typed planner/runtime fields before they are synced here.
|
|
607
624
|
|
package/SKILL.md
CHANGED
|
@@ -128,12 +128,16 @@ Path override environment variables:
|
|
|
128
128
|
|
|
129
129
|
## Recommended path: route through the hosted Sogni Intelligence endpoints
|
|
130
130
|
|
|
131
|
-
For any natural-language creative request — anything that should be planned, multi-step, or that benefits from tool selection, repair, or durable workflows — prefer the hosted endpoints over the direct-to-SDK flags. The hosted
|
|
131
|
+
For any natural-language creative request — anything that should be planned, multi-step, resumable, or that benefits from tool selection, repair, or durable workflows — prefer the hosted Sogni Intelligence endpoints over the direct-to-SDK media flags. The hosted surfaces are the canonical home for OpenAI-compatible chat, server-side creative tool dispatch, Structured Contracts v1 (gating policies, repair recipes, prompt contracts), durable chat runs, durable workflows, workflow templates, replay, and asset-manifest mapping. They stay aligned with `sogni-chat`, `sogni-api`, and the rest of the `@sogni/creative-agent` consumers.
|
|
132
132
|
|
|
133
133
|
```bash
|
|
134
134
|
# Natural-language creative request (LLM picks the tool, dispatches, repairs)
|
|
135
135
|
sogni-agent --api-chat "Turn the attached product photo into a launch poster" --ref product.jpg
|
|
136
136
|
|
|
137
|
+
# Durable hosted chat run (persisted event log + SSE stream)
|
|
138
|
+
SOGNI_SKILL_USE_SDK_TRANSPORT=1 sogni-agent --durable-chat \
|
|
139
|
+
"Create a four-shot launch campaign, generate the key art, and animate the hero clip"
|
|
140
|
+
|
|
137
141
|
# Multi-step durable workflow (resumable, replay-friendly, server-orchestrated)
|
|
138
142
|
sogni-agent --api-workflow \
|
|
139
143
|
--video-prompt "The camera slowly pushes in" \
|
|
@@ -196,11 +200,15 @@ sogni-agent --api-chat --ref product.jpg \
|
|
|
196
200
|
|
|
197
201
|
# Sogni Intelligence model/replay utilities
|
|
198
202
|
sogni-agent --list-api-models
|
|
199
|
-
sogni-agent --api-chat --task-profile reasoning --
|
|
203
|
+
sogni-agent --api-chat --task-profile reasoning --max-tokens 2000 \
|
|
200
204
|
"Plan a concise multi-step product launch workflow"
|
|
201
205
|
sogni-agent --list-replays 20
|
|
202
206
|
sogni-agent --get-replay run_abc123 --json
|
|
203
207
|
|
|
208
|
+
# Draft a savable workflow template through the hosted creative-agent tool loop
|
|
209
|
+
sogni-agent --api-chat \
|
|
210
|
+
"Design a reusable workflow for a 9:16 product teaser from one product photo"
|
|
211
|
+
|
|
204
212
|
# Durable API workflow: generated keyframe to video with resumable workflow record
|
|
205
213
|
sogni-agent --api-workflow \
|
|
206
214
|
--video-prompt "The camera slowly pushes in as the sketch comes alive" \
|
|
@@ -214,43 +222,128 @@ sogni-agent --api-workflow \
|
|
|
214
222
|
"Animate the referenced sketch"
|
|
215
223
|
|
|
216
224
|
# Exact durable workflow input with explicit steps
|
|
217
|
-
sogni-agent --api-workflow --workflow-input @workflow.json
|
|
225
|
+
sogni-agent --api-workflow --workflow-input @workflow-input.json \
|
|
226
|
+
--workflow-idempotency-key product-teaser-v1
|
|
218
227
|
|
|
219
228
|
# Durable storyboard-video workflow: storyline -> GPT Image 2 storyboard -> Seedance
|
|
220
229
|
sogni-agent --api-workflow storyboard-video --storyboard-frames 6 --duration 12 -Q hq \
|
|
221
230
|
"Create a 9:16 bakery launch video with a neon street-window reveal"
|
|
231
|
+
|
|
232
|
+
# Workflow management
|
|
233
|
+
sogni-agent --list-workflows
|
|
234
|
+
sogni-agent --resume-workflow wf_durable_workflow_123
|
|
222
235
|
```
|
|
223
236
|
|
|
224
237
|
Use `--api-chat` for text-first natural-language workflows that should go through
|
|
225
|
-
Sogni API's OpenAI-compatible
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
`
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
238
|
+
Sogni API's OpenAI-compatible `POST /v1/chat/completions` loop. The public
|
|
239
|
+
REST body uses snake_case controls such as `tool_choice`, `response_format`,
|
|
240
|
+
`task_profile`, `token_type`, `app_source`, `media_references`,
|
|
241
|
+
`chat_template_kwargs`, `sogni_tools`, and `sogni_tool_execution`. The endpoint
|
|
242
|
+
normalizes OpenAI `developer` messages to `system`; when a developer message is
|
|
243
|
+
present and no explicit `task_profile` is supplied, the server treats the task
|
|
244
|
+
as `coding`. The CLI sanitizes prompt-injection markers before forwarding
|
|
245
|
+
messages and sends API-key auth so hosted Sogni tools can execute server-side.
|
|
246
|
+
|
|
247
|
+
Hosted tool surfaces are split by `sogni_tools`:
|
|
248
|
+
|
|
249
|
+
- `creative-tools` is the public API default when `sogni_tools` is omitted or
|
|
250
|
+
true. It exposes generation/editing tools (`generate_image`,
|
|
251
|
+
`generate_video`, `generate_music`, `edit_image`, `apply_style`,
|
|
252
|
+
`restore_photo`, `refine_result`, `animate_photo`, `change_angle`,
|
|
253
|
+
`video_to_video`, `stitch_video`, `orbit_video`, `dance_montage`,
|
|
254
|
+
`sound_to_video`, `extend_video`, `replace_video_segment`, `overlay_video`,
|
|
255
|
+
`add_subtitles`), media-analysis tools (`analyze_image`, `analyze_video`,
|
|
256
|
+
`extract_metadata`), and lightweight composition tools (`enhance_prompt`,
|
|
257
|
+
`compose_lyrics`, `compose_instrumental`, `compose_script`).
|
|
258
|
+
- `creative-agent` is this CLI's default for `--api-chat`. It includes the
|
|
259
|
+
`creative-tools` surface plus session-control tools
|
|
260
|
+
(`ask_clarifying_question`, `finalize_response`), asset-manifest tools
|
|
261
|
+
(`create_asset_manifest`, `inspect_asset`, `label_asset`,
|
|
262
|
+
`map_assets_for_model`, `validate_asset_references`), and durable planning
|
|
263
|
+
tools (`compose_workflow`, `compose_workflow_template`). Use this surface
|
|
264
|
+
when the model should design one-shot workflow plans, draft savable workflow
|
|
265
|
+
templates, or maintain stable asset references across a multi-step turn.
|
|
266
|
+
- `none` disables Sogni tool injection and leaves only caller-supplied OpenAI
|
|
267
|
+
tools on raw API/SDK requests. In the CLI, use it with
|
|
268
|
+
`--no-api-tool-execution` when you want text-only planning without hosted
|
|
269
|
+
Sogni tool dispatch.
|
|
270
|
+
|
|
271
|
+
Use `--durable-chat` for long-running, LLM-in-the-loop turns that should be
|
|
272
|
+
persisted as `POST /v1/chat/runs` records instead of a single
|
|
273
|
+
`/v1/chat/completions` request. Chat runs keep an event log, stream via
|
|
274
|
+
`/v1/chat/runs/:id/events/stream`, support cancellation, and can pause for
|
|
275
|
+
persisted cost approval (`/v1/chat/runs/:id/confirm-cost`) in first-party
|
|
276
|
+
clients. The CLI can start and stream durable chat runs through the SDK
|
|
277
|
+
transport when `SOGNI_SKILL_USE_SDK_TRANSPORT=1` is set.
|
|
278
|
+
|
|
279
|
+
Use `--api-workflow` when the caller already knows it wants an async durable
|
|
280
|
+
workflow under `POST /v1/creative-agent/workflows`. The API now accepts either
|
|
281
|
+
an inline durable plan (`input.steps`) or a saved workflow template invocation
|
|
282
|
+
(`workflow_id` plus `inputs`) and rejects requests that provide both. The CLI's
|
|
283
|
+
generated-keyframe and `storyboard-video` presets submit inline `input.steps`;
|
|
284
|
+
`--workflow-input @workflow-input.json` supplies that `input` object directly.
|
|
285
|
+
Saved template CRUD lives at `/v1/creative-agent/workflows/templates`, and a
|
|
286
|
+
saved template can later be run by API/SDK callers with `workflow_id + inputs`.
|
|
287
|
+
Use `compose_workflow_template` through `--api-chat` to draft a savable template;
|
|
288
|
+
the caller is still responsible for persisting the returned `template_draft`.
|
|
289
|
+
|
|
290
|
+
Exact multi-step workflow plans should use explicit step dependencies, including
|
|
291
|
+
`replace_video_segment` steps with bounded `replacementStartSeconds` /
|
|
292
|
+
`replacementEndSeconds` when interleaving existing video slices. Workflow JSON
|
|
293
|
+
can bind request media into step arguments with `sourceStepId: "$input_media"`.
|
|
294
|
+
Use `--api-workflow storyboard-video` when the hosted sequence should generate a
|
|
295
|
+
storyline, create one GPT Image 2 storyboard sheet, and feed that image artifact
|
|
296
|
+
into Seedance as the video reference. The `-Q fast|hq|pro` preset maps to GPT
|
|
297
|
+
Image 2 low|medium|high quality for the storyboard sheet.
|
|
298
|
+
|
|
299
|
+
Hosted API requests forward media references from `-c`, `--ref`, `--ref-end`,
|
|
300
|
+
`--ref-audio`, `--reference-audio-identity`, and `--ref-video` as
|
|
301
|
+
`media_references` metadata. `--ref-audio` and `--ref-video` are repeatable in
|
|
302
|
+
api-chat / durable-chat mode — each entry uploads independently and is exposed
|
|
303
|
+
to the hosted LLM at `@Audio1` / `@Audio2` / `@Video1` etc. API chat also
|
|
304
|
+
attaches image refs as vision inputs. Local file references are uploaded to
|
|
305
|
+
Sogni media storage first, then forwarded as retrievable URLs for hosted chat
|
|
306
|
+
and durable workflows. Use the direct CLI path for private media that must not
|
|
307
|
+
leave the local machine.
|
|
308
|
+
|
|
309
|
+
### Seedance reference modes (mutually exclusive)
|
|
310
|
+
|
|
311
|
+
When `--video -m seedance2` or `-m seedance2-fast` is selected, the skill
|
|
312
|
+
exposes the same two-mode pattern that the hosted chat surfaces. Pick one
|
|
313
|
+
mode per video request:
|
|
314
|
+
|
|
315
|
+
- **Dedicated frame mode — `--ref` and/or `--ref-end`.** First-class
|
|
316
|
+
first-frame / last-frame anchoring; the Seedance worker pins them as
|
|
317
|
+
parameter-mode firstFrame / lastFrame. Max 2 images.
|
|
318
|
+
- **Loose reference mode — `-c/--context` plus optional `--ref-audio`
|
|
319
|
+
extras and `--ref-video` extras.** Anchor frame intent in the prompt with
|
|
320
|
+
`@Image1` / `@Image2` / `@Video1` / `@Audio1` etc. (e.g. *"Use @Image1 as
|
|
321
|
+
the opening shot reference"*). Supports up to 9 image refs, 3 video refs,
|
|
322
|
+
3 audio refs, and 12 total reference assets per video request. The
|
|
323
|
+
numeric caps come from the canonical
|
|
324
|
+
`@sogni-ai/sogni-protocol/catalogs/seedance-reference-limits.json` catalog,
|
|
325
|
+
surfaced through `@sogni-ai/sogni-intelligence-client/tools` as
|
|
326
|
+
`SEEDANCE_REFERENCE_LIMITS` and `validateSeedanceReferenceCounts()`.
|
|
327
|
+
|
|
328
|
+
Combining `--ref` / `--ref-end` with `-c/--context` on Seedance is rejected
|
|
329
|
+
client-side with a clear error pointing to the correct mode. In CLI direct-gen
|
|
330
|
+
mode, additional `--ref-audio` / `--ref-video` entries beyond the first must
|
|
331
|
+
be HTTPS URLs (the primary entry can still be a local file path); for local
|
|
332
|
+
multi-file Seedance uploads, use `--api-chat` / `--durable-chat` instead. Use
|
|
333
|
+
`--workflow-max-cost <n>` plus `--confirm-cost` / `--no-confirm-cost` to forward
|
|
334
|
+
explicit workflow cost policy, and `--workflow-idempotency-key` when retrying a
|
|
335
|
+
workflow start request.
|
|
336
|
+
|
|
337
|
+
Sogni Intelligence utilities are exposed through the same API-key path:
|
|
338
|
+
`--list-api-models` / `--get-api-model <id>` read `/v1/models`, `--task-profile`
|
|
339
|
+
and `--max-tokens` tune `/v1/chat/completions`, and `--list-replays`,
|
|
340
|
+
`--get-replay`, and `--ingest-replay` manage `/v1/replay/records` RunRecords for
|
|
341
|
+
replay/debug viewers. The public chat endpoint also accepts OpenAI-standard
|
|
342
|
+
`reasoning_effort` / `reasoning.effort` in raw API requests. The CLI's
|
|
343
|
+
`--thinking` / `--no-thinking` flags are forwarded as
|
|
344
|
+
`chat_template_kwargs.enable_thinking`; current hosted Qwen requests may
|
|
345
|
+
normalize thinking on server-side, so do not rely on `--no-thinking` as a hard
|
|
346
|
+
suppression switch for `/v1/chat/completions`.
|
|
254
347
|
Hosted API modes require `SOGNI_API_KEY`; this skill's CLI uses API-key
|
|
255
348
|
authentication.
|
|
256
349
|
|
|
@@ -263,15 +356,15 @@ SSRF-validated fetch path. The skill's `sogni-hosted-client.mjs`
|
|
|
263
356
|
factory still validates `restEndpoint` / `socketEndpoint` against the
|
|
264
357
|
SSRF guard before constructing the SDK client, so the safety contract
|
|
265
358
|
holds.
|
|
359
|
+
For `--durable-chat`, stream output as the run advances; the CLI reports
|
|
360
|
+
assistant deltas plus de-duplicated per-job progress / ETA / result lines from
|
|
361
|
+
hosted run events.
|
|
266
362
|
|
|
267
363
|
When changing hosted API chat/workflow behavior, keep reusable validation,
|
|
268
|
-
workflow compilation, repair-control, and guard telemetry logic in
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
`CreativeTurnPlannerFields`, `classifyMediaTurnIntent()`, `videoContinuation`,
|
|
273
|
-
`videoModification`, `outputGrouping`, `imageSelectionPolicy`, and
|
|
274
|
-
`pendingStitchAfterBatch`; regex is appropriate only for bounded CLI/fact
|
|
364
|
+
workflow compilation, repair-control, and guard telemetry logic in the shared
|
|
365
|
+
Sogni runtime first, then sync it into this public skill. The public skill
|
|
366
|
+
should consume generated or shared typed contracts instead of adding
|
|
367
|
+
skill-local regex guards. Keep local regex limited to bounded CLI/fact
|
|
275
368
|
extraction such as paths, URLs, extensions, dimensions, durations, and explicit
|
|
276
369
|
positions.
|
|
277
370
|
|
|
@@ -358,13 +451,15 @@ positions.
|
|
|
358
451
|
| `--concat-audio <path>` | Optional audio track to mux over `--concat-videos` output | - |
|
|
359
452
|
| `--concat-audio-start <sec>` | Start offset into `--concat-audio` | - |
|
|
360
453
|
| `--list-media [type]` | List recent inbound media (images\|audio\|all) | images |
|
|
361
|
-
| `--api-chat` | Call `/v1/chat/completions
|
|
362
|
-
| `--
|
|
454
|
+
| `--api-chat` | Call OpenAI-compatible `/v1/chat/completions`; CLI default sends the hosted `creative-agent` tool surface | - |
|
|
455
|
+
| `--durable-chat` | Start and stream a durable `/v1/chat/runs` record through SDK transport; requires `SOGNI_SKILL_USE_SDK_TRANSPORT=1` | - |
|
|
456
|
+
| `--api-tools <mode>` | API tool mode: creative-agent\|creative-tools\|none. CLI default is creative-agent; raw API default is creative-tools. | creative-agent |
|
|
363
457
|
| `--no-api-tool-execution` | Plan/tool-call via API chat without executing Sogni tools | - |
|
|
364
458
|
| `--llm-model <id>` | LLM model for `--api-chat` | qwen3.6-35b-a3b-gguf-iq4xs |
|
|
365
459
|
| `--task-profile <profile>` | Sogni Intelligence task profile: general\|coding\|reasoning | - |
|
|
366
460
|
| `--max-tokens <n>` | Max hosted chat completion tokens | 1600 |
|
|
367
|
-
| `--thinking`, `--no-thinking` |
|
|
461
|
+
| `--thinking`, `--no-thinking` | Forward `chat_template_kwargs.enable_thinking` for hosted chat; current public Qwen requests may normalize thinking on server-side | server default |
|
|
462
|
+
| `--system <text>` | Override the base system prompt for hosted chat | built-in creative assistant prompt |
|
|
368
463
|
| `--list-api-models`, `--get-api-model <id>` | Inspect Sogni Intelligence LLM model metadata | - |
|
|
369
464
|
| `--list-replays [n]`, `--get-replay <id>`, `--ingest-replay <json\|@path>` | Manage Sogni Intelligence replay RunRecords. List/get output is run through `redactRunRecord` from `@sogni/creative-agent/replay` before printing, so signed URLs, bearer tokens, JWTs, and PEM blocks cannot leak via the CLI. Use `@path` to load JSON from a file. | - |
|
|
370
465
|
| `--skip-redact`, `--no-redact` | Bypass the replay redactor on `--list-replays` / `--get-replay`. Debug-only — emits unredacted RunRecord payloads. | redacted |
|
|
@@ -376,9 +471,10 @@ positions.
|
|
|
376
471
|
| `--storyboard-plan-frames <n>` | Frame count for `--storyboard-plan`. | inferred |
|
|
377
472
|
| `--storyboard-plan-model <id>` | Adapter target for `--storyboard-plan` (seedance, seedance2, gpt-image-2, ltx23, wan). | inferred |
|
|
378
473
|
| `--storyboard-plan-stage <stage>` | Compilation stage for `--storyboard-plan` (storyboard_image, scene_clip). | storyboard_image |
|
|
379
|
-
| `--api-workflow` | Start
|
|
380
|
-
| `--workflow-input <json\|@path>` | Durable workflow input JSON. Use `@path` to load from a file. | - |
|
|
474
|
+
| `--api-workflow` | Start `/v1/creative-agent/workflows` with generated inline `input.steps`; optional `storyboard-video` preset | - |
|
|
475
|
+
| `--workflow-input <json\|@path>` | Durable workflow `input` JSON for the start request. Use `@path` to load from a file. | - |
|
|
381
476
|
| `--workflow-title <text>` | Title for generated or storyboard durable workflow input | - |
|
|
477
|
+
| `--workflow-idempotency-key <key>`, `--idempotency-key <key>` | Reuse safely when retrying a durable workflow start request | - |
|
|
382
478
|
| `--workflow-max-cost <n>` | Reject hosted workflow starts above this estimated capacity-unit ceiling | - |
|
|
383
479
|
| `--confirm-cost`, `--no-confirm-cost` | Forward explicit hosted workflow cost confirmation | - |
|
|
384
480
|
| `--storyboard-frames <n>` | Beat count for storyboard-video workflow | - |
|
|
@@ -387,7 +483,7 @@ positions.
|
|
|
387
483
|
| `--generate-audio`, `--no-generate-audio` | Toggle audio generation for generated video steps | - |
|
|
388
484
|
| `--expand-prompt`, `--no-expand-prompt` | Toggle prompt expansion for generated video steps | - |
|
|
389
485
|
| `--watch-workflow` | Stream durable workflow events after start | - |
|
|
390
|
-
| `--list-workflows`, `--get-workflow <id>`, `--workflow-events <id>`, `--stream-workflow <id>`, `--cancel-workflow <id>` | Durable workflow management helpers | - |
|
|
486
|
+
| `--list-workflows`, `--get-workflow <id>`, `--workflow-events <id>`, `--stream-workflow <id>`, `--cancel-workflow <id>`, `--resume-workflow <id>` | Durable workflow management helpers | - |
|
|
391
487
|
| `--api-base-url <url>` | Sogni API base for hosted API modes. Credentials are only sent to `https://api.sogni.ai` by default; use `SOGNI_API_ALLOWED_HOSTS` for trusted custom hosts or `SOGNI_ALLOW_UNSAFE_API_BASE_URL=1` for isolated local testing. | https://api.sogni.ai |
|
|
392
488
|
| `--no-filter` | Disable NSFW content filter | - |
|
|
393
489
|
| `--memory-set <key> <value>` | Save a user preference | - |
|
|
@@ -2285,7 +2285,7 @@ const PROMPT_CONTRACTS = [
|
|
|
2285
2285
|
"contractId": "sound_to_video_v1",
|
|
2286
2286
|
"version": "1.0.0",
|
|
2287
2287
|
"toolName": "sound_to_video",
|
|
2288
|
-
"baseDescription": "sound_to_video creates audio-synced video from an audio source. Works with uploaded audio\nfiles (mp3, m4a, wav) OR previously generated music from generate_music (auto-detected).\n\nWhen the user asks to \"turn that song/music into a video\" after generate_music, use\nsound_to_video — it will automatically find the generated audio.\n\nFor music visualization (syncing video to a specific song or audio track), use the\ngenerate_music → sound_to_video pipeline. Do NOT use animate_photo or generate_video for\naudio-driven visualization.\n\nanimate_photo and generate_video produce audio natively via LTX 2.3 — never pre-generate\naudio for those tools. sound_to_video is only for when the audio IS the primary creative\ninput driving the video output.",
|
|
2288
|
+
"baseDescription": "sound_to_video creates audio-synced video from an audio source. Works with uploaded audio\nfiles (mp3, m4a, wav) OR previously generated music from generate_music (auto-detected).\n\nWhen the user asks to \"turn that song/music into a video\" after generate_music, use\nsound_to_video — it will automatically find the generated audio.\n\nFor music visualization (syncing video to a specific song or audio track), use the\ngenerate_music → sound_to_video pipeline. Do NOT use animate_photo or generate_video for\naudio-driven visualization.\n\nanimate_photo and generate_video produce audio natively via LTX 2.3 — never pre-generate\naudio for those tools. sound_to_video is only for when the audio IS the primary creative\ninput driving the video output.\n\nPERSONA VOICE CLIPS: Persona voice clips returned by resolve_personas are voice identity\nreferences for LTX Audio ID, not audio tracks to synchronize. If the user explicitly asks\nfor a registered/persona/reference voice or voice clone, call resolve_personas first, then\nuse generate_video with ltx23 and voicePersonaName when there is no image source, or\nanimate_photo with ltx23 and voicePersonaName when an image/source frame should be\nanimated. Do not call sound_to_video for a persona voice clip unless a separate uploaded\naudio track is the primary sync driver.",
|
|
2289
2289
|
"parameterDocs": {
|
|
2290
2290
|
"audioSource": "Uploaded audio file or reference to a prior generate_music result. Auto-detected when omitted after generate_music."
|
|
2291
2291
|
}
|
|
@@ -2351,7 +2351,7 @@ const PROMPT_CONTRACTS = [
|
|
|
2351
2351
|
"contractId": "resolve_personas_v1",
|
|
2352
2352
|
"version": "1.0.0",
|
|
2353
2353
|
"toolName": "resolve_personas",
|
|
2354
|
-
"baseDescription": "resolve_personas is the required first step when the user explicitly names a saved Persona\nor says to use a Persona Image, Persona reference photo, Persona Voice, registered voice,\nor voice clone. Do not answer in prose, ask a follow-up, or finalize before calling this\ntool when a listed Persona name is present.\n\nDIRECT PERSONA IMAGE / VOICE VIDEO: If the user says to use the Persona image/reference\ndirectly/originally, call resolve_personas first, then call animate_photo using the injected\npersona photo as an uploaded image index. For one named Persona, use sourceImageIndex=-1\nor sourceImageIndices=[-1,...] for a multi-clip batch. If Persona Voice was explicitly\nrequested, set voicePersonaName to the exact resolved Persona name and use an
|
|
2354
|
+
"baseDescription": "resolve_personas is the required first step when the user explicitly names a saved Persona\nor says to use a Persona Image, Persona reference photo, Persona Voice, registered voice,\nor voice clone. Do not answer in prose, ask a follow-up, or finalize before calling this\ntool when a listed Persona name is present.\n\nDIRECT PERSONA IMAGE / VOICE VIDEO: If the user says to use the Persona image/reference\ndirectly/originally, call resolve_personas first, then call animate_photo using the injected\npersona photo as an uploaded image index. For one named Persona, use sourceImageIndex=-1\nor sourceImageIndices=[-1,...] for a multi-clip batch. If Persona Voice was explicitly\nrequested, set voicePersonaName to the exact resolved Persona name and use ltx23. Use\nanimate_photo when an image/source frame should be animated. Use generate_video with\nltx23 when the user requested a persona voice video but no image source exists yet.\nDo not call sound_to_video for Persona voice clips; they are voice identity references,\nnot the audio track to synchronize. Do not generate a new image first when the user\nexplicitly requested the existing Persona image directly.\n\nMULTI-CLIP PERSONA BATCHES: If the user asks for several separate clips from the same\nPersona Image, make one animate_photo call after resolve_personas with repeated persona\nsource indices, one prompt per clip, and the requested per-clip duration. If the user asks\nto stitch the clips, call stitch_video with the returned video indices after animate_photo.",
|
|
2355
2355
|
"parameterDocs": {
|
|
2356
2356
|
"names": "Persona names to load. Use the exact listed Persona name; call this before any Persona image/voice video or image generation."
|
|
2357
2357
|
}
|
package/llm.txt
CHANGED
|
@@ -11,6 +11,11 @@ runtimes.
|
|
|
11
11
|
npm install -g @sogni-ai/sogni-creative-agent-skill@latest
|
|
12
12
|
sogni-agent --version
|
|
13
13
|
|
|
14
|
+
# Claude Code plugin (requires the CLI install above; the plugin shells out to sogni-agent).
|
|
15
|
+
# Run both slash commands from inside Claude Code:
|
|
16
|
+
# /plugin marketplace add Sogni-AI/sogni-creative-agent-skill
|
|
17
|
+
# /plugin install sogni-creative-agent@sogni
|
|
18
|
+
|
|
14
19
|
# OpenClaw plugin
|
|
15
20
|
openclaw plugins install sogni-creative-agent-skill
|
|
16
21
|
|
|
@@ -67,6 +72,9 @@ for `/v1/chat/completions` with rich creative-agent tools and sanitized
|
|
|
67
72
|
message forwarding, or
|
|
68
73
|
`sogni-agent --api-workflow --video-prompt "motion" "image prompt"`
|
|
69
74
|
for durable `/v1/creative-agent/workflows` execution.
|
|
75
|
+
Use `SOGNI_SKILL_USE_SDK_TRANSPORT=1 sogni-agent --durable-chat "prompt"` for
|
|
76
|
+
durable `/v1/chat/runs` execution with SSE assistant deltas and per-job
|
|
77
|
+
progress / ETA / result events.
|
|
70
78
|
Sogni Intelligence utilities are available with `--list-api-models`,
|
|
71
79
|
`--get-api-model <id>`, `--task-profile general|coding|reasoning`,
|
|
72
80
|
`--max-tokens <n>`, `--thinking` / `--no-thinking`, `--list-replays [n]`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sogni-ai/sogni-creative-agent-skill",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Sogni Creative Agent Skill: agent skill and CLI for Sogni AI image, video, and music generation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "sogni-agent.mjs",
|
|
@@ -62,11 +62,12 @@
|
|
|
62
62
|
"openclaw.plugin.json",
|
|
63
63
|
"env.mjs",
|
|
64
64
|
"ssrf-guard.mjs",
|
|
65
|
+
"update-check.mjs",
|
|
65
66
|
"generated/creative-agent-runtime.mjs",
|
|
66
67
|
"sogni-agent.mjs"
|
|
67
68
|
],
|
|
68
69
|
"dependencies": {
|
|
69
|
-
"@sogni-ai/sogni-intelligence-client": "^2.
|
|
70
|
+
"@sogni-ai/sogni-intelligence-client": "^2.4.0",
|
|
70
71
|
"execa": "^9.6.1",
|
|
71
72
|
"json5": "^2.2.3",
|
|
72
73
|
"sharp": "^0.34.5"
|
|
@@ -77,11 +78,11 @@
|
|
|
77
78
|
]
|
|
78
79
|
},
|
|
79
80
|
"devDependencies": {
|
|
80
|
-
"@commitlint/cli": "^
|
|
81
|
-
"@commitlint/config-conventional": "^
|
|
81
|
+
"@commitlint/cli": "^21.0.1",
|
|
82
|
+
"@commitlint/config-conventional": "^21.0.1",
|
|
82
83
|
"@semantic-release/changelog": "^6.0.3",
|
|
83
84
|
"@semantic-release/git": "^10.0.1",
|
|
84
85
|
"husky": "^9.1.7",
|
|
85
|
-
"semantic-release": "^
|
|
86
|
+
"semantic-release": "^25.0.3"
|
|
86
87
|
}
|
|
87
88
|
}
|
package/skill-package.json
CHANGED
package/sogni-agent.mjs
CHANGED
|
@@ -14,6 +14,13 @@ import sharp from 'sharp';
|
|
|
14
14
|
import { getEnv, hasEnv } from './env.mjs';
|
|
15
15
|
import { PACKAGE_VERSION } from './version.mjs';
|
|
16
16
|
import { assertSafeUrl } from './ssrf-guard.mjs';
|
|
17
|
+
import {
|
|
18
|
+
INTERNAL_FLAG as UPDATE_CHECK_INTERNAL_FLAG,
|
|
19
|
+
runForegroundCheck as runUpdateCheckForeground,
|
|
20
|
+
maybeSpawnBackgroundCheck as maybeSpawnUpdateCheck,
|
|
21
|
+
getQueuedNotice as getUpdateCheckNotice,
|
|
22
|
+
runSelfUpdate as runSogniSelfUpdate,
|
|
23
|
+
} from './update-check.mjs';
|
|
17
24
|
import {
|
|
18
25
|
LTX23_WORKFLOW_MODELS,
|
|
19
26
|
PUBLIC_SKILL_DEFAULT_TOOL_DEFINITIONS,
|
|
@@ -56,6 +63,14 @@ import {
|
|
|
56
63
|
redactPayload,
|
|
57
64
|
redactRunRecord
|
|
58
65
|
} from '@sogni-ai/sogni-intelligence-client/replay';
|
|
66
|
+
import {
|
|
67
|
+
extractToolCallProgressUpdate
|
|
68
|
+
} from '@sogni-ai/sogni-intelligence-client/chatRun';
|
|
69
|
+
import {
|
|
70
|
+
SEEDANCE_REFERENCE_LIMITS,
|
|
71
|
+
SeedanceReferenceLimitError,
|
|
72
|
+
validateSeedanceReferenceCounts
|
|
73
|
+
} from '@sogni-ai/sogni-intelligence-client/tools';
|
|
59
74
|
|
|
60
75
|
const require = createRequire(import.meta.url);
|
|
61
76
|
const rootClientModule = process.env.SOGNI_AGENT_TEST_STATE_PATH
|
|
@@ -121,6 +136,26 @@ const IS_OPENCLAW_INVOCATION = Boolean(getEnv('OPENCLAW_PLUGIN_CONFIG'));
|
|
|
121
136
|
const RAW_ARGS = process.argv.slice(2);
|
|
122
137
|
const CLI_WANTS_JSON = RAW_ARGS.includes('--json');
|
|
123
138
|
const JSON_ERROR_MODE = CLI_WANTS_JSON || IS_OPENCLAW_INVOCATION;
|
|
139
|
+
|
|
140
|
+
// --- Update-check entry points --------------------------------------------
|
|
141
|
+
// Internal mode: the detached background child that fetches the npm registry.
|
|
142
|
+
if (RAW_ARGS[0] === UPDATE_CHECK_INTERNAL_FLAG) {
|
|
143
|
+
await runUpdateCheckForeground({ currentVersion: PACKAGE_VERSION });
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
// User-facing subcommand: `sogni-agent self-update`
|
|
147
|
+
if (RAW_ARGS[0] === 'self-update') {
|
|
148
|
+
process.exit(runSogniSelfUpdate({}));
|
|
149
|
+
}
|
|
150
|
+
// Fire-and-forget background check (no-op when throttled or skipped)
|
|
151
|
+
try { maybeSpawnUpdateCheck({ cliPath: process.argv[1] }); } catch { /* never break the CLI */ }
|
|
152
|
+
// Trailing notice on exit, if a newer version is on file
|
|
153
|
+
process.on('exit', () => {
|
|
154
|
+
try {
|
|
155
|
+
const notice = getUpdateCheckNotice({ currentVersion: PACKAGE_VERSION });
|
|
156
|
+
if (notice) process.stderr.write(notice + '\n');
|
|
157
|
+
} catch { /* never break exit */ }
|
|
158
|
+
});
|
|
124
159
|
const SOCKET_EVENT_SUBSCRIPTIONS = Object.freeze({
|
|
125
160
|
modelAvailability: false
|
|
126
161
|
});
|
|
@@ -1108,12 +1143,14 @@ const options = {
|
|
|
1108
1143
|
angles360Video: null,
|
|
1109
1144
|
refImage: null, // Reference image for video (start frame)
|
|
1110
1145
|
refImageEnd: null, // End frame for video interpolation
|
|
1111
|
-
refAudio: null, // Uploaded/generated audio for ia2v/a2v, or s2v lip-sync
|
|
1146
|
+
refAudio: null, // Uploaded/generated audio for ia2v/a2v, or s2v lip-sync (primary)
|
|
1147
|
+
refAudios: [], // Additional Seedance loose audio refs; first --ref-audio fills refAudio, subsequent calls append here
|
|
1112
1148
|
audioStart: null, // Optional start offset into reference audio
|
|
1113
1149
|
audioDuration: null, // Optional duration slice for reference audio
|
|
1114
1150
|
referenceAudioIdentity: null, // Voice identity reference for LTX native audio
|
|
1115
1151
|
voicePersonaName: null,
|
|
1116
|
-
refVideo: null, // Reference video for animate workflows
|
|
1152
|
+
refVideo: null, // Reference video for animate workflows (primary)
|
|
1153
|
+
refVideos: [], // Additional Seedance loose video refs; first --ref-video fills refVideo, subsequent calls append here
|
|
1117
1154
|
videoStart: null, // Optional start offset into reference video
|
|
1118
1155
|
contextImages: [], // Context images for image editing
|
|
1119
1156
|
looping: false, // Create looping video (i2v only): generate A→B then B→A and concatenate
|
|
@@ -1238,11 +1275,13 @@ const cliSet = {
|
|
|
1238
1275
|
refImage: false,
|
|
1239
1276
|
refImageEnd: false,
|
|
1240
1277
|
refAudio: false,
|
|
1278
|
+
refAudios: false,
|
|
1241
1279
|
audioStart: false,
|
|
1242
1280
|
audioDuration: false,
|
|
1243
1281
|
referenceAudioIdentity: false,
|
|
1244
1282
|
voicePersonaName: false,
|
|
1245
1283
|
refVideo: false,
|
|
1284
|
+
refVideos: false,
|
|
1246
1285
|
videoStart: false,
|
|
1247
1286
|
context: false,
|
|
1248
1287
|
looping: false,
|
|
@@ -1529,8 +1568,13 @@ for (let i = 0; i < args.length; i++) {
|
|
|
1529
1568
|
} else if (arg === '--ref-audio' || arg === '--audio') {
|
|
1530
1569
|
const raw = requireFlagValue(args, i, arg);
|
|
1531
1570
|
i++;
|
|
1532
|
-
options.refAudio
|
|
1533
|
-
|
|
1571
|
+
if (!options.refAudio) {
|
|
1572
|
+
options.refAudio = raw;
|
|
1573
|
+
cliSet.refAudio = true;
|
|
1574
|
+
} else {
|
|
1575
|
+
options.refAudios.push(raw);
|
|
1576
|
+
cliSet.refAudios = true;
|
|
1577
|
+
}
|
|
1534
1578
|
} else if (arg === '--audio-start') {
|
|
1535
1579
|
const raw = requireFlagValue(args, i, arg);
|
|
1536
1580
|
i++;
|
|
@@ -1554,8 +1598,13 @@ for (let i = 0; i < args.length; i++) {
|
|
|
1554
1598
|
} else if (arg === '--ref-video') {
|
|
1555
1599
|
const raw = requireFlagValue(args, i, arg);
|
|
1556
1600
|
i++;
|
|
1557
|
-
options.refVideo
|
|
1558
|
-
|
|
1601
|
+
if (!options.refVideo) {
|
|
1602
|
+
options.refVideo = raw;
|
|
1603
|
+
cliSet.refVideo = true;
|
|
1604
|
+
} else {
|
|
1605
|
+
options.refVideos.push(raw);
|
|
1606
|
+
cliSet.refVideos = true;
|
|
1607
|
+
}
|
|
1559
1608
|
} else if (arg === '--video-start' || arg === '--video-start-offset') {
|
|
1560
1609
|
const raw = requireFlagValue(args, i, arg);
|
|
1561
1610
|
i++;
|
|
@@ -1947,6 +1996,9 @@ for (let i = 0; i < args.length; i++) {
|
|
|
1947
1996
|
options.showBalance = true;
|
|
1948
1997
|
} else if (arg === '--version' || arg === '-V') {
|
|
1949
1998
|
options.showVersion = true;
|
|
1999
|
+
} else if (arg === '--no-update-check') {
|
|
2000
|
+
// Update-check opt-out handled at module load; no-op here so the parser
|
|
2001
|
+
// doesn't reject it as an unknown option.
|
|
1950
2002
|
} else if (arg === '--help') {
|
|
1951
2003
|
console.log(`
|
|
1952
2004
|
sogni-agent - Generate images, videos, and music using Sogni AI
|
|
@@ -2015,14 +2067,30 @@ Video Options:
|
|
|
2015
2067
|
--auto-resize-assets Auto-resize video reference assets (default)
|
|
2016
2068
|
--no-auto-resize-assets Disable auto-resize for video assets
|
|
2017
2069
|
--estimate-video-cost Estimate video cost and exit
|
|
2018
|
-
--ref <path|url> Reference image for video (start frame)
|
|
2019
|
-
--ref-end <path|url> End frame for interpolation/morphing
|
|
2020
|
-
--ref-audio <path|url>
|
|
2070
|
+
--ref <path|url> Reference image for video (start/first frame on Seedance)
|
|
2071
|
+
--ref-end <path|url> End frame for interpolation/morphing (last frame on Seedance)
|
|
2072
|
+
--ref-audio <path|url> Audio reference. Repeatable on Seedance models (up to 3 total);
|
|
2073
|
+
first entry is the primary, extras must be HTTPS URLs in CLI
|
|
2074
|
+
direct-gen (use --api-chat for multi local-file uploads).
|
|
2075
|
+
On LTX/WAN: single primary only (for ia2v/a2v/s2v lip-sync).
|
|
2021
2076
|
--audio-start <sec> Start offset into --ref-audio for audio-driven clips
|
|
2022
2077
|
--audio-duration <sec> Duration slice from --ref-audio
|
|
2023
2078
|
--reference-audio-identity <path> Voice identity clip for LTX native audio
|
|
2024
2079
|
--voice-persona <name> Use saved persona voice clip as LTX voice identity
|
|
2025
|
-
--ref-video <path|url>
|
|
2080
|
+
--ref-video <path|url> Video reference. Repeatable on Seedance models (up to 3 total);
|
|
2081
|
+
first entry is the primary, extras must be HTTPS URLs in CLI
|
|
2082
|
+
direct-gen. On LTX/WAN: single primary for animate/v2v workflows.
|
|
2083
|
+
|
|
2084
|
+
Seedance Reference Modes (mutually exclusive on seedance2 / seedance2-fast):
|
|
2085
|
+
- DEDICATED FRAME MODE: --ref (first frame) and/or --ref-end (last frame).
|
|
2086
|
+
Best when you want canonical first/last frame anchoring; max 2 images.
|
|
2087
|
+
- LOOSE REFERENCE MODE: -c/--context image refs plus optional --ref-audio /
|
|
2088
|
+
--ref-video extras. Anchor frame intent in the prompt with @Image1, @Image2,
|
|
2089
|
+
@Video1, @Audio1 etc. (e.g. "Use @Image1 as the opening shot reference").
|
|
2090
|
+
Up to 9 image / 3 video / 3 audio / 12 total references per video request.
|
|
2091
|
+
Combining --ref/--ref-end with -c/--context on Seedance is rejected client-side.
|
|
2092
|
+
All three modalities pull caps from the canonical
|
|
2093
|
+
@sogni-ai/sogni-protocol seedance-reference-limits catalog.
|
|
2026
2094
|
--video-start <sec> Start offset into --ref-video for segmented V2V/animate
|
|
2027
2095
|
--controlnet-name <n> ControlNet type for v2v: canny|pose|depth|detailer
|
|
2028
2096
|
--controlnet-strength <n> ControlNet strength for v2v (0.0-1.0, default: 0.8)
|
|
@@ -2076,6 +2144,8 @@ General:
|
|
|
2076
2144
|
--token-type <type> Token type: spark|sogni|auto (default: spark, auto retries with alternate)
|
|
2077
2145
|
--balance, --balances Show SPARK/SOGNI balances and exit
|
|
2078
2146
|
--version, -V Show sogni-agent version and exit
|
|
2147
|
+
--no-update-check Skip the once-daily npm update check for this run
|
|
2148
|
+
self-update Upgrade sogni-agent in place (npm/pnpm/yarn/bun auto-detected)
|
|
2079
2149
|
--extract-last-frame <video> <image> Extract last frame from a video (safe ffmpeg wrapper)
|
|
2080
2150
|
--concat-videos <out> <clips...> Concatenate video clips (safe ffmpeg wrapper, min 2 clips)
|
|
2081
2151
|
--concat-audio <path> Optional audio track to mux over --concat-videos output
|
|
@@ -2916,8 +2986,47 @@ if (options.video) {
|
|
|
2916
2986
|
if (options.videoStart !== null && !options.refVideo) {
|
|
2917
2987
|
fatalCliError('--video-start requires --ref-video.', { code: 'INVALID_ARGUMENT' });
|
|
2918
2988
|
}
|
|
2919
|
-
if (isSeedanceVideo && options.refAudio && !options.refImage && !options.refImageEnd && !options.refVideo
|
|
2920
|
-
|
|
2989
|
+
if (isSeedanceVideo && options.refAudio && !options.refImage && !options.refImageEnd && !options.refVideo
|
|
2990
|
+
&& (!Array.isArray(options.contextImages) || options.contextImages.length === 0)) {
|
|
2991
|
+
fatalCliError('Seedance audio references require --ref, --ref-video, or -c/--context image refs.', { code: 'INVALID_ARGUMENT' });
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
// Seedance reference modes are mutually exclusive:
|
|
2995
|
+
// - DEDICATED FRAME MODE: --ref (first frame) and/or --ref-end (last frame).
|
|
2996
|
+
// Up to 2 images; the platform pins them as parameter-mode firstFrame/lastFrame.
|
|
2997
|
+
// - LOOSE REFERENCE MODE: -c/--context (repeatable image refs), --ref-audio extras,
|
|
2998
|
+
// --ref-video extras. Up to 9 images / 3 videos / 3 audios / 12 total.
|
|
2999
|
+
// Anchor frame intent in the prompt with @Image1 / @Video1 / @Audio1 etc.
|
|
3000
|
+
// Mixing dedicated frames with loose image refs is rejected at sogni-socket
|
|
3001
|
+
// (jobsController.js) so we catch it client-side with a clearer message.
|
|
3002
|
+
if (isSeedanceVideo
|
|
3003
|
+
&& (options.refImage || options.refImageEnd)
|
|
3004
|
+
&& Array.isArray(options.contextImages) && options.contextImages.length > 0) {
|
|
3005
|
+
fatalCliError(
|
|
3006
|
+
'Seedance reference modes are mutually exclusive: --ref/--ref-end (dedicated first/last frame) cannot be combined with -c/--context (loose image references). '
|
|
3007
|
+
+ 'Pick one: use --ref/--ref-end for first-class first-frame/last-frame anchoring (max 2 images), '
|
|
3008
|
+
+ 'or use -c/--context (plus optional @Image1/@Image2 prompt language) for up to 9 loose image references.',
|
|
3009
|
+
{ code: 'INVALID_ARGUMENT', details: {
|
|
3010
|
+
dedicatedFrames: [options.refImage, options.refImageEnd].filter(Boolean),
|
|
3011
|
+
looseImageRefs: options.contextImages,
|
|
3012
|
+
} },
|
|
3013
|
+
);
|
|
3014
|
+
}
|
|
3015
|
+
// Non-Seedance video models do not understand multi-ref audio/video extras —
|
|
3016
|
+
// they only support a single primary --ref-audio / --ref-video each.
|
|
3017
|
+
if (!isSeedanceVideo) {
|
|
3018
|
+
if (Array.isArray(options.refAudios) && options.refAudios.length > 0) {
|
|
3019
|
+
fatalCliError('Multiple --ref-audio entries are only supported for Seedance models (seedance2, seedance2-fast).', {
|
|
3020
|
+
code: 'INVALID_ARGUMENT',
|
|
3021
|
+
details: { model: options.model, extras: options.refAudios },
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
if (Array.isArray(options.refVideos) && options.refVideos.length > 0) {
|
|
3025
|
+
fatalCliError('Multiple --ref-video entries are only supported for Seedance models (seedance2, seedance2-fast).', {
|
|
3026
|
+
code: 'INVALID_ARGUMENT',
|
|
3027
|
+
details: { model: options.model, extras: options.refVideos },
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
2921
3030
|
}
|
|
2922
3031
|
|
|
2923
3032
|
if (options.referenceAudioIdentity && !['t2v', 'i2v'].includes(options.videoWorkflow)) {
|
|
@@ -3246,8 +3355,9 @@ function apiRequestHeaders(apiKey, extra = {}) {
|
|
|
3246
3355
|
* Phase 6 P0 — SDK transport dispatch for hosted workflow operations.
|
|
3247
3356
|
*
|
|
3248
3357
|
* When `SOGNI_SKILL_USE_SDK_TRANSPORT=1` is set, route hosted workflow
|
|
3249
|
-
* start / get / list / events / cancel through
|
|
3250
|
-
*
|
|
3358
|
+
* start / get / list / events / cancel through
|
|
3359
|
+
* `@sogni-ai/sogni-intelligence-client`'s SDK-backed client via the
|
|
3360
|
+
* SSRF-validated `SogniHostedClientFactory` in
|
|
3251
3361
|
* `sogni-hosted-client.mjs`. Otherwise fall back to the legacy
|
|
3252
3362
|
* `fetchApiJson` path so existing users on older SDK versions are
|
|
3253
3363
|
* unaffected.
|
|
@@ -3325,8 +3435,9 @@ async function dispatchWorkflowActionViaSdk(action, apiKey, params) {
|
|
|
3325
3435
|
* Phase 6 P0 — SDK transport dispatch for hosted chat completions.
|
|
3326
3436
|
*
|
|
3327
3437
|
* When `SOGNI_SKILL_USE_SDK_TRANSPORT=1` is set, route synchronous
|
|
3328
|
-
* hosted chat through `@sogni-ai/sogni-client`
|
|
3329
|
-
* factory. The SDK's `chat.hosted.create`
|
|
3438
|
+
* hosted chat through `@sogni-ai/sogni-intelligence-client`'s SDK-backed
|
|
3439
|
+
* client via the SSRF-validated factory. The SDK's `chat.hosted.create`
|
|
3440
|
+
* accepts the same field
|
|
3330
3441
|
* names the legacy fetch sends (`model`, `messages`, `temperature`,
|
|
3331
3442
|
* `max_tokens`, `token_type`, `app_source`, `sogni_tools`,
|
|
3332
3443
|
* `sogni_tool_execution`, `task_profile`, `chat_template_kwargs`,
|
|
@@ -3464,8 +3575,14 @@ function getApiModeMediaReferences() {
|
|
|
3464
3575
|
if (options.refImage) refs.push({ flag: '--ref', value: options.refImage, kind: 'image' });
|
|
3465
3576
|
if (options.refImageEnd) refs.push({ flag: '--ref-end', value: options.refImageEnd, kind: 'image' });
|
|
3466
3577
|
if (options.refAudio) refs.push({ flag: '--ref-audio', value: options.refAudio, kind: 'audio' });
|
|
3578
|
+
for (const value of options.refAudios || []) {
|
|
3579
|
+
if (value) refs.push({ flag: '--ref-audio', value, kind: 'audio' });
|
|
3580
|
+
}
|
|
3467
3581
|
if (options.referenceAudioIdentity) refs.push({ flag: '--reference-audio-identity', value: options.referenceAudioIdentity, kind: 'audio' });
|
|
3468
3582
|
if (options.refVideo) refs.push({ flag: '--ref-video', value: options.refVideo, kind: 'video' });
|
|
3583
|
+
for (const value of options.refVideos || []) {
|
|
3584
|
+
if (value) refs.push({ flag: '--ref-video', value, kind: 'video' });
|
|
3585
|
+
}
|
|
3469
3586
|
return refs;
|
|
3470
3587
|
}
|
|
3471
3588
|
|
|
@@ -4014,6 +4131,17 @@ async function runApiChatDurable(log, { apiKey, body }) {
|
|
|
4014
4131
|
}
|
|
4015
4132
|
if (!options.json) log(`Durable chat run started: ${runId}`);
|
|
4016
4133
|
|
|
4134
|
+
// Per-job tool_call_progress dedupe state. The sogni-api throttled
|
|
4135
|
+
// emitter sends 1 Hz `jobETA` countdowns + per-step progress
|
|
4136
|
+
// ticks per job; we log only when the value actually changes
|
|
4137
|
+
// (and only in non-JSON CLI mode) so a 16-image batch doesn't
|
|
4138
|
+
// pour ~16 lines/sec into the log file.
|
|
4139
|
+
const perJobLogState = new Map();
|
|
4140
|
+
const logJobUpdate = (line) => {
|
|
4141
|
+
if (options.json) return;
|
|
4142
|
+
log(line);
|
|
4143
|
+
};
|
|
4144
|
+
|
|
4017
4145
|
for await (const event of helpers.sdkChatRunsStreamEvents(client, runId, {})) {
|
|
4018
4146
|
const type = event?.type || event?.event || '';
|
|
4019
4147
|
const payload = event?.data || event;
|
|
@@ -4028,6 +4156,50 @@ async function runApiChatDurable(log, { apiKey, body }) {
|
|
|
4028
4156
|
process.stdout.write(delta);
|
|
4029
4157
|
}
|
|
4030
4158
|
}
|
|
4159
|
+
// Per-job progress / ETA / completion / error log lines for
|
|
4160
|
+
// CLI watchers. The sogni-api `tool_call_progress` SSE event
|
|
4161
|
+
// packs `jobIndex` + per-job fields (`jobProgress`,
|
|
4162
|
+
// `jobEtaSeconds`, `resultUrl`, `jobError`) for vendor-emulated
|
|
4163
|
+
// jobs (GPT, Seedance — 1 Hz `jobETA` heartbeat from
|
|
4164
|
+
// sogni-socket) and real workers (per-step progress).
|
|
4165
|
+
// Untouched payloads from older sogni-api builds simply lack
|
|
4166
|
+
// `jobIndex` and skip this block — forward-compatible.
|
|
4167
|
+
if (type === 'tool_call_progress' && payload && typeof payload === 'object') {
|
|
4168
|
+
const {
|
|
4169
|
+
jobIndex,
|
|
4170
|
+
jobProgress,
|
|
4171
|
+
jobEtaSeconds,
|
|
4172
|
+
resultUrl,
|
|
4173
|
+
jobError,
|
|
4174
|
+
} = extractToolCallProgressUpdate(payload);
|
|
4175
|
+
if (jobIndex !== undefined) {
|
|
4176
|
+
const state = perJobLogState.get(jobIndex) ?? {};
|
|
4177
|
+
if (jobError && state.error !== jobError) {
|
|
4178
|
+
logJobUpdate(`[job ${jobIndex}] error: ${jobError}`);
|
|
4179
|
+
state.error = jobError;
|
|
4180
|
+
} else if (resultUrl && state.resultUrl !== resultUrl) {
|
|
4181
|
+
logJobUpdate(`[job ${jobIndex}] done${jobProgress !== undefined ? ` (${Math.round(jobProgress * 100)}%)` : ''} → ${resultUrl}`);
|
|
4182
|
+
state.resultUrl = resultUrl;
|
|
4183
|
+
state.progress = jobProgress ?? state.progress;
|
|
4184
|
+
} else if (jobProgress !== undefined || jobEtaSeconds !== undefined) {
|
|
4185
|
+
// Dedupe: only emit when progress moved >=5% or ETA changed.
|
|
4186
|
+
const pctBefore = state.progress !== undefined ? Math.round(state.progress * 100) : -1;
|
|
4187
|
+
const pctNow = jobProgress !== undefined ? Math.round(jobProgress * 100) : pctBefore;
|
|
4188
|
+
const progressChanged = jobProgress !== undefined && Math.abs(pctNow - pctBefore) >= 5;
|
|
4189
|
+
const etaChanged = jobEtaSeconds !== undefined && jobEtaSeconds !== state.eta;
|
|
4190
|
+
if (progressChanged || etaChanged) {
|
|
4191
|
+
const parts = [`[job ${jobIndex}]`];
|
|
4192
|
+
if (jobProgress !== undefined) parts.push(`${pctNow}%`);
|
|
4193
|
+
else if (state.progress !== undefined) parts.push(`${pctBefore}%`);
|
|
4194
|
+
if (jobEtaSeconds !== undefined) parts.push(`(${jobEtaSeconds}s)`);
|
|
4195
|
+
logJobUpdate(parts.join(' '));
|
|
4196
|
+
if (jobProgress !== undefined) state.progress = jobProgress;
|
|
4197
|
+
if (jobEtaSeconds !== undefined) state.eta = jobEtaSeconds;
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
perJobLogState.set(jobIndex, state);
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4031
4203
|
const eventToolCalls =
|
|
4032
4204
|
payload?.toolCalls
|
|
4033
4205
|
|| payload?.tool_calls
|
|
@@ -5306,6 +5478,51 @@ async function appendSafeSeedanceReferenceUrl(target, pathOrUrl, label) {
|
|
|
5306
5478
|
return true;
|
|
5307
5479
|
}
|
|
5308
5480
|
|
|
5481
|
+
// Effective Seedance reference counts for the current `options` snapshot.
|
|
5482
|
+
// Mirrors the per-modality bookkeeping sogni-chat does in
|
|
5483
|
+
// uploadedModalityReferenceIndices(...) (chatService.ts ~6149), translated to
|
|
5484
|
+
// the skill's primary + extras CLI shape:
|
|
5485
|
+
// images = refImage + refImageEnd + contextImages (loose Seedance @ImageN refs)
|
|
5486
|
+
// audios = refAudio + refAudios (extras)
|
|
5487
|
+
// videos = refVideo + refVideos (extras)
|
|
5488
|
+
function effectiveSeedanceReferenceCounts() {
|
|
5489
|
+
const images =
|
|
5490
|
+
(options.refImage ? 1 : 0)
|
|
5491
|
+
+ (options.refImageEnd ? 1 : 0)
|
|
5492
|
+
+ (Array.isArray(options.contextImages) ? options.contextImages.length : 0);
|
|
5493
|
+
const audios =
|
|
5494
|
+
(options.refAudio ? 1 : 0)
|
|
5495
|
+
+ (Array.isArray(options.refAudios) ? options.refAudios.length : 0);
|
|
5496
|
+
const videos =
|
|
5497
|
+
(options.refVideo ? 1 : 0)
|
|
5498
|
+
+ (Array.isArray(options.refVideos) ? options.refVideos.length : 0);
|
|
5499
|
+
return { images, audios, videos };
|
|
5500
|
+
}
|
|
5501
|
+
|
|
5502
|
+
// Wraps the shared validateSeedanceReferenceCounts() so a thrown
|
|
5503
|
+
// SeedanceReferenceLimitError is re-raised as a CLI fatal error with the same
|
|
5504
|
+
// human message the hosted chat surfaces. Source of truth for the numeric caps
|
|
5505
|
+
// (9 / 3 / 3 / 12) is @sogni-ai/sogni-protocol's seedance-reference-limits
|
|
5506
|
+
// catalog, surfaced through @sogni-ai/sogni-intelligence-client/tools.
|
|
5507
|
+
function enforceSeedanceReferenceCaps() {
|
|
5508
|
+
try {
|
|
5509
|
+
validateSeedanceReferenceCounts(effectiveSeedanceReferenceCounts());
|
|
5510
|
+
} catch (err) {
|
|
5511
|
+
if (err instanceof SeedanceReferenceLimitError) {
|
|
5512
|
+
fatalCliError(err.message, {
|
|
5513
|
+
code: err.code,
|
|
5514
|
+
details: {
|
|
5515
|
+
limitKind: err.limitKind,
|
|
5516
|
+
requestedCount: err.requestedCount,
|
|
5517
|
+
maxCount: err.maxCount,
|
|
5518
|
+
limits: SEEDANCE_REFERENCE_LIMITS,
|
|
5519
|
+
},
|
|
5520
|
+
});
|
|
5521
|
+
}
|
|
5522
|
+
throw err;
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5309
5526
|
function resolveMultiAngleOutputConfig(outputPath, outputFormat) {
|
|
5310
5527
|
if (!outputPath) return null;
|
|
5311
5528
|
const ext = extname(outputPath);
|
|
@@ -6499,6 +6716,11 @@ async function main() {
|
|
|
6499
6716
|
if (options.refVideo) log(`Reference video: ${options.refVideo}`);
|
|
6500
6717
|
|
|
6501
6718
|
const isSeedanceVideo = isSeedanceModel(options.model);
|
|
6719
|
+
if (isSeedanceVideo) {
|
|
6720
|
+
// Source of truth: @sogni-ai/sogni-protocol catalogs/seedance-reference-limits.json
|
|
6721
|
+
// surfaced through @sogni-ai/sogni-intelligence-client/tools.
|
|
6722
|
+
enforceSeedanceReferenceCaps();
|
|
6723
|
+
}
|
|
6502
6724
|
const seedanceReferenceImageUrls = [];
|
|
6503
6725
|
const seedanceReferenceVideoUrls = [];
|
|
6504
6726
|
const seedanceReferenceAudioUrls = [];
|
|
@@ -6518,6 +6740,45 @@ async function main() {
|
|
|
6518
6740
|
&& options.videoStart === null
|
|
6519
6741
|
&& await appendSafeSeedanceReferenceUrl(seedanceReferenceVideoUrls, options.refVideo, 'Reference video');
|
|
6520
6742
|
|
|
6743
|
+
// Seedance loose-reference extras: -c/--context images beyond start/end,
|
|
6744
|
+
// plus repeated --ref-audio / --ref-video entries past the first. The
|
|
6745
|
+
// Sogni Client SDK accepts only URL arrays for these (createJobRequestMessage),
|
|
6746
|
+
// so extras MUST be HTTPS URLs. For multi-file local uploads, use --api-chat /
|
|
6747
|
+
// --durable-chat where the LLM upload pipeline handles per-file uploads.
|
|
6748
|
+
if (isSeedanceVideo) {
|
|
6749
|
+
for (const ctxImage of (Array.isArray(options.contextImages) ? options.contextImages : [])) {
|
|
6750
|
+
if (!ctxImage) continue;
|
|
6751
|
+
if (!isHttpsUrl(ctxImage)) {
|
|
6752
|
+
fatalCliError(
|
|
6753
|
+
`Seedance extra image reference "${ctxImage}" must be an HTTPS URL. ` +
|
|
6754
|
+
'Local file uploads beyond --ref / --ref-end are only supported in --api-chat / --durable-chat mode.',
|
|
6755
|
+
{ code: 'INVALID_ARGUMENT', details: { flag: '-c/--context', value: ctxImage } },
|
|
6756
|
+
);
|
|
6757
|
+
}
|
|
6758
|
+
await appendSafeSeedanceReferenceUrl(seedanceReferenceImageUrls, ctxImage, 'Seedance image reference');
|
|
6759
|
+
}
|
|
6760
|
+
for (const extraAudio of options.refAudios) {
|
|
6761
|
+
if (!isHttpsUrl(extraAudio)) {
|
|
6762
|
+
fatalCliError(
|
|
6763
|
+
`Additional --ref-audio "${extraAudio}" must be an HTTPS URL. ` +
|
|
6764
|
+
'Local file uploads beyond the primary --ref-audio are only supported in --api-chat / --durable-chat mode.',
|
|
6765
|
+
{ code: 'INVALID_ARGUMENT', details: { flag: '--ref-audio', value: extraAudio } },
|
|
6766
|
+
);
|
|
6767
|
+
}
|
|
6768
|
+
await appendSafeSeedanceReferenceUrl(seedanceReferenceAudioUrls, extraAudio, 'Seedance audio reference');
|
|
6769
|
+
}
|
|
6770
|
+
for (const extraVideo of options.refVideos) {
|
|
6771
|
+
if (!isHttpsUrl(extraVideo)) {
|
|
6772
|
+
fatalCliError(
|
|
6773
|
+
`Additional --ref-video "${extraVideo}" must be an HTTPS URL. ` +
|
|
6774
|
+
'Local file uploads beyond the primary --ref-video are only supported in --api-chat / --durable-chat mode.',
|
|
6775
|
+
{ code: 'INVALID_ARGUMENT', details: { flag: '--ref-video', value: extraVideo } },
|
|
6776
|
+
);
|
|
6777
|
+
}
|
|
6778
|
+
await appendSafeSeedanceReferenceUrl(seedanceReferenceVideoUrls, extraVideo, 'Seedance video reference');
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6781
|
+
|
|
6521
6782
|
let imageBuffer = options.refImage && !useRefImageUrl ? await fetchMediaBuffer(options.refImage) : undefined;
|
|
6522
6783
|
let endImageBuffer = options.refImageEnd && !useRefImageEndUrl ? await fetchMediaBuffer(options.refImageEnd) : undefined;
|
|
6523
6784
|
let audioBuffer = options.refAudio && !useRefAudioUrl ? await fetchMediaBuffer(options.refAudio) : undefined;
|
package/update-check.mjs
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sogni-agent update check — trailing-notification style.
|
|
3
|
+
*
|
|
4
|
+
* Public API:
|
|
5
|
+
* shouldSkipForEnvironment(opts) → boolean (pure)
|
|
6
|
+
* compareSemver(a, b) → -1|0|1 (pure)
|
|
7
|
+
* detectPackageManager(env) → { manager, installCmd }
|
|
8
|
+
* formatUpdateNotice(opts) → string (pure)
|
|
9
|
+
* readState(path) → state | null
|
|
10
|
+
* writeState(path, state) → void
|
|
11
|
+
* runForegroundCheck(opts) → Promise<void> (used by --__update-check)
|
|
12
|
+
* maybeSpawnBackgroundCheck(opts) → 'spawned' | 'skipped' | 'fresh'
|
|
13
|
+
* getQueuedNotice(opts) → string | null
|
|
14
|
+
* runSelfUpdate(opts) → number (exit code)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawn, spawnSync } from 'child_process';
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
19
|
+
import { dirname, join } from 'path';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
import https from 'https';
|
|
22
|
+
|
|
23
|
+
export const PACKAGE_NAME = '@sogni-ai/sogni-creative-agent-skill';
|
|
24
|
+
export const DEFAULT_STATE_PATH = join(homedir(), '.config', 'sogni', 'update-check.json');
|
|
25
|
+
export const DEFAULT_THROTTLE_MS = 24 * 60 * 60 * 1000; // 24h
|
|
26
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
|
|
27
|
+
const REGISTRY_TIMEOUT_MS = 1500;
|
|
28
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
29
|
+
const INTERNAL_FLAG = '--__update-check';
|
|
30
|
+
|
|
31
|
+
export { INTERNAL_FLAG };
|
|
32
|
+
|
|
33
|
+
// ---------- pure helpers ----------
|
|
34
|
+
|
|
35
|
+
function parseSemverPart(value) {
|
|
36
|
+
const [main, prerelease] = String(value).split('-', 2);
|
|
37
|
+
const nums = main.split('.').map((n) => Number.parseInt(n, 10));
|
|
38
|
+
if (nums.length !== 3 || nums.some((n) => !Number.isFinite(n) || n < 0)) return null;
|
|
39
|
+
return { nums, prerelease: prerelease || '' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function compareSemver(a, b) {
|
|
43
|
+
const pa = parseSemverPart(a);
|
|
44
|
+
const pb = parseSemverPart(b);
|
|
45
|
+
if (!pa || !pb) return 0;
|
|
46
|
+
for (let i = 0; i < 3; i++) {
|
|
47
|
+
if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] < pb.nums[i] ? -1 : 1;
|
|
48
|
+
}
|
|
49
|
+
if (pa.prerelease === pb.prerelease) return 0;
|
|
50
|
+
if (!pa.prerelease) return 1;
|
|
51
|
+
if (!pb.prerelease) return -1;
|
|
52
|
+
return pa.prerelease < pb.prerelease ? -1 : 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function detectPackageManager(env = process.env) {
|
|
56
|
+
const ua = env.npm_config_user_agent || '';
|
|
57
|
+
if (ua.startsWith('pnpm/')) {
|
|
58
|
+
return { manager: 'pnpm', installCmd: `pnpm add -g ${PACKAGE_NAME}` };
|
|
59
|
+
}
|
|
60
|
+
if (ua.startsWith('yarn/')) {
|
|
61
|
+
return { manager: 'yarn', installCmd: `yarn global add ${PACKAGE_NAME}` };
|
|
62
|
+
}
|
|
63
|
+
if (ua.startsWith('bun/')) {
|
|
64
|
+
return { manager: 'bun', installCmd: `bun add -g ${PACKAGE_NAME}` };
|
|
65
|
+
}
|
|
66
|
+
return { manager: 'npm', installCmd: `npm install -g ${PACKAGE_NAME}` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function shouldSkipForEnvironment({
|
|
70
|
+
argv = process.argv,
|
|
71
|
+
env = process.env,
|
|
72
|
+
stderr = process.stderr,
|
|
73
|
+
cliPath = process.argv[1] || '',
|
|
74
|
+
} = {}) {
|
|
75
|
+
if (Array.isArray(argv) && argv.includes('--no-update-check')) return true;
|
|
76
|
+
if (env.SOGNI_NO_UPDATE_CHECK === '1' || env.SOGNI_NO_UPDATE_CHECK === 'true') return true;
|
|
77
|
+
if (env.NO_UPDATE_NOTIFIER === '1' || env.NO_UPDATE_NOTIFIER === 'true') return true;
|
|
78
|
+
if (env.CI) return true;
|
|
79
|
+
if (env.SOGNI_AGENT_TEST_STATE_PATH) return true;
|
|
80
|
+
if (env.OPENCLAW_PLUGIN_CONFIG) return true;
|
|
81
|
+
if (env.NODE_ENV === 'test') return true;
|
|
82
|
+
if (env.npm_lifecycle_event) return true; // running under `npm <script>`
|
|
83
|
+
if (Array.isArray(argv) && argv.includes('--json')) return true;
|
|
84
|
+
if (stderr && stderr.isTTY === false) return true;
|
|
85
|
+
// Dev / source checkout: CLI directory contains .git
|
|
86
|
+
if (cliPath) {
|
|
87
|
+
try {
|
|
88
|
+
const cliDir = dirname(cliPath);
|
|
89
|
+
if (existsSync(join(cliDir, '.git'))) return true;
|
|
90
|
+
} catch {
|
|
91
|
+
// ignore
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function formatUpdateNotice({
|
|
98
|
+
currentVersion,
|
|
99
|
+
latestVersion,
|
|
100
|
+
installCmd,
|
|
101
|
+
useColor,
|
|
102
|
+
} = {}) {
|
|
103
|
+
const color = useColor !== false && !process.env.NO_COLOR && process.stderr.isTTY;
|
|
104
|
+
const c = {
|
|
105
|
+
dim: color ? '\x1b[2m' : '',
|
|
106
|
+
bold: color ? '\x1b[1m' : '',
|
|
107
|
+
yellow: color ? '\x1b[33m' : '',
|
|
108
|
+
cyan: color ? '\x1b[36m' : '',
|
|
109
|
+
reset: color ? '\x1b[0m' : '',
|
|
110
|
+
};
|
|
111
|
+
const headline = `Update available ${c.dim}${currentVersion}${c.reset} → ${c.bold}${c.yellow}${latestVersion}${c.reset}`;
|
|
112
|
+
const cta = `Run ${c.cyan}${installCmd}${c.reset} to update`;
|
|
113
|
+
const tip = `${c.dim}(or run ${c.reset}${c.cyan}sogni-agent self-update${c.reset}${c.dim}, disable with --no-update-check)${c.reset}`;
|
|
114
|
+
return ['', headline, cta, tip, ''].join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------- state file ----------
|
|
118
|
+
|
|
119
|
+
export function readState(path = DEFAULT_STATE_PATH) {
|
|
120
|
+
try {
|
|
121
|
+
if (!existsSync(path)) return null;
|
|
122
|
+
const raw = readFileSync(path, 'utf8');
|
|
123
|
+
const parsed = JSON.parse(raw);
|
|
124
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
125
|
+
return parsed;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function writeState(path, state) {
|
|
132
|
+
try {
|
|
133
|
+
const dir = dirname(path);
|
|
134
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
135
|
+
writeFileSync(path, JSON.stringify(state, null, 2));
|
|
136
|
+
} catch {
|
|
137
|
+
// best-effort; never throw
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function clearState(path = DEFAULT_STATE_PATH) {
|
|
142
|
+
try {
|
|
143
|
+
if (existsSync(path)) unlinkSync(path);
|
|
144
|
+
} catch {
|
|
145
|
+
// ignore
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------- network ----------
|
|
150
|
+
|
|
151
|
+
function fetchLatestVersion({ url = REGISTRY_URL, timeoutMs = REGISTRY_TIMEOUT_MS } = {}) {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
let settled = false;
|
|
154
|
+
const finish = (fn, value) => {
|
|
155
|
+
if (settled) return;
|
|
156
|
+
settled = true;
|
|
157
|
+
fn(value);
|
|
158
|
+
};
|
|
159
|
+
let req;
|
|
160
|
+
try {
|
|
161
|
+
req = https.get(url, { headers: { accept: 'application/json' } }, (res) => {
|
|
162
|
+
if (res.statusCode !== 200) {
|
|
163
|
+
res.resume();
|
|
164
|
+
finish(reject, new Error(`registry status ${res.statusCode}`));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
let received = 0;
|
|
168
|
+
const chunks = [];
|
|
169
|
+
res.on('data', (chunk) => {
|
|
170
|
+
received += chunk.length;
|
|
171
|
+
if (received > MAX_RESPONSE_BYTES) {
|
|
172
|
+
res.destroy();
|
|
173
|
+
finish(reject, new Error('registry response too large'));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
chunks.push(chunk);
|
|
177
|
+
});
|
|
178
|
+
res.on('end', () => {
|
|
179
|
+
try {
|
|
180
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
181
|
+
const parsed = JSON.parse(body);
|
|
182
|
+
if (parsed && typeof parsed.version === 'string') {
|
|
183
|
+
finish(resolve, parsed.version);
|
|
184
|
+
} else {
|
|
185
|
+
finish(reject, new Error('registry response missing version'));
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
finish(reject, err);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
res.on('error', (err) => finish(reject, err));
|
|
192
|
+
});
|
|
193
|
+
} catch (err) {
|
|
194
|
+
finish(reject, err);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
req.setTimeout(timeoutMs, () => {
|
|
198
|
+
req.destroy(new Error('registry timeout'));
|
|
199
|
+
});
|
|
200
|
+
req.on('error', (err) => finish(reject, err));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------- foreground (child) check ----------
|
|
205
|
+
|
|
206
|
+
export async function runForegroundCheck({
|
|
207
|
+
currentVersion,
|
|
208
|
+
statePath = DEFAULT_STATE_PATH,
|
|
209
|
+
url = REGISTRY_URL,
|
|
210
|
+
timeoutMs = REGISTRY_TIMEOUT_MS,
|
|
211
|
+
fetcher = fetchLatestVersion,
|
|
212
|
+
now = Date.now,
|
|
213
|
+
} = {}) {
|
|
214
|
+
try {
|
|
215
|
+
const latest = await fetcher({ url, timeoutMs });
|
|
216
|
+
writeState(statePath, {
|
|
217
|
+
lastCheckedAt: now(),
|
|
218
|
+
lastKnownLatest: latest,
|
|
219
|
+
currentVersion: currentVersion || null,
|
|
220
|
+
});
|
|
221
|
+
} catch {
|
|
222
|
+
// Still record the attempt timestamp so we don't hammer the registry
|
|
223
|
+
// when offline. Keep any previously-known latest version so the user
|
|
224
|
+
// still sees the notice for an older known update.
|
|
225
|
+
const prev = readState(statePath) || {};
|
|
226
|
+
writeState(statePath, {
|
|
227
|
+
lastCheckedAt: now(),
|
|
228
|
+
lastKnownLatest: prev.lastKnownLatest || null,
|
|
229
|
+
currentVersion: currentVersion || null,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------- parent helpers ----------
|
|
235
|
+
|
|
236
|
+
export function maybeSpawnBackgroundCheck({
|
|
237
|
+
cliPath = process.argv[1],
|
|
238
|
+
statePath = DEFAULT_STATE_PATH,
|
|
239
|
+
throttleMs = DEFAULT_THROTTLE_MS,
|
|
240
|
+
now = Date.now,
|
|
241
|
+
spawnFn = spawn,
|
|
242
|
+
execPath = process.execPath,
|
|
243
|
+
env = process.env,
|
|
244
|
+
} = {}) {
|
|
245
|
+
if (shouldSkipForEnvironment({ env })) return 'skipped';
|
|
246
|
+
const state = readState(statePath);
|
|
247
|
+
if (state && typeof state.lastCheckedAt === 'number' && now() - state.lastCheckedAt < throttleMs) {
|
|
248
|
+
return 'fresh';
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const child = spawnFn(execPath, [cliPath, INTERNAL_FLAG], {
|
|
252
|
+
detached: true,
|
|
253
|
+
stdio: 'ignore',
|
|
254
|
+
env,
|
|
255
|
+
});
|
|
256
|
+
child.on('error', () => {});
|
|
257
|
+
if (typeof child.unref === 'function') child.unref();
|
|
258
|
+
return 'spawned';
|
|
259
|
+
} catch {
|
|
260
|
+
return 'skipped';
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function getQueuedNotice({
|
|
265
|
+
currentVersion,
|
|
266
|
+
statePath = DEFAULT_STATE_PATH,
|
|
267
|
+
env = process.env,
|
|
268
|
+
} = {}) {
|
|
269
|
+
if (shouldSkipForEnvironment({ env })) return null;
|
|
270
|
+
const state = readState(statePath);
|
|
271
|
+
if (!state || typeof state.lastKnownLatest !== 'string') return null;
|
|
272
|
+
if (compareSemver(state.lastKnownLatest, currentVersion) <= 0) return null;
|
|
273
|
+
const { installCmd } = detectPackageManager(env);
|
|
274
|
+
return formatUpdateNotice({
|
|
275
|
+
currentVersion,
|
|
276
|
+
latestVersion: state.lastKnownLatest,
|
|
277
|
+
installCmd,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function runSelfUpdate({
|
|
282
|
+
env = process.env,
|
|
283
|
+
statePath = DEFAULT_STATE_PATH,
|
|
284
|
+
spawnSyncFn = spawnSync,
|
|
285
|
+
stdio = 'inherit',
|
|
286
|
+
} = {}) {
|
|
287
|
+
const { manager, installCmd } = detectPackageManager(env);
|
|
288
|
+
const [command, ...args] = installCmd.split(' ');
|
|
289
|
+
console.error(`Running: ${installCmd}`);
|
|
290
|
+
const result = spawnSyncFn(command, args, { stdio, env });
|
|
291
|
+
if (result.error) {
|
|
292
|
+
console.error(`self-update failed: ${result.error.message}`);
|
|
293
|
+
if (manager === 'npm' && /EACCES|EPERM/i.test(result.error.message)) {
|
|
294
|
+
console.error('Hint: re-run with sudo, or install with a Node version manager (nvm/fnm/volta).');
|
|
295
|
+
}
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
299
|
+
return result.status;
|
|
300
|
+
}
|
|
301
|
+
clearState(statePath);
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
package/version.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '3.
|
|
1
|
+
export const PACKAGE_VERSION = '3.2.0';
|