@gentbajko/slopify 3.23.0 → 3.25.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/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/edge/http/studio.js +8 -6
- 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.24.0.md +12 -0
- package/dist/patch-notes/3.25.0.md +11 -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 +37 -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/web/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
7
|
<meta name="color-scheme" content="dark light" />
|
|
8
8
|
<title>Slopify</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-D8LqNsCl.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-BadsoORk.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gentbajko/slopify",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.25.0",
|
|
4
4
|
"description": "Turn a prompt into a finished video, locally, with your own provider keys",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Gent Bajko",
|
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
"onnxruntime-web": "1.30.0",
|
|
42
42
|
"remark": "^15.0.1",
|
|
43
43
|
"remark-gfm": "^4.0.1",
|
|
44
|
-
"strip-json-comments": "^5.0.3",
|
|
45
44
|
"strip-markdown": "^6.0.0",
|
|
46
45
|
"ulid": "^3.0.2",
|
|
47
46
|
"yaml": "^2.9.1",
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { constants } from "node:fs";
|
|
2
|
-
import { access, realpath } from "node:fs/promises";
|
|
3
|
-
import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
4
|
-
import { cliCommand } from "../../kernel/cli-command.js";
|
|
5
|
-
import { readCatalogueFile } from "./catalogue-files.js";
|
|
6
|
-
export async function nodeGeminiModels(binary) {
|
|
7
|
-
try {
|
|
8
|
-
const command = cliCommand(binary);
|
|
9
|
-
const entry = await executablePath(command.args[0] ?? command.file);
|
|
10
|
-
const candidates = new Set();
|
|
11
|
-
let directory = dirname(entry);
|
|
12
|
-
// npm and bun can install the core package nested under gemini-cli or
|
|
13
|
-
// hoisted beside it. Resolve the configured executable's symlinks first.
|
|
14
|
-
for (let depth = 0; depth < 10; depth += 1) {
|
|
15
|
-
const suffix = "gemini-cli-core/dist/src/config/models.js";
|
|
16
|
-
candidates.add(join(directory, "node_modules/@google", suffix));
|
|
17
|
-
candidates.add(join(directory, suffix));
|
|
18
|
-
const parent = dirname(directory);
|
|
19
|
-
if (parent === directory)
|
|
20
|
-
break;
|
|
21
|
-
directory = parent;
|
|
22
|
-
}
|
|
23
|
-
for (const candidate of candidates) {
|
|
24
|
-
try {
|
|
25
|
-
const models = parseGeminiModels(await readCatalogueFile(candidate, 256 * 1024));
|
|
26
|
-
if (models.length > 1)
|
|
27
|
-
return models;
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
// A candidate is an optional package layout, not the final discovery result.
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
// Recent Gemini CLI releases ship only bundled chunks. Follow the entry
|
|
34
|
-
// point's literal chunk imports and read their model constants as text.
|
|
35
|
-
const bundled = await bundledModels(entry);
|
|
36
|
-
if (bundled.length > 1)
|
|
37
|
-
return bundled;
|
|
38
|
-
throw new Error("No installed model metadata");
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
throw new Error("Slopify could not get the model list from the Gemini CLI. Check the CLI is installed (Settings → Providers), then refresh the list, or type a model name the Gemini CLI accepts.");
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
async function bundledModels(entry) {
|
|
45
|
-
const source = await readCatalogueFile(entry, 256 * 1024);
|
|
46
|
-
const imports = new Set();
|
|
47
|
-
for (const match of source.matchAll(/\b(?:from\s+|import\s*)["']\.\/(chunk-[A-Za-z0-9-]+\.js)["']/g)) {
|
|
48
|
-
if (match[1] !== undefined)
|
|
49
|
-
imports.add(match[1]);
|
|
50
|
-
}
|
|
51
|
-
if (imports.size === 0 || imports.size > 32)
|
|
52
|
-
return [];
|
|
53
|
-
for (const name of imports) {
|
|
54
|
-
try {
|
|
55
|
-
const source = await readCatalogueFile(join(dirname(entry), name), 24 * 1024 * 1024);
|
|
56
|
-
if (!source.includes("packages/core/dist/src/config/models.js"))
|
|
57
|
-
continue;
|
|
58
|
-
const models = parseGeminiModels(source);
|
|
59
|
-
if (models.length > 1)
|
|
60
|
-
return models;
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// Other chunks can be absent or too large; do not execute them.
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return [];
|
|
67
|
-
}
|
|
68
|
-
function parseGeminiModels(source) {
|
|
69
|
-
const models = new Map();
|
|
70
|
-
// Read literal model constants only. Never import or execute an
|
|
71
|
-
// installed package, parse comments as entries, or inspect login/settings.
|
|
72
|
-
const declarations = /^(?:export\s+)?(?:const|let|var)\s+([A-Z][A-Z0-9_]*)\s*=\s*(['"])([^'"\r\n]+)\2\s*;/gm;
|
|
73
|
-
const uncommented = source.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
74
|
-
for (const match of uncommented.matchAll(declarations)) {
|
|
75
|
-
const name = match[1];
|
|
76
|
-
const id = match[3];
|
|
77
|
-
if (name === undefined ||
|
|
78
|
-
id === undefined ||
|
|
79
|
-
!name.includes("MODEL") ||
|
|
80
|
-
name.includes("EMBEDDING") ||
|
|
81
|
-
!/^gemini-\d+(?:\.\d+)?-(?:pro|flash|flash-lite)(?:-preview(?:-\d{2}-\d{2})?)?$/.test(id))
|
|
82
|
-
continue;
|
|
83
|
-
// SECONDARY and CUSTOM_TOOLS constants are routing aliases, not separate
|
|
84
|
-
// picker choices. Gemma and embedding models use different availability rules.
|
|
85
|
-
// In 0.61+, DEFAULT_FLASH points at a BASE constant while new releases
|
|
86
|
-
// use LATEST constants. Read both literal sources without evaluating JS.
|
|
87
|
-
if (!/^(?:PREVIEW|DEFAULT|BASE|LATEST)_GEMINI_/.test(name) || name.includes("CUSTOM_TOOLS"))
|
|
88
|
-
continue;
|
|
89
|
-
const parts = /^gemini-(\d+(?:\.\d+)?)-(pro|flash-lite|flash)/.exec(id);
|
|
90
|
-
if (!parts)
|
|
91
|
-
continue;
|
|
92
|
-
const tier = parts[2] === "pro" ? "Pro" : parts[2] === "flash-lite" ? "Flash-Lite" : "Flash";
|
|
93
|
-
models.set(id, {
|
|
94
|
-
id,
|
|
95
|
-
name: `Gemini ${parts[1]} ${tier}${id.includes("-preview") ? " (Preview)" : ""}`,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
if (models.size === 0)
|
|
99
|
-
return [];
|
|
100
|
-
const sorted = [...models.values()].sort((a, b) => b.id.localeCompare(a.id, "en", { numeric: true }));
|
|
101
|
-
const tiers = new Set();
|
|
102
|
-
const choices = sorted.map((model) => {
|
|
103
|
-
const tier = /-(pro|flash-lite|flash)(?:-|$)/.exec(model.id)?.[1] ?? model.id;
|
|
104
|
-
const group = tiers.has(tier) ? "Other versions" : "Latest in installed CLI";
|
|
105
|
-
tiers.add(tier);
|
|
106
|
-
return { ...model, group };
|
|
107
|
-
});
|
|
108
|
-
return [{ id: "auto", name: "Automatic (CLI default)" }, ...choices];
|
|
109
|
-
}
|
|
110
|
-
async function executablePath(binary) {
|
|
111
|
-
if (isAbsolute(binary) || /[\\/]/.test(binary))
|
|
112
|
-
return realpath(binary);
|
|
113
|
-
const path = Object.entries(process.env).find(([key]) => key.toLowerCase() === "path")?.[1] ?? "";
|
|
114
|
-
for (const directory of path.split(delimiter).slice(0, 128)) {
|
|
115
|
-
if (directory === "")
|
|
116
|
-
continue;
|
|
117
|
-
const candidate = join(directory.replace(/^"|"$/g, ""), binary);
|
|
118
|
-
try {
|
|
119
|
-
await access(candidate, constants.X_OK);
|
|
120
|
-
return await realpath(candidate);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
// Continue normal PATH lookup; no process is launched for discovery.
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
throw new Error("Gemini executable was not found");
|
|
127
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir, tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import stripJsonComments from "strip-json-comments";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { providerError } from "../../kernel/ports/model.js";
|
|
7
|
-
import { documentServerName, documentTool } from "./document-workspace.js";
|
|
8
|
-
// Gemini 0.61 rejects non-root system settings and null context objects. Use a
|
|
9
|
-
// request-owned CLI home instead. OAuth reads the host's existing credential file
|
|
10
|
-
// through its supported ADC fallback; settings, hooks and memory are not inherited.
|
|
11
|
-
export function geminiWorkspace(webSearch, request, documents, env = process.env) {
|
|
12
|
-
const directory = mkdtempSync(join(tmpdir(), "slopify-gemini-"));
|
|
13
|
-
try {
|
|
14
|
-
const state = join(directory, ".gemini");
|
|
15
|
-
mkdirSync(state, { mode: 0o700 });
|
|
16
|
-
const settings = join(state, "settings.json");
|
|
17
|
-
const originalState = join(env.GEMINI_CLI_HOME || env.HOME || env.USERPROFILE || homedir(), ".gemini");
|
|
18
|
-
let selectedType;
|
|
19
|
-
const originalSettings = join(originalState, "settings.json");
|
|
20
|
-
if (existsSync(originalSettings))
|
|
21
|
-
try {
|
|
22
|
-
selectedType = z
|
|
23
|
-
.object({
|
|
24
|
-
security: z
|
|
25
|
-
.object({
|
|
26
|
-
auth: z.object({ selectedType: z.string().max(128).optional() }).optional(),
|
|
27
|
-
})
|
|
28
|
-
.optional(),
|
|
29
|
-
})
|
|
30
|
-
.parse(JSON.parse(stripJsonComments(readFileSync(originalSettings, "utf8")))).security
|
|
31
|
-
?.auth?.selectedType;
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
throw providerError({
|
|
35
|
-
kind: "unavailable",
|
|
36
|
-
message: "Slopify could not read the Gemini CLI's settings file (settings.json in the .gemini folder of your home directory), which says how it signs in. Fix or remove that file, run gemini in a terminal to sign in again, then use Try again.",
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
const oauth = join(originalState, "oauth_creds.json");
|
|
40
|
-
const system = join(directory, "writing.md");
|
|
41
|
-
const trust = join(directory, "trusted-folders.json");
|
|
42
|
-
const mcpAllowlist = documents
|
|
43
|
-
? documentServerName
|
|
44
|
-
: `slopify-disabled-${directory.split(/[\\/]/).at(-1)}`;
|
|
45
|
-
writeFileSync(settings, JSON.stringify({
|
|
46
|
-
...(selectedType ? { security: { auth: { selectedType } } } : {}),
|
|
47
|
-
...(request?.thinkingConfig
|
|
48
|
-
? {
|
|
49
|
-
modelConfigs: {
|
|
50
|
-
customOverrides: [
|
|
51
|
-
{
|
|
52
|
-
match: { model: request.model },
|
|
53
|
-
modelConfig: {
|
|
54
|
-
generateContentConfig: {
|
|
55
|
-
thinkingConfig: {
|
|
56
|
-
...(request.thinkingConfig.level === undefined
|
|
57
|
-
? {}
|
|
58
|
-
: { thinkingLevel: request.thinkingConfig.level }),
|
|
59
|
-
...(request.thinkingConfig.budget === undefined
|
|
60
|
-
? {}
|
|
61
|
-
: { thinkingBudget: request.thinkingConfig.budget }),
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
},
|
|
68
|
-
}
|
|
69
|
-
: {}),
|
|
70
|
-
tools: {
|
|
71
|
-
core: webSearch ? ["google_web_search"] : [],
|
|
72
|
-
allowed: webSearch ? ["google_web_search"] : [],
|
|
73
|
-
discoveryCommand: "",
|
|
74
|
-
callCommand: "",
|
|
75
|
-
enableHooks: false,
|
|
76
|
-
},
|
|
77
|
-
mcp: { allowed: [mcpAllowlist], serverCommand: "" },
|
|
78
|
-
...(documents
|
|
79
|
-
? {
|
|
80
|
-
mcpServers: {
|
|
81
|
-
[documentServerName]: {
|
|
82
|
-
command: documents.command,
|
|
83
|
-
args: documents.args,
|
|
84
|
-
trust: true,
|
|
85
|
-
includeTools: [documentTool],
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
: {}),
|
|
90
|
-
context: {
|
|
91
|
-
fileName: "SLOPIFY_NO_CONTEXT.md",
|
|
92
|
-
includeDirectories: [],
|
|
93
|
-
loadMemoryFromIncludeDirectories: false,
|
|
94
|
-
},
|
|
95
|
-
experimental: { codebaseInvestigatorSettings: { enabled: false } },
|
|
96
|
-
hooks: {},
|
|
97
|
-
hooksConfig: { enabled: false },
|
|
98
|
-
skills: { enabled: false },
|
|
99
|
-
ide: { enabled: false },
|
|
100
|
-
telemetry: { enabled: false },
|
|
101
|
-
privacy: { usageStatisticsEnabled: false },
|
|
102
|
-
advanced: { autoConfigureMemory: false },
|
|
103
|
-
}), { mode: 0o600 });
|
|
104
|
-
writeFileSync(system, "You are the writing and research component of Slopify, a video creation app. " +
|
|
105
|
-
"Write the requested articles, narration, research notes or image prompts. " +
|
|
106
|
-
"Follow the topic, language, tone, length and format. Return only the requested content. " +
|
|
107
|
-
"Use web search only when available and useful. Never read or modify other local files. " +
|
|
108
|
-
(documents ? "Use only the supplied research MCP tool to read request documents. " : "") +
|
|
109
|
-
"A backslash immediately before @ escapes a literal @ in the supplied text.\n", { mode: 0o600 });
|
|
110
|
-
writeFileSync(trust, JSON.stringify({ [directory]: "TRUST_FOLDER" }), { mode: 0o600 });
|
|
111
|
-
return {
|
|
112
|
-
mcpAllowlist,
|
|
113
|
-
options: {
|
|
114
|
-
cwd: directory,
|
|
115
|
-
env: {
|
|
116
|
-
...env,
|
|
117
|
-
GEMINI_CLI_HOME: directory,
|
|
118
|
-
...(selectedType === "oauth-personal" && existsSync(oauth)
|
|
119
|
-
? { GOOGLE_APPLICATION_CREDENTIALS: oauth }
|
|
120
|
-
: {}),
|
|
121
|
-
GEMINI_CLI_TRUSTED_FOLDERS_PATH: trust,
|
|
122
|
-
NO_BROWSER: "true",
|
|
123
|
-
GEMINI_SYSTEM_MD: system,
|
|
124
|
-
GEMINI_WRITE_SYSTEM_MD: "false",
|
|
125
|
-
GEMINI_CLI_NO_RELAUNCH: "true",
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
remove: () => rmSync(directory, { recursive: true, force: true }),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
rmSync(directory, { recursive: true, force: true });
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { redact } from "../../kernel/log.js";
|
|
3
|
-
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
-
import { cliCheck, cliName, cliReported } from "../explain.js";
|
|
5
|
-
import { documentWorkspace } from "./document-workspace.js";
|
|
6
|
-
import { nodeGeminiModels } from "./gemini-models.js";
|
|
7
|
-
import { geminiWorkspace } from "./gemini-workspace.js";
|
|
8
|
-
import { noImages } from "./image-workspace.js";
|
|
9
|
-
import { cliEvent, cliInput, cliShaped, deliveredInput, endedWithout, promptOf, stopCliRun, stuckCli, } from "./run-cli.js";
|
|
10
|
-
import { lines } from "./sse-lines.js";
|
|
11
|
-
export const geminiBinary = "gemini";
|
|
12
|
-
// Verified against installed Gemini CLI 0.16.0's config/nonInteractiveCli sources.
|
|
13
|
-
// Its --allowed-tools flag bypasses approval; tools.core in the isolated settings
|
|
14
|
-
// actually restricts discovery. Never request yolo/auto_edit.
|
|
15
|
-
export function geminiArgs(req, mcpAllowlist) {
|
|
16
|
-
return [
|
|
17
|
-
"--output-format",
|
|
18
|
-
"stream-json",
|
|
19
|
-
"--approval-mode",
|
|
20
|
-
"default",
|
|
21
|
-
"--extensions",
|
|
22
|
-
"none",
|
|
23
|
-
"--allowed-mcp-server-names",
|
|
24
|
-
mcpAllowlist,
|
|
25
|
-
...(req.webSearch === true ? ["--allowed-tools", "google_web_search"] : []),
|
|
26
|
-
...(req.model === "" ? [] : ["--model", req.model]),
|
|
27
|
-
"-p",
|
|
28
|
-
"Produce the requested content from the supplied conversation on stdin.",
|
|
29
|
-
];
|
|
30
|
-
}
|
|
31
|
-
const messageSchema = z.object({
|
|
32
|
-
role: z.string(),
|
|
33
|
-
content: z.string(),
|
|
34
|
-
delta: z.boolean().optional(),
|
|
35
|
-
});
|
|
36
|
-
const errorSchema = z.object({ severity: z.string(), message: z.string() });
|
|
37
|
-
const resultSchema = z.object({
|
|
38
|
-
status: z.string(),
|
|
39
|
-
error: z.object({ message: z.string(), type: z.string().optional() }).optional(),
|
|
40
|
-
stats: z.object({ input_tokens: z.number(), output_tokens: z.number() }).optional(),
|
|
41
|
-
});
|
|
42
|
-
export function geminiLlm(deps) {
|
|
43
|
-
const binary = deps.binary ?? geminiBinary;
|
|
44
|
-
async function* complete(req) {
|
|
45
|
-
req.signal.throwIfAborted();
|
|
46
|
-
// Its CLI has no image flag, and `@file` inclusion would need a file tool this adapter keeps off.
|
|
47
|
-
if (req.images?.length)
|
|
48
|
-
throw noImages("the Gemini CLI");
|
|
49
|
-
const documents = documentWorkspace(req.documents);
|
|
50
|
-
let workspace;
|
|
51
|
-
let run;
|
|
52
|
-
let ended;
|
|
53
|
-
try {
|
|
54
|
-
workspace = geminiWorkspace(req.webSearch === true, req, documents, deps.env ?? process.env);
|
|
55
|
-
run = deps.run(binary, geminiArgs(req, workspace.mcpAllowlist), req.signal, {
|
|
56
|
-
...workspace.options,
|
|
57
|
-
stdin: cliInput(`Produce the requested content from this conversation:\n\n${[documents?.instructions, promptOf(req.messages)].filter(Boolean).join("\n\n").replaceAll("@", "\\@")}`),
|
|
58
|
-
});
|
|
59
|
-
for await (const line of lines(authChecked(run.stdout), req.signal)) {
|
|
60
|
-
if (line.trim() === "")
|
|
61
|
-
continue;
|
|
62
|
-
const event = cliEvent(binary, line);
|
|
63
|
-
yield { type: "activity" };
|
|
64
|
-
req.signal.throwIfAborted();
|
|
65
|
-
if (event.type === "message") {
|
|
66
|
-
const message = cliShaped(binary, messageSchema, event.value);
|
|
67
|
-
if (message.role === "assistant" && message.content !== "")
|
|
68
|
-
yield { type: "delta", text: message.content };
|
|
69
|
-
}
|
|
70
|
-
else if (event.type === "error") {
|
|
71
|
-
const error = cliShaped(binary, errorSchema, event.value);
|
|
72
|
-
if (error.severity !== "warning")
|
|
73
|
-
throw failure(binary, error.message);
|
|
74
|
-
}
|
|
75
|
-
else if (event.type === "result") {
|
|
76
|
-
const result = cliShaped(binary, resultSchema, event.value);
|
|
77
|
-
if (result.status !== "success")
|
|
78
|
-
throw failure(binary, result.error?.message ?? result.status);
|
|
79
|
-
await deliveredInput(run);
|
|
80
|
-
documents?.verifyRead();
|
|
81
|
-
yield {
|
|
82
|
-
type: "done",
|
|
83
|
-
usage: result.stats === undefined
|
|
84
|
-
? null
|
|
85
|
-
: {
|
|
86
|
-
inputTokens: result.stats.input_tokens,
|
|
87
|
-
outputTokens: result.stats.output_tokens,
|
|
88
|
-
},
|
|
89
|
-
finishReason: null,
|
|
90
|
-
};
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
req.signal.throwIfAborted();
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
try {
|
|
101
|
-
if (run !== undefined)
|
|
102
|
-
ended = await stopCliRun(run);
|
|
103
|
-
}
|
|
104
|
-
finally {
|
|
105
|
-
documents?.remove();
|
|
106
|
-
workspace?.remove();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
req.signal.throwIfAborted();
|
|
110
|
-
if (run === undefined)
|
|
111
|
-
throw failure(binary, "", `The ${cliName(binary)} could not be started. Check it is installed and set up in Settings → Providers, then use Try again.`);
|
|
112
|
-
if (authRequired(run.stderr()))
|
|
113
|
-
throw authFailure();
|
|
114
|
-
throw failure(binary, run.stderr(), ended === undefined ? stuckCli(binary) : endedWithout(binary, ended, run.stderr()));
|
|
115
|
-
}
|
|
116
|
-
return {
|
|
117
|
-
id: "gemini",
|
|
118
|
-
capabilities: { streams: true, reportsUsage: true, webSearch: true },
|
|
119
|
-
models: deps.readModels ?? (() => nodeGeminiModels(binary)),
|
|
120
|
-
complete,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
// `message` is what the CLI said, read for the two failures that need the user's hand;
|
|
124
|
-
// `sentence` replaces the default wording when Slopify already knows what happened.
|
|
125
|
-
function failure(binary, message, sentence) {
|
|
126
|
-
if (/(?:#?3501\b|do not have a valid licen[cs]e of this product)/i.test(message)) {
|
|
127
|
-
return providerError({
|
|
128
|
-
kind: "unsupported",
|
|
129
|
-
message: "Google refused the Gemini CLI because the signed-in account has no licence for it (#3501). Update the Gemini CLI, run gemini in a terminal to sign in again, then use Try again; for a work or school account, ask your administrator for a Gemini licence.",
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
// The daily allowance of a signed-in Gemini plan. The CLI names no reset time, so the wait
|
|
133
|
-
// checks again later; a per-minute quota is an ordinary rate limit and is retried as one.
|
|
134
|
-
if (/(?:daily|per.day).{0,40}quota|quota.{0,40}(?:daily|per.day)/i.test(message)) {
|
|
135
|
-
return providerError({
|
|
136
|
-
kind: "rate_limit",
|
|
137
|
-
message: `Your Gemini plan's daily quota is used up (the Gemini CLI said: ${redact(message)}). Slopify waits and tries again later by itself.`,
|
|
138
|
-
planLimit: { account: "gemini", resetsAt: null },
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
return authRequired(message)
|
|
142
|
-
? authFailure()
|
|
143
|
-
: providerError({
|
|
144
|
-
kind: "other",
|
|
145
|
-
message: sentence ?? cliReported(binary, redact(message), cliCheck(binary)),
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
function authRequired(message) {
|
|
149
|
-
return /Code Assist login required|authorize the application|authorization code|Please set an Auth method|re-authenticate|reauthenticate/i.test(message);
|
|
150
|
-
}
|
|
151
|
-
function authFailure() {
|
|
152
|
-
return providerError({
|
|
153
|
-
kind: "missing_key",
|
|
154
|
-
message: 'The Gemini CLI is not signed in, or its sign-in has expired. Open a terminal on the computer running the CLI, run "gemini" and sign in, then use Try again.',
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
async function* authChecked(source) {
|
|
158
|
-
const decoder = new TextDecoder();
|
|
159
|
-
let tail = "";
|
|
160
|
-
for await (const bytes of source) {
|
|
161
|
-
tail = (tail + decoder.decode(bytes, { stream: true })).slice(-8192);
|
|
162
|
-
// NO_BROWSER prevents opening OAuth. A logged-out CLI instead writes this
|
|
163
|
-
// non-JSON prompt without a newline; fail before lines() waits for user input.
|
|
164
|
-
if (/^(?:Enter the authorization code:|Please visit the following URL to authorize)/m.test(tail))
|
|
165
|
-
throw authFailure();
|
|
166
|
-
yield bytes;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
export function nodeGeminiLoginHost() {
|
|
5
|
-
return {
|
|
6
|
-
home: homedir(),
|
|
7
|
-
env: process.env,
|
|
8
|
-
read: (path) => readFile(path, "utf8").catch(() => undefined),
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
export async function readGeminiLogin(host) {
|
|
12
|
-
const folder = join(host.home, ".gemini");
|
|
13
|
-
const raw = await host.read(join(folder, "settings.json"));
|
|
14
|
-
const settings = parse(raw);
|
|
15
|
-
const method = selectedAuth(settings);
|
|
16
|
-
const hasKey = async (name) => nonEmpty(host.env[name]) || dotenvHas(await host.read(join(folder, ".env")), name);
|
|
17
|
-
const oauth = async () => {
|
|
18
|
-
const creds = parse(await host.read(join(folder, "oauth_creds.json")));
|
|
19
|
-
return typeof creds?.refresh_token === "string" && creds.refresh_token !== "";
|
|
20
|
-
};
|
|
21
|
-
switch (method) {
|
|
22
|
-
case "oauth-personal":
|
|
23
|
-
case "login-with-google":
|
|
24
|
-
return (await oauth()) ? "signed-in" : "signed-out";
|
|
25
|
-
case "gemini-api-key":
|
|
26
|
-
return (await hasKey("GEMINI_API_KEY")) ? "signed-in" : "signed-out";
|
|
27
|
-
case "vertex-ai":
|
|
28
|
-
return (await hasKey("GOOGLE_API_KEY")) || (await hasKey("GOOGLE_CLOUD_PROJECT"))
|
|
29
|
-
? "signed-in"
|
|
30
|
-
: "signed-out";
|
|
31
|
-
case "cloud-shell":
|
|
32
|
-
return "signed-in";
|
|
33
|
-
case undefined:
|
|
34
|
-
// No choice saved yet: the CLI still starts without asking when a key or an earlier
|
|
35
|
-
// Google sign-in is there; otherwise it opens its sign-in menu.
|
|
36
|
-
if ((await hasKey("GEMINI_API_KEY")) || (await oauth()))
|
|
37
|
-
return "signed-in";
|
|
38
|
-
// Nothing saved says nothing either: the CLI can be signed in some other way (Vertex or
|
|
39
|
-
// a key in its own environment), so only a sign-in the files prove missing blocks a run.
|
|
40
|
-
return "unknown";
|
|
41
|
-
default:
|
|
42
|
-
// A method this version of Slopify doesn't know.
|
|
43
|
-
return "unknown";
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function parse(text) {
|
|
47
|
-
if (text === undefined)
|
|
48
|
-
return undefined;
|
|
49
|
-
try {
|
|
50
|
-
const value = JSON.parse(text);
|
|
51
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
52
|
-
? value
|
|
53
|
-
: undefined;
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function selectedAuth(settings) {
|
|
60
|
-
const security = settings?.security;
|
|
61
|
-
const auth = typeof security === "object" && security !== null
|
|
62
|
-
? security.auth
|
|
63
|
-
: undefined;
|
|
64
|
-
const nested = typeof auth === "object" && auth !== null
|
|
65
|
-
? auth.selectedType
|
|
66
|
-
: undefined;
|
|
67
|
-
if (typeof nested === "string" && nested !== "")
|
|
68
|
-
return nested;
|
|
69
|
-
const flat = settings?.selectedAuthType;
|
|
70
|
-
return typeof flat === "string" && flat !== "" ? flat : undefined;
|
|
71
|
-
}
|
|
72
|
-
function nonEmpty(value) {
|
|
73
|
-
return value !== undefined && value.trim() !== "";
|
|
74
|
-
}
|
|
75
|
-
// `NAME=value` in a .env file, optionally quoted or exported.
|
|
76
|
-
function dotenvHas(text, name) {
|
|
77
|
-
if (text === undefined)
|
|
78
|
-
return false;
|
|
79
|
-
return text.split(/\r?\n/).some((line) => {
|
|
80
|
-
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
81
|
-
return match?.[1] === name && nonEmpty((match[2] ?? "").replace(/^["']|["']$/g, ""));
|
|
82
|
-
});
|
|
83
|
-
}
|