@gentbajko/slopify 3.24.0 → 3.25.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/dist/adapter-registry.js +5 -4
- package/dist/adapters/explain.js +2 -1
- package/dist/adapters/host-cli/index.js +1 -1
- package/dist/adapters/host-cli/transport.js +1 -1
- package/dist/adapters/llm/antigravity-models.js +37 -0
- package/dist/adapters/llm/antigravity.js +247 -0
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/edge/docker-install/run.js +1 -1
- package/dist/edge/docker.js +2 -2
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/host-cli/runtime.js +4 -4
- package/dist/host-cli/status.js +23 -9
- package/dist/patch-notes/3.25.0.md +11 -0
- package/dist/patch-notes/3.25.1.md +5 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/fixes/rules.js +2 -2
- package/dist/slices/onboarding/quick-short.js +1 -1
- package/dist/slices/rebuild/recipe-build.js +2 -1
- package/dist/slices/rebuild/recipe-exports.js +29 -18
- package/dist/slices/rebuild/recipe-softening.js +42 -0
- package/dist/slices/reviews/model.js +1 -1
- package/dist/slices/run-cost/pricing.js +2 -1
- package/dist/slices/settings/first-run.js +1 -1
- package/dist/slices/settings/health.js +3 -7
- package/dist/slices/settings/model.js +2 -2
- package/dist/slices/settings/models.js +1 -1
- package/dist/tutorials/AI-CLIs.md +21 -8
- package/dist/tutorials/Docker.md +2 -2
- package/dist/tutorials/FAQ.md +2 -2
- package/dist/tutorials/First-Launch-and-Welcome.md +1 -1
- package/dist/tutorials/Home-and-Projects.md +1 -1
- package/dist/tutorials/Home.md +2 -2
- package/dist/tutorials/Install.md +1 -1
- package/dist/tutorials/Providers-and-Keys.md +3 -3
- package/dist/tutorials/Recovery-and-Retries.md +3 -3
- package/dist/tutorials/Troubleshooting.md +8 -4
- package/dist/tutorials/Your-First-Short.md +3 -3
- package/dist/web/assets/{index-D4ikiEys.js → index-D8LqNsCl.js} +6 -6
- package/dist/web/assets/{pdf-DSYcdduN.js → pdf-T7MCwjIv.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -2
- package/dist/adapters/llm/gemini-models.js +0 -127
- package/dist/adapters/llm/gemini-workspace.js +0 -135
- package/dist/adapters/llm/gemini.js +0 -168
- package/dist/host-cli/gemini-login.js +0 -83
package/dist/adapter-registry.js
CHANGED
|
@@ -4,13 +4,13 @@ import { falImage } from "./adapters/image/fal.js";
|
|
|
4
4
|
import { googleImage } from "./adapters/image/google.js";
|
|
5
5
|
import { openAiImage } from "./adapters/image/openai.js";
|
|
6
6
|
import { replicateImage } from "./adapters/image/replicate.js";
|
|
7
|
+
import { antigravityLlm } from "./adapters/llm/antigravity.js";
|
|
8
|
+
import { nodeAntigravityModels } from "./adapters/llm/antigravity-models.js";
|
|
7
9
|
import { claudeCodeLlm } from "./adapters/llm/claude-code.js";
|
|
8
10
|
import { nodeClaudeCodeModels } from "./adapters/llm/claude-code-models.js";
|
|
9
11
|
import { codexLlm } from "./adapters/llm/codex.js";
|
|
10
12
|
import { nodeCodexLimits } from "./adapters/llm/codex-limits.js";
|
|
11
13
|
import { nodeCodexModels } from "./adapters/llm/codex-models.js";
|
|
12
|
-
import { geminiLlm } from "./adapters/llm/gemini.js";
|
|
13
|
-
import { nodeGeminiModels } from "./adapters/llm/gemini-models.js";
|
|
14
14
|
import { openRouterLlm } from "./adapters/llm/openrouter.js";
|
|
15
15
|
import { cartesiaTts } from "./adapters/tts/cartesia.js";
|
|
16
16
|
import { elevenLabsTts } from "./adapters/tts/elevenlabs.js";
|
|
@@ -54,9 +54,10 @@ export function buildRegistry(deps) {
|
|
|
54
54
|
],
|
|
55
55
|
[
|
|
56
56
|
"gemini",
|
|
57
|
-
|
|
57
|
+
antigravityLlm({
|
|
58
58
|
run: cliFor("gemini"),
|
|
59
|
-
|
|
59
|
+
binary: () => cliBinary(deps.db, "gemini"),
|
|
60
|
+
readModels: () => nodeAntigravityModels(cliBinary(deps.db, "gemini")),
|
|
60
61
|
}),
|
|
61
62
|
],
|
|
62
63
|
]);
|
package/dist/adapters/explain.js
CHANGED
|
@@ -65,7 +65,8 @@ const cliNames = {
|
|
|
65
65
|
claude: "Claude Code CLI",
|
|
66
66
|
"claude-code": "Claude Code CLI",
|
|
67
67
|
codex: "Codex CLI",
|
|
68
|
-
gemini: "
|
|
68
|
+
gemini: "Antigravity CLI",
|
|
69
|
+
agy: "Antigravity CLI",
|
|
69
70
|
};
|
|
70
71
|
// A configured path (`/opt/bin/claude`, `C:\\tools\\codex.cmd`) still names the CLI the user chose.
|
|
71
72
|
export function cliName(binary) {
|
|
@@ -225,7 +225,7 @@ export function withPlanLimit(error, id) {
|
|
|
225
225
|
const planLimit = id === "claude-code"
|
|
226
226
|
? claudePlanLimit(error.message, 429, undefined)
|
|
227
227
|
: id === "gemini"
|
|
228
|
-
? /daily quota/i.test(error.message)
|
|
228
|
+
? /quota is used up|daily quota/i.test(error.message)
|
|
229
229
|
? { account: "gemini", resetsAt: null }
|
|
230
230
|
: undefined
|
|
231
231
|
: codexPlanLimit(error.message, new Date());
|
|
@@ -10,7 +10,7 @@ export function hostUnavailable(submitted = false) {
|
|
|
10
10
|
kind: "unavailable",
|
|
11
11
|
message: submitted
|
|
12
12
|
? "The connection to the host helper dropped before the CLI's answer arrived, so Slopify cannot tell whether it finished (it may already have used your quota). Check the host helper is still running, then use Try again."
|
|
13
|
-
: "Slopify cannot reach its host helper, which runs the Claude Code, Codex and
|
|
13
|
+
: "Slopify cannot reach its host helper, which runs the Claude Code, Codex and Antigravity CLIs outside Docker. On the host computer, run npx @gentbajko/slopify@latest --docker to set it up or restart it, then use Try again.",
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
async function tokenAt(directory) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { cliCommand } from "../../kernel/cli-command.js";
|
|
3
|
+
// `agy models` prints one model per line: its slug, a tab (spaces in Google's docs), then its
|
|
4
|
+
// display name. Lines that are not a slug are skipped.
|
|
5
|
+
export function parseAntigravityModels(text) {
|
|
6
|
+
const models = new Map();
|
|
7
|
+
for (const line of text.split(/\r?\n/).slice(0, 1000)) {
|
|
8
|
+
const match = /^\s*([a-z0-9][a-z0-9._-]{1,127})(?:\t+|\s{2,})(\S.{0,255}?)\s*$/.exec(line);
|
|
9
|
+
if (match?.[1] === undefined || match[2] === undefined || models.has(match[1]))
|
|
10
|
+
continue;
|
|
11
|
+
models.set(match[1], { id: match[1], name: match[2] });
|
|
12
|
+
}
|
|
13
|
+
return [...models.values()];
|
|
14
|
+
}
|
|
15
|
+
function nodeReadCliOutput(file, args) {
|
|
16
|
+
const command = cliCommand(file);
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
execFile(command.file, [...command.args, ...args], {
|
|
19
|
+
timeout: 15_000,
|
|
20
|
+
maxBuffer: 256 * 1024,
|
|
21
|
+
windowsHide: true,
|
|
22
|
+
env: { ...process.env, AGY_CLI_DISABLE_AUTO_UPDATE: "true" },
|
|
23
|
+
}, (error, stdout) => (error ? reject(error) : resolve(stdout)));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export async function nodeAntigravityModels(binary, read = nodeReadCliOutput) {
|
|
27
|
+
try {
|
|
28
|
+
const models = parseAntigravityModels(await read(binary, ["models"]));
|
|
29
|
+
if (models.length === 0)
|
|
30
|
+
throw new Error("No models listed");
|
|
31
|
+
return models;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Do not pass the CLI's raw output or home paths to the browser.
|
|
35
|
+
throw new Error('Slopify could not get the model list from the Antigravity CLI (it ran "agy models"). Check agy is installed and signed in (Settings → Providers), then refresh the list, or type a model slug that "agy models" shows.');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { redact } from "../../kernel/log.js";
|
|
6
|
+
import { documentMessages } from "../../kernel/ports/llm-documents.js";
|
|
7
|
+
import { providerError } from "../../kernel/ports/model.js";
|
|
8
|
+
import { cliCheck, cliName, cliReported, commandOf } from "../explain.js";
|
|
9
|
+
import { nodeAntigravityModels } from "./antigravity-models.js";
|
|
10
|
+
import { imageWorkspace, readFileName, unseenImages } from "./image-workspace.js";
|
|
11
|
+
import { cliInput, cliShaped, deliveredInput, endedWithout, promptOf, stopCliRun, stuckCli, } from "./run-cli.js";
|
|
12
|
+
import { lines } from "./sse-lines.js";
|
|
13
|
+
// Google retired the Gemini CLI for personal accounts on 2026-06-18; Antigravity CLI (`agy`)
|
|
14
|
+
// replaced it. Protocol from antigravity.google/docs/cli/headless.md: with
|
|
15
|
+
// `--input-format stream-json` the prompt is a `{"event":"user"}` line on stdin (a `-p` prompt
|
|
16
|
+
// would be dropped), closing stdin ends the session after that turn, and stdout carries
|
|
17
|
+
// `init`, `step_update` and one `result` event per turn.
|
|
18
|
+
export const antigravityBinary = "agy";
|
|
19
|
+
// No --dangerously-skip-permissions: headless agy refuses shell, MCP and page-reading tools,
|
|
20
|
+
// and its file tools stay inside the private folder the run starts in. Checked against agy
|
|
21
|
+
// 1.3.6 on 2026-10-15; its print timeout defaults to none, so Slopify's stall checks govern.
|
|
22
|
+
export function antigravityArgs(req) {
|
|
23
|
+
const level = req.thinkingConfig?.level;
|
|
24
|
+
const effort = level === "minimal" ? "low" : level;
|
|
25
|
+
return [
|
|
26
|
+
"--input-format",
|
|
27
|
+
"stream-json",
|
|
28
|
+
"--output-format",
|
|
29
|
+
"stream-json",
|
|
30
|
+
"--disable-slash-commands",
|
|
31
|
+
...(req.model === "" ? [] : ["--model", req.model]),
|
|
32
|
+
...(effort === undefined ? [] : ["--effort", effort]),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
const eventSchema = z.object({ event: z.string() });
|
|
36
|
+
// The file a finished view_file step opened, as agy 1.3.6 reports it.
|
|
37
|
+
const viewedSchema = z.object({
|
|
38
|
+
step_update: z.object({
|
|
39
|
+
state: z.string().optional(),
|
|
40
|
+
tool_name: z.string().optional(),
|
|
41
|
+
tool_info: z
|
|
42
|
+
.object({ parameters: z.object({ AbsolutePath: z.string().optional() }).optional() })
|
|
43
|
+
.optional(),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
const resultSchema = z.object({
|
|
47
|
+
result: z.object({
|
|
48
|
+
status: z.string(),
|
|
49
|
+
response: z.string().optional(),
|
|
50
|
+
error: z.string().optional(),
|
|
51
|
+
usage: z.object({ input_tokens: z.number(), output_tokens: z.number() }).optional(),
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
// agy reads AGENTS.md in its workspace as standing instructions. Headless agy ends the turn
|
|
55
|
+
// with no answer when the model reaches for a tool it may not use (page reading, the browser,
|
|
56
|
+
// commands), so the rules name the one web tool that needs no permission: search_web.
|
|
57
|
+
function instructions(webSearch, pictures) {
|
|
58
|
+
return [
|
|
59
|
+
"You are the writing and research component of Slopify, a video creation app.",
|
|
60
|
+
"Write the requested articles, narration, research notes or image prompts. Follow the topic, language, tone, length and format. Return only the requested content.",
|
|
61
|
+
webSearch
|
|
62
|
+
? "For anything you need from the web, use only the search_web tool. Never open web pages: do not use read_url_content, the browser or any browser_ tool; the search results' snippets and URLs are your sources."
|
|
63
|
+
: "Do not search the web or open web pages; answer from the supplied conversation.",
|
|
64
|
+
pictures
|
|
65
|
+
? "Open every picture file listed in the request with view_file before you answer; they are in the current folder and are the only files you may open."
|
|
66
|
+
: "Do not open, read or list any files.",
|
|
67
|
+
"Do not run commands, edit or create files, generate images or start subagents.",
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
export function antigravityLlm(deps) {
|
|
71
|
+
const now = deps.now ?? Date.now;
|
|
72
|
+
const binaryNow = () => (typeof deps.binary === "function" ? deps.binary() : deps.binary) ?? antigravityBinary;
|
|
73
|
+
async function* complete(req) {
|
|
74
|
+
req.signal.throwIfAborted();
|
|
75
|
+
const binary = binaryNow();
|
|
76
|
+
if (/^gemini$/i.test(commandOf(binary)))
|
|
77
|
+
throw providerError({
|
|
78
|
+
kind: "unsupported",
|
|
79
|
+
message: `The saved program path for the Antigravity CLI (${binary}) still points at the Gemini CLI, which Google has retired. In Settings → Providers, clear the Antigravity CLI path or set it to the agy program, then use Try again.`,
|
|
80
|
+
});
|
|
81
|
+
// The documents travel inline, as with OpenRouter: headless agy refuses MCP tools it was
|
|
82
|
+
// not pre-approved for, so a research reader tool would be silently unavailable.
|
|
83
|
+
const prompt = promptOf([...documentMessages(req.documents), ...req.messages]);
|
|
84
|
+
let directory;
|
|
85
|
+
let images;
|
|
86
|
+
let run;
|
|
87
|
+
let ended;
|
|
88
|
+
const viewed = new Set();
|
|
89
|
+
try {
|
|
90
|
+
// Pictures are copied into the folder agy works in, where view_file needs no permission.
|
|
91
|
+
images = imageWorkspace(req.images);
|
|
92
|
+
directory = images?.directory ?? mkdtempSync(join(tmpdir(), "slopify-antigravity-"));
|
|
93
|
+
writeFileSync(join(directory, "AGENTS.md"), instructions(req.webSearch === true, images !== undefined), { mode: 0o600 });
|
|
94
|
+
const pictures = images
|
|
95
|
+
? `Pictures to look at (open each with view_file):\n${images.files.map((one) => `- ${one.file}: ${one.name}`).join("\n")}\n\n`
|
|
96
|
+
: "";
|
|
97
|
+
const env = deps.env ?? process.env;
|
|
98
|
+
run = deps.run(binary, antigravityArgs(req), req.signal, {
|
|
99
|
+
cwd: directory,
|
|
100
|
+
env: { ...env, PWD: directory, AGY_CLI_DISABLE_AUTO_UPDATE: "true" },
|
|
101
|
+
// A leading sentence keeps a prompt that starts with "/" from reading as a slash command.
|
|
102
|
+
stdin: `${JSON.stringify({
|
|
103
|
+
event: "user",
|
|
104
|
+
message: {
|
|
105
|
+
content: cliInput(`Produce the requested content from this conversation:\n\n${pictures}${prompt}`),
|
|
106
|
+
},
|
|
107
|
+
})}\n`,
|
|
108
|
+
});
|
|
109
|
+
for await (const line of lines(run.stdout, req.signal)) {
|
|
110
|
+
if (line.trim() === "")
|
|
111
|
+
continue;
|
|
112
|
+
const value = parseLine(binary, line);
|
|
113
|
+
yield { type: "activity" };
|
|
114
|
+
req.signal.throwIfAborted();
|
|
115
|
+
if (value.event === "step_update" && images !== undefined) {
|
|
116
|
+
const step = viewedSchema.safeParse(value);
|
|
117
|
+
const update = step.success ? step.data.step_update : undefined;
|
|
118
|
+
const file = readFileName(update?.tool_info?.parameters?.AbsolutePath);
|
|
119
|
+
if (update?.tool_name === "view_file" && update.state === "DONE" && file)
|
|
120
|
+
viewed.add(file);
|
|
121
|
+
}
|
|
122
|
+
if (value.event !== "result")
|
|
123
|
+
continue;
|
|
124
|
+
const { result } = cliShaped(binary, resultSchema, value);
|
|
125
|
+
if (result.status !== "SUCCESS")
|
|
126
|
+
throw failure(binary, req.model, result.error ?? result.status, undefined, now);
|
|
127
|
+
await deliveredInput(run);
|
|
128
|
+
if (result.response === undefined || result.response.trim() === "")
|
|
129
|
+
throw emptyAnswer(binary, run.stderr());
|
|
130
|
+
if (images !== undefined && images.unread(viewed).length > 0)
|
|
131
|
+
throw unseenImages();
|
|
132
|
+
yield { type: "delta", text: result.response };
|
|
133
|
+
yield {
|
|
134
|
+
type: "done",
|
|
135
|
+
usage: result.usage === undefined
|
|
136
|
+
? null
|
|
137
|
+
: {
|
|
138
|
+
inputTokens: result.usage.input_tokens,
|
|
139
|
+
outputTokens: result.usage.output_tokens,
|
|
140
|
+
},
|
|
141
|
+
finishReason: null,
|
|
142
|
+
};
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
req.signal.throwIfAborted();
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
try {
|
|
152
|
+
if (run !== undefined)
|
|
153
|
+
ended = await stopCliRun(run);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
if (directory !== undefined)
|
|
157
|
+
rmSync(directory, { recursive: true, force: true, maxRetries: 3 });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
req.signal.throwIfAborted();
|
|
161
|
+
if (run === undefined)
|
|
162
|
+
throw failure(binary, req.model, "", `The ${cliName(binary)} could not be started. Check it is installed and set up in Settings → Providers, then use Try again.`);
|
|
163
|
+
if (authRequired(run.stderr()))
|
|
164
|
+
throw authFailure();
|
|
165
|
+
throw failure(binary, req.model, run.stderr(), ended === undefined ? stuckCli(binary) : endedWithout(binary, ended, run.stderr()));
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
id: "gemini",
|
|
169
|
+
// search_web runs headless without a permission rule (checked with agy 1.3.6).
|
|
170
|
+
capabilities: { streams: false, reportsUsage: true, webSearch: true, images: true },
|
|
171
|
+
models: deps.readModels ?? (() => nodeAntigravityModels(binaryNow())),
|
|
172
|
+
complete,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function parseLine(binary, line) {
|
|
176
|
+
let value;
|
|
177
|
+
try {
|
|
178
|
+
value = JSON.parse(line);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
value = undefined;
|
|
182
|
+
}
|
|
183
|
+
// Keep every key: the result schema reads the rest of the event.
|
|
184
|
+
cliShaped(binary, eventSchema, value);
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
// agy 1.3.6 ends a turn whose tool it refused with SUCCESS, no text and exit 0, and says
|
|
188
|
+
// why only on stderr.
|
|
189
|
+
function emptyAnswer(binary, stderr) {
|
|
190
|
+
if (/no output produced|auto-denied|permission/i.test(stderr))
|
|
191
|
+
return providerError({
|
|
192
|
+
kind: "other",
|
|
193
|
+
message: `The ${cliName(binary)} tried to use a tool Slopify does not allow (it said: ${redact(stderr.trim()).slice(0, 300)}), so it gave no answer. Use Try again; if it keeps happening, choose another model or text provider in the Providers section of Edit project.`,
|
|
194
|
+
});
|
|
195
|
+
return providerError({
|
|
196
|
+
kind: "other",
|
|
197
|
+
message: `The ${cliName(binary)} answered with no text. Use Try again; if it keeps happening, run agy in a terminal to check it works, or choose another model in the Providers section of Edit project.`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// `message` is what the CLI said; `sentence` replaces the default wording when Slopify
|
|
201
|
+
// already knows what happened.
|
|
202
|
+
function failure(binary, model, message, sentence, now = Date.now) {
|
|
203
|
+
if (authRequired(message))
|
|
204
|
+
return authFailure();
|
|
205
|
+
if (/invalid model selection/i.test(message))
|
|
206
|
+
return providerError({
|
|
207
|
+
kind: "unsupported",
|
|
208
|
+
message: `The ${cliName(binary)} does not offer the model "${model}". Run "agy models" in a terminal to see the ones it has, pick one in the Edit tab (Edit project → Providers), then use Continue the run.`,
|
|
209
|
+
});
|
|
210
|
+
if (/quota|resource.?exhausted|capacity/i.test(message) &&
|
|
211
|
+
/exhaust|exceed|used up|reached|limit/i.test(message)) {
|
|
212
|
+
// agy 1.3.6 says "Resets in 0s" for a per-minute capacity limit: retry it like any rate
|
|
213
|
+
// limit. A longer or unnamed reset is the plan's allowance, waited out until it returns.
|
|
214
|
+
const reset = /Resets in\s+(?:(\d+)h)?\s*(?:(\d+)m)?\s*(?:(\d+)s)?/i.exec(message);
|
|
215
|
+
const waitMs = reset === null || reset[0].trim() === "Resets in"
|
|
216
|
+
? undefined
|
|
217
|
+
: ((Number(reset[1] ?? 0) * 60 + Number(reset[2] ?? 0)) * 60 + Number(reset[3] ?? 0)) *
|
|
218
|
+
1000;
|
|
219
|
+
if (waitMs !== undefined && waitMs <= 5 * 60_000)
|
|
220
|
+
return providerError({
|
|
221
|
+
kind: "rate_limit",
|
|
222
|
+
message: `The ${cliName(binary)} is busy: Google's capacity for this model is used for the moment (it said: ${redact(message)}). Slopify tries again by itself shortly; if it keeps happening, run fewer projects at once or choose another model in the Providers section of Edit project.`,
|
|
223
|
+
retryAfterMs: Math.max(1000, waitMs),
|
|
224
|
+
});
|
|
225
|
+
return providerError({
|
|
226
|
+
kind: "rate_limit",
|
|
227
|
+
message: `Your Antigravity plan's quota is used up (the ${cliName(binary)} said: ${redact(message)}). Slopify waits and tries again by itself when it resets.`,
|
|
228
|
+
planLimit: {
|
|
229
|
+
account: "gemini",
|
|
230
|
+
resetsAt: waitMs === undefined ? null : new Date(now() + waitMs).toISOString(),
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return providerError({
|
|
235
|
+
kind: "other",
|
|
236
|
+
message: sentence ?? cliReported(binary, redact(message), cliCheck(binary)),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function authRequired(message) {
|
|
240
|
+
return /authentication (?:required|failed)|Please sign in|not (?:signed|logged) in|sign.?in (?:has )?expired/i.test(message);
|
|
241
|
+
}
|
|
242
|
+
function authFailure() {
|
|
243
|
+
return providerError({
|
|
244
|
+
kind: "missing_key",
|
|
245
|
+
message: 'The Antigravity CLI is not signed in, or its sign-in has expired. Open a terminal on the computer running it, run "agy" and sign in with your Google account, then use Try again.',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
@@ -91,7 +91,7 @@ export async function runDockerCommand(command) {
|
|
|
91
91
|
console.log(hostCli.enabled
|
|
92
92
|
? "Host CLI bridge ready: Claude Code, Codex and Gemini run on this machine with their existing logins."
|
|
93
93
|
: command.hostCli === "off"
|
|
94
|
-
? "Using API keys only (--host-cli=off). Leave that option out to use the Claude Code, Codex or
|
|
94
|
+
? "Using API keys only (--host-cli=off). Leave that option out to use the Claude Code, Codex or Antigravity CLI installed on this machine."
|
|
95
95
|
: "Using API keys only. Install Claude Code, Codex or Gemini on this machine and run the command again to use them.");
|
|
96
96
|
if (result.recovery)
|
|
97
97
|
console.log(`Recovery copy of your data: Docker volume ${result.recovery}`);
|
package/dist/edge/docker.js
CHANGED
|
@@ -53,8 +53,8 @@ export async function planDockerHostCli(options) {
|
|
|
53
53
|
}
|
|
54
54
|
if (!approved) {
|
|
55
55
|
if (!options.interactive)
|
|
56
|
-
throw new Error("Slopify needs your OK to use the Claude Code, Codex or
|
|
57
|
-
approved = await options.prompt("Allow Slopify to run your host Claude Code, Codex and
|
|
56
|
+
throw new Error("Slopify needs your OK to use the Claude Code, Codex or Antigravity CLI installed on this machine, but can't ask because this isn't an interactive terminal. Rerun with --accept-host-cli to allow it, or with --host-cli=off to use API keys only.");
|
|
57
|
+
approved = await options.prompt("Allow Slopify to run your host Claude Code, Codex and Antigravity CLIs using their existing logins? This installs a private helper and enables automatic startup. User lingering keeps your user services running after logout and at boot. Credentials stay on the host. [y/N] ");
|
|
58
58
|
if (!approved)
|
|
59
59
|
throw new Error("You declined, so Slopify did not set up your host AI CLIs and nothing was changed. Run the launcher again and answer y to allow it, or add --host-cli=off to start with API keys only.");
|
|
60
60
|
}
|
|
Binary file
|
|
Binary file
|
package/dist/host-cli/runtime.js
CHANGED
|
@@ -2,13 +2,13 @@ import { mkdtemp, rm } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { codexImage, codexImageTimeoutMs } from "../adapters/image/codex.js";
|
|
5
|
+
import { antigravityLlm } from "../adapters/llm/antigravity.js";
|
|
6
|
+
import { nodeAntigravityModels } from "../adapters/llm/antigravity-models.js";
|
|
5
7
|
import { claudeCodeLlm } from "../adapters/llm/claude-code.js";
|
|
6
8
|
import { nodeClaudeCodeModels } from "../adapters/llm/claude-code-models.js";
|
|
7
9
|
import { codexLlm } from "../adapters/llm/codex.js";
|
|
8
10
|
import { nodeCodexLimits } from "../adapters/llm/codex-limits.js";
|
|
9
11
|
import { nodeCodexModels } from "../adapters/llm/codex-models.js";
|
|
10
|
-
import { geminiLlm } from "../adapters/llm/gemini.js";
|
|
11
|
-
import { nodeGeminiModels } from "../adapters/llm/gemini-models.js";
|
|
12
12
|
import { providerError } from "../kernel/ports/model.js";
|
|
13
13
|
import { createHostFolderOpener } from "./open-folder.js";
|
|
14
14
|
import { createHostStatus } from "./status.js";
|
|
@@ -24,14 +24,14 @@ export function createHostRuntime(deps) {
|
|
|
24
24
|
});
|
|
25
25
|
return deps.resolve(id === "codex-image" ? "codex" : id);
|
|
26
26
|
}
|
|
27
|
-
const factories = { "claude-code": claudeCodeLlm, codex: codexLlm, gemini:
|
|
27
|
+
const factories = { "claude-code": claudeCodeLlm, codex: codexLlm, gemini: antigravityLlm };
|
|
28
28
|
const models = async (id) => {
|
|
29
29
|
const binary = await deps.resolve(id);
|
|
30
30
|
return id === "claude-code"
|
|
31
31
|
? nodeClaudeCodeModels(binary)
|
|
32
32
|
: id === "codex"
|
|
33
33
|
? nodeCodexModels(deps.env, binary)
|
|
34
|
-
:
|
|
34
|
+
: nodeAntigravityModels(binary);
|
|
35
35
|
};
|
|
36
36
|
return {
|
|
37
37
|
status,
|
package/dist/host-cli/status.js
CHANGED
|
@@ -3,12 +3,16 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { access, stat } from "node:fs/promises";
|
|
4
4
|
import { delimiter, isAbsolute, join } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { parseAntigravityModels } from "../adapters/llm/antigravity-models.js";
|
|
6
7
|
import { cliLoginError } from "../adapters/llm/cli-login-error.js";
|
|
7
8
|
import { cliCommand } from "../kernel/cli-command.js";
|
|
8
9
|
import { cliProbeTimeoutMs, readinessFromProbe, } from "../slices/settings/cli-status.js";
|
|
9
|
-
import { nodeGeminiLoginHost, readGeminiLogin } from "./gemini-login.js";
|
|
10
10
|
export function hostCommandName(id) {
|
|
11
|
-
|
|
11
|
+
if (id === "claude-code")
|
|
12
|
+
return "claude";
|
|
13
|
+
if (id === "gemini")
|
|
14
|
+
return "agy";
|
|
15
|
+
return id === "codex-image" ? "codex" : id;
|
|
12
16
|
}
|
|
13
17
|
export async function resolveHostCommand(id, env) {
|
|
14
18
|
for (const directory of (env.PATH ?? "").split(delimiter)) {
|
|
@@ -31,8 +35,14 @@ export async function resolveHostCommand(id, env) {
|
|
|
31
35
|
}
|
|
32
36
|
const authSchema = z.object({ loggedIn: z.boolean() });
|
|
33
37
|
export function parseHostLogin(id, stdout, stderr) {
|
|
34
|
-
if (id === "gemini")
|
|
35
|
-
|
|
38
|
+
if (id === "gemini") {
|
|
39
|
+
// agy has no status command; "agy models" lists models only when signed in (agy 1.3.6).
|
|
40
|
+
if (parseAntigravityModels(stdout).length > 0)
|
|
41
|
+
return "signed-in";
|
|
42
|
+
return /Please sign in|authentication required/i.test(`${stdout}\n${stderr}`)
|
|
43
|
+
? "signed-out"
|
|
44
|
+
: "unknown";
|
|
45
|
+
}
|
|
36
46
|
if (id === "codex") {
|
|
37
47
|
const text = `${stdout}\n${stderr}`.trim();
|
|
38
48
|
if (/^Not logged in\.?$/i.test(text))
|
|
@@ -49,13 +59,17 @@ export function parseHostLogin(id, stdout, stderr) {
|
|
|
49
59
|
return "unknown";
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
|
-
export async function readHostLogin(binary, id, signal
|
|
53
|
-
// The Gemini CLI has no status command; its settings and credential files answer instead.
|
|
54
|
-
if (id === "gemini")
|
|
55
|
-
return readGeminiLogin(gemini);
|
|
62
|
+
export async function readHostLogin(binary, id, signal) {
|
|
56
63
|
const command = cliCommand(binary);
|
|
57
64
|
return new Promise((resolve) => {
|
|
58
|
-
execFile(command.file, [
|
|
65
|
+
execFile(command.file, [
|
|
66
|
+
...command.args,
|
|
67
|
+
...(id === "claude-code"
|
|
68
|
+
? ["auth", "status"]
|
|
69
|
+
: id === "gemini"
|
|
70
|
+
? ["models"]
|
|
71
|
+
: ["login", "status"]),
|
|
72
|
+
], { timeout: cliProbeTimeoutMs, maxBuffer: 64 * 1024, signal, windowsHide: true }, (error, stdout, stderr) => {
|
|
59
73
|
if (error && (error.killed || typeof error.code !== "number"))
|
|
60
74
|
return resolve("unknown");
|
|
61
75
|
resolve(parseHostLogin(id, stdout, stderr));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Slopify 3.25.0
|
|
2
|
+
|
|
3
|
+
Released 16 October 2026.
|
|
4
|
+
|
|
5
|
+
- **The Antigravity CLI replaces the Gemini CLI.** Google retired the Gemini CLI for personal accounts and moved them to Antigravity. Settings → Providers now shows **Antigravity CLI** where Gemini CLI was, and projects that chose it keep that choice. Install it with `curl -fsSL https://antigravity.google/cli/install.sh | bash`, then run `agy` once in a terminal to sign in with your Google account. Research searches the web with it, and picture reviews work.
|
|
6
|
+
- **Turning on Soften breaths or Soften S sounds no longer rewrites the description or re-picks the shorts.** Softening never moves a word, but turning it on used to time every word again and then write the YouTube description and the shorts' pick again. Now a finished project keeps its timing, description and shorts' pick. Only the narration is joined again and the video and shorts are rendered again.
|
|
7
|
+
|
|
8
|
+
## Upgrading from 3.24.0
|
|
9
|
+
|
|
10
|
+
- A model saved for the old Gemini CLI, such as `gemini-2.5-pro`, isn't on Antigravity's list. Pick a new one in Edit project → Providers. The error message says so if you forget.
|
|
11
|
+
- For a finished project, turning softening on now remakes only the narration, the video, the shorts' renders and the PDF.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Slopify 3.25.1
|
|
2
|
+
|
|
3
|
+
Released 16 October 2026.
|
|
4
|
+
|
|
5
|
+
- **Turning on Soften breaths or Soften S sounds really keeps the description and the shorts now.** In 3.25.0 a finished project still asked for its description and shorts' pick again once the softened version was saved. Now only the narration is joined again and the video and shorts are rendered again.
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3.25.1",
|
|
4
|
+
"title": "Slopify 3.25.1",
|
|
5
|
+
"version": "3.25.1",
|
|
6
|
+
"date": "2026-10-16"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "3.25.0",
|
|
10
|
+
"title": "Slopify 3.25.0",
|
|
11
|
+
"version": "3.25.0",
|
|
12
|
+
"date": "2026-10-16"
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"id": "3.24.0",
|
|
4
16
|
"title": "Slopify 3.24.0",
|
|
@@ -2,7 +2,7 @@ import { providers } from "../settings/model.js";
|
|
|
2
2
|
const signIn = {
|
|
3
3
|
"claude-code": { name: "Claude Code", command: "claude auth login" },
|
|
4
4
|
codex: { name: "Codex", command: "codex login" },
|
|
5
|
-
gemini: { name: "
|
|
5
|
+
gemini: { name: "Antigravity", command: "agy" },
|
|
6
6
|
};
|
|
7
7
|
const signedOut = /not signed in|sign-in has expired|not logged in|run\s+"?(?:codex login|claude auth login)|authentication required/i;
|
|
8
8
|
const diskFull = /ENOSPC|no space left on device|disk (?:is )?full|not enough (?:free )?(?:disk )?space/i;
|
|
@@ -51,7 +51,7 @@ function cliOf(provider, reason) {
|
|
|
51
51
|
return "codex";
|
|
52
52
|
if (/Claude Code CLI/.test(reason))
|
|
53
53
|
return "claude-code";
|
|
54
|
-
if (/Gemini CLI/.test(reason))
|
|
54
|
+
if (/Antigravity CLI|Gemini CLI/.test(reason))
|
|
55
55
|
return "gemini";
|
|
56
56
|
return undefined;
|
|
57
57
|
}
|
|
@@ -42,7 +42,7 @@ export async function planShortProviders(input) {
|
|
|
42
42
|
if (llm === undefined)
|
|
43
43
|
gaps.push({
|
|
44
44
|
need: "text",
|
|
45
|
-
message: "No text model is ready. Install Claude Code, Codex or
|
|
45
|
+
message: "No text model is ready. Install Claude Code, Codex or Antigravity CLI and sign in to it (Settings → Providers shows what Slopify found), or add an OpenRouter key in Settings → Providers.",
|
|
46
46
|
});
|
|
47
47
|
let images;
|
|
48
48
|
for (const id of ["codex-image", ...keyedImages]) {
|
|
@@ -11,6 +11,7 @@ import { imageReference, referenceRecipe } from "./recipe-reference.js";
|
|
|
11
11
|
import { withReviews } from "./recipe-reviews.js";
|
|
12
12
|
import { imageScenes, thumbnailScenes } from "./recipe-scenes.js";
|
|
13
13
|
import { shortsRecipes } from "./recipe-shorts.js";
|
|
14
|
+
import { unsoftenedTimelines } from "./recipe-softening.js";
|
|
14
15
|
import { textRecipes } from "./recipe-text.js";
|
|
15
16
|
import { thumbnailRecipes, visualAssets, visualRecipes } from "./recipe-visual.js";
|
|
16
17
|
import { youtubeRecipes } from "./recipe-youtube.js";
|
|
@@ -52,7 +53,7 @@ const descriptionKey = "youtube:description";
|
|
|
52
53
|
function buildEach(context) {
|
|
53
54
|
const text = textRecipes(context);
|
|
54
55
|
const audio = audioRecipes(context, text);
|
|
55
|
-
const exports = exportRecipes(context, audio);
|
|
56
|
+
const exports = exportRecipes(context, audio, () => unsoftenedTimelines(context, text));
|
|
56
57
|
const captions = exports.find((value) => value.key === "subtitles:files");
|
|
57
58
|
const timing = exports.find((value) => value.key === "subtitles:timing");
|
|
58
59
|
// How the subject and characters look, looked up when a prompt has `{{Appearance}}`.
|
|
@@ -11,7 +11,10 @@ import { portraitValues } from "../voices/portraits.js";
|
|
|
11
11
|
import { linePlan, linesLevelled } from "./recipe-lines.js";
|
|
12
12
|
import { masterPlan } from "./recipe-loudness.js";
|
|
13
13
|
import { recipe, resourceIdentity, } from "./recipe-model.js";
|
|
14
|
-
export function exportRecipes(context, audio
|
|
14
|
+
export function exportRecipes(context, audio,
|
|
15
|
+
// The timelines a finished timing may have been made from before Soften breaths or Soften S
|
|
16
|
+
// sounds were turned on (`recipe-softening.ts`).
|
|
17
|
+
unsoftened = () => []) {
|
|
15
18
|
const { config, content } = context;
|
|
16
19
|
if (audio.mediaFingerprint === null)
|
|
17
20
|
return [];
|
|
@@ -33,23 +36,31 @@ export function exportRecipes(context, audio) {
|
|
|
33
36
|
reviewsNarration(config) ||
|
|
34
37
|
voices?.audioFiles === true ||
|
|
35
38
|
lined;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
const timingOf = (timeline) => recipe(context, "subtitles:timing", "video", {
|
|
40
|
+
kind: "local",
|
|
41
|
+
version: 1,
|
|
42
|
+
operation: timingOperation(projectLanguage(config)),
|
|
43
|
+
// The lead-in moves every word, so the edge silence is part of the timing.
|
|
44
|
+
values: [
|
|
45
|
+
timeline,
|
|
46
|
+
config.silenceGapSeconds,
|
|
47
|
+
// The project language; an English project reads "en" here as it always did.
|
|
48
|
+
config.language ?? config.subtitles?.language ?? "en",
|
|
49
|
+
config.edgeSilenceSeconds,
|
|
50
|
+
// Each word learns its speaker and turn on a multi-voice run.
|
|
51
|
+
...(voices === undefined ? [] : ["voice-words-v1"]),
|
|
52
|
+
],
|
|
53
|
+
}, audio.keys);
|
|
54
|
+
const timedNow = timed ? timingOf(audio.timeline) : undefined;
|
|
55
|
+
// A finished timing made before the narration was softened is kept: softening moves no word.
|
|
56
|
+
const doneTiming = (fingerprint) => (context.history ?? context.manifest).pieces.some((row) => row.key === "subtitles:timing" &&
|
|
57
|
+
row.piece.state === "done" &&
|
|
58
|
+
row.fingerprint === fingerprint);
|
|
59
|
+
const timing = timedNow === undefined || doneTiming(timedNow.fingerprint)
|
|
60
|
+
? timedNow
|
|
61
|
+
: (unsoftened()
|
|
62
|
+
.map(timingOf)
|
|
63
|
+
.find((one) => doneTiming(one.fingerprint)) ?? timedNow);
|
|
53
64
|
const lines = linePlan(context, timing);
|
|
54
65
|
// The timing goes ahead of what waits for it; any other run keeps its old order.
|
|
55
66
|
if (lined && timing !== undefined)
|