cozyclay 3.1.2 → 3.1.4
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/CHANGELOG.md +37 -0
- package/README.md +1 -1
- package/bin/agent/agent-runner.mjs +160 -151
- package/bin/agent/agent-stream.mjs +86 -0
- package/bin/agent/anthropic-subscription-events.mjs +120 -0
- package/bin/agent/anthropic-subscription-prompt.mjs +116 -0
- package/bin/agent/anthropic-subscription-stream.mjs +251 -0
- package/bin/agent/anthropic-subscription.mjs +73 -0
- package/bin/agent/claude-code-executable.mjs +93 -0
- package/bin/agent/cliproxy-provider.mjs +25 -11
- package/bin/agent/model-roles.mjs +112 -0
- package/bin/agent/openai-compatible-provider.mjs +95 -0
- package/bin/agent/pi-tools.mjs +19 -10
- package/bin/agent/provider-login.mjs +3 -1
- package/bin/agent/provider-routes.mjs +2 -0
- package/bin/agent/providers.mjs +35 -130
- package/bin/cozyclay.mjs +2 -48
- package/bin/star-prompt.mjs +123 -0
- package/dist/app/index.html +5 -5
- package/dist/assets/TaskBoard-U1Yl-4F1.js +1 -0
- package/dist/assets/app-BdRlMf4J.css +1 -0
- package/dist/assets/app-KXdgBC-S.js +5144 -0
- package/dist/assets/{motion3-compile-dgGhrvPK.js → motion3-compile-DDG3wklD.js} +1 -1
- package/dist/assets/scene-asset-sync-Bz59Ghdz.js +13 -0
- package/dist/assets/task-board-DFfNa0xg.js +15 -0
- package/dist/assets/{workflow-C87p6E1E.js → workflow-BMJGd_gZ.js} +1 -1
- package/dist/cozyclay-package.json +1 -1
- package/dist/workflow/index.html +3 -3
- package/package.json +6 -4
- package/src/App.jsx +28 -28
- package/src/agent-image-client.js +2 -2
- package/src/asset-pane.jsx +14 -14
- package/src/character-kind.js +13 -12
- package/src/commands/project.js +2 -1
- package/src/commands/shot.js +1 -1
- package/src/commands/view.js +2 -2
- package/src/domains/cast.js +15 -15
- package/src/domains/motion/motion-range-pin-rules.js +1 -1
- package/src/domains/motion.js +11 -11
- package/src/domains/objects.js +20 -30
- package/src/domains/scenes.js +14 -21
- package/src/domains/shots.js +3 -3
- package/src/first-success-guide.jsx +19 -3
- package/src/hierarchy-panel.jsx +6 -7
- package/src/locale-zh/app.js +124 -0
- package/src/locale-zh/domains.js +36 -0
- package/src/locale-zh/labels.js +148 -0
- package/src/locale-zh/panels.js +225 -0
- package/src/locale-zh/shell.js +230 -0
- package/src/locale-zh/timeline.js +145 -0
- package/src/locale-zh/workflow.js +104 -0
- package/src/locale-zh/workspace.js +190 -0
- package/src/locale-zh.js +21 -0
- package/src/locale.js +16 -6
- package/src/main.jsx +1 -1
- package/src/motion/beat-board-model.js +4 -4
- package/src/motion/beat-board.jsx +1 -1
- package/src/motion/ik-pick.js +24 -0
- package/src/motion/take-segments.jsx +5 -5
- package/src/motion/timeline.jsx +30 -40
- package/src/motion/waypoints.js +6 -6
- package/src/object-catalog.jsx +4 -5
- package/src/panels/CameraPanel.jsx +3 -3
- package/src/panels/HdrEnvironmentControls.jsx +2 -1
- package/src/panels/ObjectTransformPanel.jsx +19 -29
- package/src/panels/PoseImagePanel.jsx +8 -8
- package/src/panels/PosePanel.jsx +2 -2
- package/src/panels/ReferenceImageField.jsx +2 -2
- package/src/panels/RigControlPanel.jsx +2 -2
- package/src/panels/SculptPanel.jsx +9 -9
- package/src/panels/SubjectBox.jsx +3 -3
- package/src/panels/SubjectsPanel.jsx +1 -1
- package/src/posestudio.jsx +10 -8
- package/src/project-browser.css +7 -6
- package/src/project-browser.jsx +7 -6
- package/src/project-open-file.js +19 -0
- package/src/qa-label.js +1 -1
- package/src/range-pin-panel.jsx +14 -14
- package/src/resource-status.jsx +3 -3
- package/src/result-modal.jsx +10 -10
- package/src/settings-menu.jsx +4 -1
- package/src/shell/BoardSlot.jsx +5 -5
- package/src/shell/CompositionGuideButton.jsx +1 -1
- package/src/shell/GhostsMenuGroup.jsx +1 -1
- package/src/shell/StatusBar.jsx +4 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +2 -2
- package/src/shell/details-selection.js +1 -1
- package/src/shell/preference-controls.jsx +2 -1
- package/src/shot-guides.js +5 -5
- package/src/stage-hdr.js +3 -3
- package/src/stage-labels-ko.js +2 -2
- package/src/stage-startup-defaults.js +3 -3
- package/src/studio-actions.js +2 -2
- package/src/styles/tokens.css +1 -1
- package/src/styles.css +1 -1
- package/src/use-case-question.jsx +18 -18
- package/src/workflow/AgentPanel.jsx +4 -4
- package/src/workflow/AgentPlanCard.jsx +1 -1
- package/src/workflow/AgentProgressCard.jsx +4 -4
- package/src/workflow/AgentTurnSteps.jsx +1 -1
- package/src/workflow/LoginPanel.jsx +1 -1
- package/src/workflow/TaskBoard.jsx +1 -1
- package/src/workflow/TaskInline.jsx +1 -1
- package/src/workflow/agent-client.js +1 -1
- package/src/workflow/agent-turn.js +5 -5
- package/src/workflow/task-board.js +4 -4
- package/tools/loc-baseline.json +2 -2
- package/tools/run-tests.mjs +5 -5
- package/dist/assets/TaskBoard-GR8xZm5c.js +0 -1
- package/dist/assets/app-Bhe0TpGz.css +0 -1
- package/dist/assets/app-C4QTcDcb.js +0 -5144
- package/dist/assets/scene-asset-sync-qoxcdrhj.js +0 -13
- package/dist/assets/task-board-BqReHzOi.js +0 -15
package/bin/agent/providers.mjs
CHANGED
|
@@ -5,6 +5,9 @@ import { createCredentialStore } from "./credential-store.mjs";
|
|
|
5
5
|
import { readCredentials } from "./provider-keys.mjs";
|
|
6
6
|
import { cliproxyBase, cliproxyProvider, extendCliproxyProvider, registryLiveCliproxy } from "./cliproxy-provider.mjs";
|
|
7
7
|
export { cliproxyLiveModel } from "./cliproxy-provider.mjs";
|
|
8
|
+
import { OPENAI_COMPATIBLE_ID, openaiCompatibleProvider, refreshOpenAICompatible } from "./openai-compatible-provider.mjs";
|
|
9
|
+
import { rolesByMain } from "./model-roles.mjs";
|
|
10
|
+
export { MODEL_ROLES, fallbackChain, resolveRoleModels } from "./model-roles.mjs";
|
|
8
11
|
|
|
9
12
|
// #379 (F2 pass 5 finding 4): pi never forwards a per-request `fetch` to the
|
|
10
13
|
// codex API module — `pi-agent-core`'s `createRequestOptions` (the options
|
|
@@ -58,30 +61,38 @@ function ensureCodexFetchPatched() {
|
|
|
58
61
|
export const PROVIDERS = [
|
|
59
62
|
{ id: "openai-codex", label: "ChatGPT (OpenAI Codex)", auth: "chatgpt-oauth", env: [] },
|
|
60
63
|
{ id: "anthropic", label: "Anthropic", auth: "api_key", env: ["ANTHROPIC_API_KEY"] },
|
|
64
|
+
// #claude-subscription: Claude Pro/Max OAuth sign-in; requests run through the official
|
|
65
|
+
// Claude Code binary (anthropic-subscription.mjs), never against the API with a raw token.
|
|
66
|
+
{ id: "anthropic-subscription", label: "Claude (Anthropic)", auth: "oauth", env: [] },
|
|
61
67
|
{ id: "openai", label: "OpenAI", auth: "api_key", env: ["OPENAI_API_KEY"] },
|
|
62
68
|
{ id: "google", label: "Google Gemini", auth: "api_key", env: ["GEMINI_API_KEY", "GOOGLE_API_KEY"] },
|
|
69
|
+
{ id: "minimax", label: "MiniMax", auth: "api_key", env: ["MINIMAX_API_KEY"] }, { id: "minimax-cn", label: "MiniMax (China)", auth: "api_key", env: ["MINIMAX_CN_API_KEY"] },
|
|
63
70
|
{ id: "openrouter", label: "OpenRouter", auth: "api_key", env: ["OPENROUTER_API_KEY"] },
|
|
64
71
|
{ id: "cliproxy", label: "CLIProxyAPI", auth: "api_key", env: ["CLIPROXY_API_KEY"] },
|
|
72
|
+
// #885: LM Studio, Ollama or any OpenAI-compatible server; its address rides the stored credential
|
|
73
|
+
{ id: OPENAI_COMPATIBLE_ID, label: "OpenAI-compatible server", auth: "api_key", env: [] },
|
|
65
74
|
];
|
|
66
75
|
|
|
67
76
|
const factories = {
|
|
68
|
-
"openai-codex": "openaiCodexProvider",
|
|
69
|
-
anthropic: "anthropicProvider",
|
|
70
|
-
openai: "openaiProvider",
|
|
71
|
-
google: "googleProvider",
|
|
77
|
+
"openai-codex": "openaiCodexProvider", openai: "openaiProvider", google: "googleProvider", minimax: "minimaxProvider", "minimax-cn": "minimaxCnProvider",
|
|
72
78
|
openrouter: "openrouterProvider",
|
|
73
79
|
};
|
|
74
80
|
|
|
75
81
|
const providerConfig = (id) => PROVIDERS.find((provider) => provider.id === id);
|
|
76
82
|
|
|
77
|
-
export async function loadProvider(id,
|
|
78
|
-
|
|
79
|
-
const config = providerConfig(id);
|
|
80
|
-
if (!config) throw Object.assign(new Error(`Unknown provider: ${id}`), { code: "UNKNOWN_PROVIDER" });
|
|
83
|
+
export async function loadProvider(id, { baseUrl, env = process.env } = {}) {
|
|
84
|
+
if (!providerConfig(id)) throw Object.assign(new Error(`Unknown provider: ${id}`), { code: "UNKNOWN_PROVIDER" });
|
|
81
85
|
if (id === "openai-codex") ensureCodexFetchPatched();
|
|
86
|
+
// Both Anthropic lanes come from CozyClay's own module: the subscription lane, and pi-ai's
|
|
87
|
+
// API-key lane minus the OAuth login it also advertises (see anthropicLaneProvider).
|
|
82
88
|
const provider = id === "cliproxy"
|
|
83
89
|
? await cliproxyProvider({ baseUrl, env })
|
|
84
|
-
: await
|
|
90
|
+
: id === OPENAI_COMPATIBLE_ID ? await openaiCompatibleProvider() : id === "anthropic" || id === "anthropic-subscription"
|
|
91
|
+
? await import("./anthropic-subscription.mjs").then((module) => module.anthropicLaneProvider(id))
|
|
92
|
+
: await import(`@earendil-works/pi-ai/providers/${id}`).then((module) => module[factories[id]]());
|
|
93
|
+
// pi 0.99 gave the API-key OpenAI lane a ChatGPT sign-in too; CozyClay's ChatGPT sign-in is the
|
|
94
|
+
// openai-codex lane, so the panel keeps showing one ChatGPT button.
|
|
95
|
+
if (id === "openai") provider.auth = { ...provider.auth, oauth: undefined };
|
|
85
96
|
if (baseUrl && id !== "cliproxy") {
|
|
86
97
|
provider.baseUrl = baseUrl;
|
|
87
98
|
const getModels = provider.getModels.bind(provider);
|
|
@@ -95,19 +106,18 @@ const piEffort = (level) => level === "none" ? "off" : level === "ultra" ? "max"
|
|
|
95
106
|
const wireEffort = (level) => (level === "off" ? "none" : level);
|
|
96
107
|
|
|
97
108
|
const registryCatalogueState = new WeakMap();
|
|
98
|
-
|
|
99
109
|
function catalogueState(models) {
|
|
100
110
|
let state = registryCatalogueState.get(models);
|
|
101
111
|
if (!state) {
|
|
102
|
-
state = { fetch: null, models: [], extendedProviders: new WeakSet(), cliproxyFetch: null };
|
|
112
|
+
state = { fetch: null, models: [], extendedProviders: new WeakSet(), cliproxyFetch: null, cliproxyFetchError: null };
|
|
103
113
|
registryCatalogueState.set(models, state);
|
|
104
114
|
}
|
|
105
115
|
return state;
|
|
106
116
|
}
|
|
107
|
-
|
|
108
117
|
function livePiModel(model, template) {
|
|
109
118
|
const supported = new Set(model.efforts.map(piEffort));
|
|
110
|
-
|
|
119
|
+
// map values are what goes on the wire: pi sends a model's off mapping, and Codex calls it "none"
|
|
120
|
+
const thinkingLevelMap = Object.fromEntries(PI_EFFORT_LEVELS.map((level) => [level, supported.has(level) ? wireEffort(level) : null]));
|
|
111
121
|
return { ...template, id: model.id, name: model.label, thinkingLevelMap };
|
|
112
122
|
}
|
|
113
123
|
|
|
@@ -171,6 +181,8 @@ export async function resolveModel(requested, options = {}) {
|
|
|
171
181
|
if (!modelId) throw unknownModel(requested);
|
|
172
182
|
const models = options.models ?? await createModels(options);
|
|
173
183
|
if (!providerConfig(provider) && !models.getProvider?.(provider)) throw unknownModel(requested);
|
|
184
|
+
// a custom server's models are its live /v1/models list, read again when a turn names one not listed yet (#885)
|
|
185
|
+
if (provider === OPENAI_COMPATIBLE_ID && !models.getModel(provider, modelId)) await refreshOpenAICompatible(models);
|
|
174
186
|
const model = models.getModel(provider, modelId);
|
|
175
187
|
if (!model) throw unknownModel(requested);
|
|
176
188
|
return { provider, modelId, model };
|
|
@@ -207,7 +219,8 @@ function resolveAuthSource(provider, { auth, saved, env }) {
|
|
|
207
219
|
}
|
|
208
220
|
|
|
209
221
|
export function providerStatus({ auth = defaultAuth, keys = defaultKeys, env = process.env } = {}) {
|
|
210
|
-
|
|
222
|
+
// a stored API key (keys file) and an OAuth sign-in such as a subscription (auth.json) both count
|
|
223
|
+
const saved = { ...keys.readKeys(), ...readCredentials() };
|
|
211
224
|
return PROVIDERS.map((provider) => {
|
|
212
225
|
const authSource = resolveAuthSource(provider, { auth, saved, env });
|
|
213
226
|
return { id: provider.id, label: provider.label, authSource, signedIn: !!authSource };
|
|
@@ -254,119 +267,6 @@ function liveModelsCodex(result) {
|
|
|
254
267
|
}).filter((model) => model.id);
|
|
255
268
|
}
|
|
256
269
|
|
|
257
|
-
// --- model roles (#717) ------------------------------------------------------
|
|
258
|
-
//
|
|
259
|
-
// One picked model (main) implies the rest of the lineup a turn may lean on: a
|
|
260
|
-
// cheap helper, a mid-tier advisor, a model that can see images, and the
|
|
261
|
-
// ordered alternatives to try when main fails. Subscription accounts usually
|
|
262
|
-
// have exactly one signed-in provider, so every derived role stays on main's
|
|
263
|
-
// provider; only the fallback chain may cross to another signed-in provider
|
|
264
|
-
// that serves the very same model id. Both functions are pure over a
|
|
265
|
-
// listAgentModels-shaped `{ providers }` catalog and never throw.
|
|
266
|
-
|
|
267
|
-
export const MODEL_ROLES = ["main", "helper", "vision", "advisor"];
|
|
268
|
-
|
|
269
|
-
const CLAUDE_TIERS = ["opus", "sonnet", "haiku"];
|
|
270
|
-
const GPT_MINI_CLASS = /mini|nano/;
|
|
271
|
-
const newestFirst = (a, b) => b.id.localeCompare(a.id, undefined, { numeric: true });
|
|
272
|
-
|
|
273
|
-
function splitModelKey(key) {
|
|
274
|
-
const slash = typeof key === "string" ? key.indexOf("/") : -1;
|
|
275
|
-
return slash > 0 ? { providerId: key.slice(0, slash), modelId: key.slice(slash + 1) } : null;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const modelFamily = (id) => /^claude-/.test(id) ? "claude" : /^gpt-/.test(id) ? "gpt" : null;
|
|
279
|
-
const claudeTier = (id) => CLAUDE_TIERS.find((tier) => id.startsWith(`claude-${tier}`)) ?? null;
|
|
280
|
-
const takesImages = (model) => Array.isArray(model?.input) && model.input.includes("image");
|
|
281
|
-
const modelKeyOf = (providerId, model) => model.key ?? `${providerId}/${model.id}`;
|
|
282
|
-
|
|
283
|
-
/** main's provider entry, main's own model entry and the same-family models on that provider. */
|
|
284
|
-
function locateMain(mainKey, catalog) {
|
|
285
|
-
const parts = splitModelKey(mainKey);
|
|
286
|
-
const providers = Array.isArray(catalog?.providers) ? catalog.providers : [];
|
|
287
|
-
const provider = parts && providers.find((entry) => entry?.id === parts.providerId);
|
|
288
|
-
const models = Array.isArray(provider?.models) ? provider.models.filter((model) => typeof model?.id === "string") : [];
|
|
289
|
-
const main = parts && models.find((model) => model.id === parts.modelId);
|
|
290
|
-
if (!main) return null;
|
|
291
|
-
const family = modelFamily(main.id);
|
|
292
|
-
return { providers, provider, models, main, family, sameFamily: family ? models.filter((model) => modelFamily(model.id) === family) : [] };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** The first preference that matches a model: an exact id, or the newest id a pattern matches. */
|
|
296
|
-
function pickModel(models, preferences) {
|
|
297
|
-
for (const preference of preferences) {
|
|
298
|
-
const match = typeof preference === "string"
|
|
299
|
-
? models.find((model) => model.id === preference)
|
|
300
|
-
: [...models].filter((model) => preference.test(model.id)).sort(newestFirst)[0];
|
|
301
|
-
if (match) return match;
|
|
302
|
-
}
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** One tier of a Claude lineup, preferring main's own version (opus-5-5 -> sonnet-5-5). */
|
|
307
|
-
function claudeTierModel(models, mainId, tier) {
|
|
308
|
-
const current = claudeTier(mainId);
|
|
309
|
-
return pickModel(models, [current ? mainId.replace(`claude-${current}`, `claude-${tier}`) : null, new RegExp(`^claude-${tier}`)].filter(Boolean));
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export function resolveRoleModels(mainKey, catalog) {
|
|
313
|
-
const main = typeof mainKey === "string" ? mainKey : "";
|
|
314
|
-
const found = locateMain(main, catalog);
|
|
315
|
-
if (!found) return { main, helper: main, vision: main, advisor: main };
|
|
316
|
-
const { provider, models, main: model, family, sameFamily } = found;
|
|
317
|
-
const key = (entry) => (entry ? modelKeyOf(provider.id, entry) : main);
|
|
318
|
-
const helper = family === "claude" ? pickModel(sameFamily, ["claude-haiku-5-5", /^claude-haiku/, /^claude-sonnet/])
|
|
319
|
-
: family === "gpt" ? pickModel(sameFamily, [GPT_MINI_CLASS, /luna/])
|
|
320
|
-
: null;
|
|
321
|
-
const advisor = family === "claude" ? pickModel(sameFamily, ["claude-sonnet-5-5", /^claude-sonnet/])
|
|
322
|
-
: family === "gpt" ? pickModel(sameFamily, ["gpt-6-astra"])
|
|
323
|
-
: null;
|
|
324
|
-
const vision = takesImages(model) ? model : models.find(takesImages);
|
|
325
|
-
return { main, helper: key(helper), vision: key(vision), advisor: key(advisor) };
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function fallbackChain(mainKey, catalog) {
|
|
329
|
-
const found = locateMain(mainKey, catalog);
|
|
330
|
-
if (!found) return [];
|
|
331
|
-
const { providers, provider, main, family, sameFamily } = found;
|
|
332
|
-
const chain = [];
|
|
333
|
-
// The same model served by another signed-in provider is the closest substitute there is.
|
|
334
|
-
for (const other of providers) {
|
|
335
|
-
if (other === provider || !other?.signedIn || !Array.isArray(other.models)) continue;
|
|
336
|
-
const same = other.models.find((model) => model?.id === main.id);
|
|
337
|
-
if (same) chain.push(modelKeyOf(other.id, same));
|
|
338
|
-
}
|
|
339
|
-
// Then a step down main's own family on main's provider.
|
|
340
|
-
if (family === "claude") {
|
|
341
|
-
const tier = CLAUDE_TIERS.indexOf(claudeTier(main.id));
|
|
342
|
-
if (tier !== -1) {
|
|
343
|
-
for (const lower of CLAUDE_TIERS.slice(tier + 1)) {
|
|
344
|
-
const model = claudeTierModel(sameFamily, main.id, lower);
|
|
345
|
-
if (model) chain.push(modelKeyOf(provider.id, model));
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
} else if (family === "gpt" && !GPT_MINI_CLASS.test(main.id)) {
|
|
349
|
-
for (const model of [pickModel(sameFamily, ["gpt-6-astra"]), pickModel(sameFamily, [GPT_MINI_CLASS])]) {
|
|
350
|
-
if (model) chain.push(modelKeyOf(provider.id, model));
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
const mainKeyFound = modelKeyOf(provider.id, main);
|
|
354
|
-
return [...new Set(chain)].filter((key) => key !== mainKeyFound).slice(0, 3);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/** `roles.byMain` for listAgentModels: every model of every signed-in provider. */
|
|
358
|
-
function rolesByMain(catalog) {
|
|
359
|
-
const byMain = {};
|
|
360
|
-
for (const provider of catalog.providers) {
|
|
361
|
-
if (!provider.signedIn) continue;
|
|
362
|
-
for (const model of provider.models) {
|
|
363
|
-
const { helper, vision, advisor } = resolveRoleModels(model.key, catalog);
|
|
364
|
-
byMain[model.key] = { helper, vision, advisor, fallback: fallbackChain(model.key, catalog) };
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
return { byMain };
|
|
368
|
-
}
|
|
369
|
-
|
|
370
270
|
const astraFirst = (a, b) => Number(b.id === "gpt-6-astra") - Number(a.id === "gpt-6-astra");
|
|
371
271
|
const cliproxyFirst = (a, b) => astraFirst(a, b) || Number(b.id === "claude-fable-5-1") - Number(a.id === "claude-fable-5-1");
|
|
372
272
|
|
|
@@ -379,7 +279,8 @@ const cliproxyFirst = (a, b) => astraFirst(a, b) || Number(b.id === "claude-fabl
|
|
|
379
279
|
export async function listAgentModels({ models, codex, auth = defaultAuth, keys = defaultKeys, env = process.env } = {}) {
|
|
380
280
|
const { getSupportedThinkingLevels } = await import("@earendil-works/pi-ai");
|
|
381
281
|
const resolvedModels = models ?? await createModels({ auth, keys, env });
|
|
382
|
-
|
|
282
|
+
// a stored API key (keys file) and an OAuth sign-in such as a subscription (auth.json) both count
|
|
283
|
+
const saved = { ...keys.readKeys(), ...readCredentials() };
|
|
383
284
|
const providers = [];
|
|
384
285
|
for (const provider of PROVIDERS) {
|
|
385
286
|
const authResult = await resolvedModels.getAuth(provider.id).catch(() => undefined);
|
|
@@ -408,14 +309,18 @@ export async function listAgentModels({ models, codex, auth = defaultAuth, keys
|
|
|
408
309
|
}
|
|
409
310
|
}
|
|
410
311
|
list = [...list].sort(cliproxyFirst);
|
|
312
|
+
const error = catalogueState(resolvedModels).cliproxyFetchError; providers.push({ id: provider.id, label: provider.label, signedIn, authSource, models: list, ...(error ? { error } : {}) }); continue;
|
|
313
|
+
} else if (provider.id === OPENAI_COMPATIBLE_ID && signedIn) {
|
|
314
|
+
// the server's own /v1/models is the lineup, read on every listing so a model loaded since then shows up
|
|
315
|
+
const error = await refreshOpenAICompatible(resolvedModels);
|
|
316
|
+
providers.push({ id: provider.id, label: provider.label, signedIn, authSource, models: catalogModels(resolvedModels, provider.id, getSupportedThinkingLevels), ...(error ? { error } : {}) }); continue;
|
|
411
317
|
}
|
|
412
318
|
providers.push({ id: provider.id, label: provider.label, signedIn, authSource, models: list });
|
|
413
319
|
}
|
|
414
320
|
// a provider outside the curated list joins the picker once a pi login stored its credential here (#pi-login); ambient
|
|
415
321
|
// credentials (an AWS profile, cloud ADC files) alone do not list it, so the picker shows what the author connected
|
|
416
|
-
const stored = new Set([...Object.keys(readCredentials()), ...Object.keys(saved)]);
|
|
417
322
|
for (const provider of resolvedModels.getProviders?.() ?? []) {
|
|
418
|
-
const check = providerConfig(provider.id) || !
|
|
323
|
+
const check = providerConfig(provider.id) || !(provider.id in saved) ? undefined : await resolvedModels.checkAuth?.(provider.id).catch(() => undefined);
|
|
419
324
|
if (check) providers.push({ id: provider.id, label: provider.name ?? provider.id, signedIn: true, authSource: check.type === "oauth" || /stored/i.test(check.source ?? "") ? "file" : "env", models: catalogModels(resolvedModels, provider.id, getSupportedThinkingLevels) });
|
|
420
325
|
}
|
|
421
326
|
// The flat union is what the panel's dropdown reads today (`models[].id`);
|
package/bin/cozyclay.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { createInterface } from "node:readline";
|
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
import { ensureMcpRuntime, runMcp } from "./mcp-runtime.mjs";
|
|
25
25
|
import { openBrowser } from "./open-browser.mjs";
|
|
26
|
+
import { maybeAskForStar } from "./star-prompt.mjs";
|
|
26
27
|
import { checkForUpdate, readUpdateStatus, runUpdate } from "./update-check.mjs";
|
|
27
28
|
import { handleOAuthRequest } from "./codex-auth.mjs";
|
|
28
29
|
import { createAgentHandler } from "./agent/agent-routes.mjs";
|
|
@@ -120,12 +121,8 @@ function parseArgs(argv) {
|
|
|
120
121
|
return opts;
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
const REPO_URL = "https://github.com/NomaDamas/CozyClay";
|
|
124
124
|
const STATE_DIR = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "cozyclay");
|
|
125
125
|
const STATE_FILE = join(STATE_DIR, "state.json");
|
|
126
|
-
// Only worth asking someone who actually used the thing. A prompt three
|
|
127
|
-
// seconds in is a popup; a prompt after a real session is a question.
|
|
128
|
-
const STAR_AFTER_MS = Number(process.env.COZYCLAY_STAR_AFTER_MS ?? 60_000);
|
|
129
126
|
|
|
130
127
|
async function askFirstLaunchSource() {
|
|
131
128
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
@@ -197,49 +194,6 @@ function writeState(patch) {
|
|
|
197
194
|
}
|
|
198
195
|
}
|
|
199
196
|
|
|
200
|
-
async function starCount() {
|
|
201
|
-
try {
|
|
202
|
-
const res = await fetch("https://api.github.com/repos/NomaDamas/CozyClay", {
|
|
203
|
-
headers: { accept: "application/vnd.github+json" },
|
|
204
|
-
signal: AbortSignal.timeout(2500),
|
|
205
|
-
});
|
|
206
|
-
if (!res.ok) return null;
|
|
207
|
-
const body = await res.json();
|
|
208
|
-
return Number.isInteger(body?.stargazers_count) ? body.stargazers_count : null;
|
|
209
|
-
} catch {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Asked once, on the way out, and never again on this machine. A CLI that
|
|
215
|
-
// nags on every run is worse than one that never asks.
|
|
216
|
-
async function maybeAskForStar(startedAt, opts) {
|
|
217
|
-
if (!opts.star) return;
|
|
218
|
-
if (process.env.CI || process.env.COZYCLAY_NO_STAR) return;
|
|
219
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
220
|
-
if (Date.now() - startedAt < STAR_AFTER_MS) return;
|
|
221
|
-
if (readState().starPromptedAt) return;
|
|
222
|
-
|
|
223
|
-
writeState({ starPromptedAt: new Date().toISOString() });
|
|
224
|
-
const stars = await starCount();
|
|
225
|
-
const tally = stars === null ? "" : ` (${stars} so far)`;
|
|
226
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
227
|
-
const answer = await new Promise((done) => {
|
|
228
|
-
const timer = setTimeout(() => done(""), 12_000);
|
|
229
|
-
rl.question(`\nWas CozyClay any good? A star helps people find it${tally}. Open GitHub? [Y/n] `, (a) => {
|
|
230
|
-
clearTimeout(timer);
|
|
231
|
-
done(a);
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
rl.close();
|
|
235
|
-
if (/^(y|yes|)$/i.test(answer.trim())) {
|
|
236
|
-
openBrowser(REPO_URL);
|
|
237
|
-
console.log("Thanks. Opening the repo.");
|
|
238
|
-
} else {
|
|
239
|
-
console.log(`Fair enough. ${REPO_URL} if you change your mind.`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
197
|
// Read on every launch now, so it must not be able to take the launcher down:
|
|
244
198
|
// the version is cosmetic here, and "0.0.0" just suppresses a bogus notice.
|
|
245
199
|
function readVersion() {
|
|
@@ -343,7 +297,7 @@ async function shutdown(exitCode = 0) {
|
|
|
343
297
|
shuttingDown = true;
|
|
344
298
|
if (server?.listening) await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
345
299
|
try {
|
|
346
|
-
await maybeAskForStar(startedAt, opts);
|
|
300
|
+
await maybeAskForStar(startedAt, opts, { readState, writeState });
|
|
347
301
|
} catch {
|
|
348
302
|
/* never let the goodbye prompt hold the process hostage */
|
|
349
303
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-time "star the repo" question on the way out of `npx cozyclay` (#900).
|
|
3
|
+
*
|
|
4
|
+
* With the GitHub CLI installed and signed in, a "y" stars the repo through `gh` on
|
|
5
|
+
* the spot: no browser hop, which is where most people who meant to star it got lost.
|
|
6
|
+
* Without gh, a "yes" opens the repo page as before. Asked once per machine, after a
|
|
7
|
+
* real session, never in CI or without a terminal, and never to an account that has
|
|
8
|
+
* already starred it. Every failure here is silent: this runs while the launcher exits.
|
|
9
|
+
*/
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
12
|
+
import { openBrowser } from "./open-browser.mjs";
|
|
13
|
+
|
|
14
|
+
const REPO = "NomaDamas/CozyClay";
|
|
15
|
+
const REPO_URL = `https://github.com/${REPO}`;
|
|
16
|
+
const STARRED_PATH = `/user/starred/${REPO}`;
|
|
17
|
+
const ANSWER_TIMEOUT_MS = 12_000;
|
|
18
|
+
// Only worth asking someone who actually used the thing. A prompt three
|
|
19
|
+
// seconds in is a popup; a prompt after a real session is a question.
|
|
20
|
+
const STAR_AFTER_MS = Number(process.env.COZYCLAY_STAR_AFTER_MS ?? 60_000);
|
|
21
|
+
|
|
22
|
+
/** Runs gh and resolves true on exit 0. No gh on PATH, a timeout or an error status is false. */
|
|
23
|
+
function runGh(args) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
try {
|
|
26
|
+
execFile("gh", args, {
|
|
27
|
+
timeout: 4000,
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
env: { ...process.env, GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1" },
|
|
30
|
+
}, (error) => resolve(!error));
|
|
31
|
+
} catch {
|
|
32
|
+
resolve(false);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function starCount() {
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`https://api.github.com/repos/${REPO}`, {
|
|
40
|
+
headers: { accept: "application/vnd.github+json" },
|
|
41
|
+
signal: AbortSignal.timeout(2500),
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) return null;
|
|
44
|
+
const body = await res.json();
|
|
45
|
+
return Number.isInteger(body?.stargazers_count) ? body.stargazers_count : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function askLine(question) {
|
|
52
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
53
|
+
return new Promise((done) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
rl.close();
|
|
56
|
+
done("");
|
|
57
|
+
}, ANSWER_TIMEOUT_MS);
|
|
58
|
+
rl.question(question, (answer) => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
rl.close();
|
|
61
|
+
done(answer);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Asked once, on the way out, and never again on this machine. A CLI that nags on
|
|
68
|
+
* every run is worse than one that never asks. Everything after `state` is injectable
|
|
69
|
+
* so the test can drive it without a terminal, gh, or the network.
|
|
70
|
+
*/
|
|
71
|
+
export async function maybeAskForStar(startedAt, opts, {
|
|
72
|
+
readState,
|
|
73
|
+
writeState,
|
|
74
|
+
env = process.env,
|
|
75
|
+
interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
76
|
+
now = Date.now,
|
|
77
|
+
minSessionMs = STAR_AFTER_MS,
|
|
78
|
+
gh = runGh,
|
|
79
|
+
ask = askLine,
|
|
80
|
+
stars = starCount,
|
|
81
|
+
open = openBrowser,
|
|
82
|
+
log = console.log,
|
|
83
|
+
}) {
|
|
84
|
+
if (!opts.star) return;
|
|
85
|
+
if (env.CI || env.COZYCLAY_NO_STAR) return;
|
|
86
|
+
if (!interactive) return;
|
|
87
|
+
if (now() - startedAt < minSessionMs) return;
|
|
88
|
+
if (readState().starPromptedAt) return;
|
|
89
|
+
|
|
90
|
+
const promptedAt = new Date(now()).toISOString();
|
|
91
|
+
const viaGh = await gh(["auth", "status"]);
|
|
92
|
+
// GET /user/starred/<repo> answers 204 for a starred repo and 404 otherwise.
|
|
93
|
+
if (viaGh && await gh(["api", STARRED_PATH, "--silent"])) {
|
|
94
|
+
writeState({ starPromptedAt: promptedAt });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
writeState({ starPromptedAt: promptedAt });
|
|
98
|
+
const count = await stars();
|
|
99
|
+
const tally = count === null ? "" : ` (${count} so far)`;
|
|
100
|
+
|
|
101
|
+
if (viaGh) {
|
|
102
|
+
const answer = await ask(`\nWas CozyClay any good? A star helps people find it${tally}. Star ${REPO} with your gh account? [y/N] `);
|
|
103
|
+
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
104
|
+
log(`No problem. ${REPO_URL} if you change your mind.`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (await gh(["api", "--method", "PUT", STARRED_PATH, "--silent"])) {
|
|
108
|
+
log(`Starred ${REPO}. Thank you!`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
log("gh could not star it, so opening the repo instead.");
|
|
112
|
+
open(REPO_URL);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const answer = await ask(`\nWas CozyClay any good? A star helps people find it${tally}. Open GitHub? [Y/n] `);
|
|
117
|
+
if (/^(y|yes|)$/i.test(answer.trim())) {
|
|
118
|
+
open(REPO_URL);
|
|
119
|
+
log("Thanks. Opening the repo.");
|
|
120
|
+
} else {
|
|
121
|
+
log(`Fair enough. ${REPO_URL} if you change your mind.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
package/dist/app/index.html
CHANGED
|
@@ -34,15 +34,15 @@
|
|
|
34
34
|
<title>CozyClay Studio — 3D staging and previs in your browser</title>
|
|
35
35
|
|
|
36
36
|
<script defer src="https://static.cloudflareinsights.com/beacon.min.js" data-cf-beacon='{"token": "6a4384d1375d4ffc8a30a6346a8ad1a0"}'></script>
|
|
37
|
-
<script type="module" crossorigin src="/assets/app-
|
|
37
|
+
<script type="module" crossorigin src="/assets/app-KXdgBC-S.js"></script>
|
|
38
38
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
39
39
|
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-P2Xu9kJm.js">
|
|
40
|
-
<link rel="modulepreload" crossorigin href="/assets/task-board-
|
|
40
|
+
<link rel="modulepreload" crossorigin href="/assets/task-board-DFfNa0xg.js">
|
|
41
41
|
<link rel="modulepreload" crossorigin href="/assets/fflate.module-KbGj5-H6.js">
|
|
42
|
-
<link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-
|
|
43
|
-
<link rel="modulepreload" crossorigin href="/assets/motion3-compile-
|
|
42
|
+
<link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-Bz59Ghdz.js">
|
|
43
|
+
<link rel="modulepreload" crossorigin href="/assets/motion3-compile-DDG3wklD.js">
|
|
44
44
|
<link rel="stylesheet" crossorigin href="/assets/scene-asset-sync-BEjjhNEo.css">
|
|
45
|
-
<link rel="stylesheet" crossorigin href="/assets/app-
|
|
45
|
+
<link rel="stylesheet" crossorigin href="/assets/app-BdRlMf4J.css">
|
|
46
46
|
</head>
|
|
47
47
|
<body>
|
|
48
48
|
<div id="root">
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as e}from"./rolldown-runtime-hePW80VL.js";import{Ti as t,c as n,d as r,f as i,fa as a,l as o,m as s,o as c,v as l}from"./task-board-DFfNa0xg.js";import{o as u,r as d,t as f}from"./motion3-compile-DDG3wklD.js";var p=e(a(),1),m=2e3,h=(e,t)=>new Promise(n=>{let r=setTimeout(n,e);t.addEventListener(`abort`,()=>{clearTimeout(r),n()},{once:!0})});function g(e,n){(0,p.useEffect)(()=>{let r=new Map;for(let[t,i]of Object.entries(e))for(let e of i)r.set(e,n[t]??t);let i=()=>{for(let e of document.querySelectorAll(`[data-node-id]`)){let n=r.get(e.getAttribute(`data-node-id`));n?(e.setAttribute(`data-task-scope`,n),e.setAttribute(`title`,t(`In task: ${n}`,`작업 중: ${n}`,`任务中:${n}`))):e.hasAttribute(`data-task-scope`)&&(e.removeAttribute(`data-task-scope`),e.removeAttribute(`title`))}};i();let a=new MutationObserver(i);return a.observe(document.body,{childList:!0,subtree:!0}),()=>{a.disconnect(),r.clear(),i()}},[e,n])}function _({sessionId:e,pulse:t,labelOf:a,onMainNote:_}){let v=(0,p.useMemo)(()=>c(),[]),y=u(),[b,x]=(0,p.useReducer)((e,t)=>t.seed?s(e,t.seed):t.reset?n():r(e,t.event),void 0,n),[S,C]=(0,p.useState)({}),[w,T]=(0,p.useState)(f),E=(0,p.useRef)(0),D=(0,p.useRef)(_);(0,p.useEffect)(()=>{E.current=b.after,D.current=_},[b.after,_]),(0,p.useEffect)(()=>{x({reset:!0}),E.current=0},[e]),(0,p.useEffect)(()=>d(T),[]),(0,p.useEffect)(()=>y.subscribe(e=>{typeof e?.type!=`string`||!e.type.startsWith(`task.`)||(C(t=>i(t,e)),(e.type===`task.userEdit`||e.type===`task.undone`)&&x({event:e}))}),[y]),(0,p.useEffect)(()=>{if(!e)return;let t=new AbortController;return(async()=>{for(;!t.signal.aborted;){try{let{tasks:n}=await v.board(e);x({seed:n}),await v.events(e,E.current,e=>{if(e.type===`main.turn`&&e.turnId){let n=``;v.frames(e.turnId,e=>{e.type===`text.delta`&&(n+=e.text??``),e.type===`tool.start`&&(n=``)},t.signal).then(()=>D.current?.(n),()=>{})}x({event:e})},t.signal)}catch{}await h(m,t.signal)}})(),()=>t.abort()},[v,e,t]);let O=b.order.filter(e=>b.tasks[e]?.status===`running`&&b.tasks[e]?.turnId),k=O.map(e=>`${e}:${b.tasks[e].turnId}`).join();return(0,p.useEffect)(()=>{let e=O.map(e=>{let t=new AbortController,{turnId:n}=b.tasks[e];return v.frames(n,t=>{let n=o(t,a);n&&x({event:{type:`task.progress`,taskId:e,status:`running`,progress:n}})},t.signal).catch(()=>{}),t});return()=>e.forEach(e=>e.abort())},[v,k,a]),g(S,(0,p.useMemo)(()=>Object.fromEntries(b.order.map(e=>[e,b.tasks[e].title])),[b])),(0,p.useEffect)(()=>{l.set({client:v,labelOf:a,undo:async e=>{let t=await y.run(`task.undo`,{taskId:e});if(t?.ok===!1)throw Error(t.message??t.code);x({event:{type:`task.undone`,taskId:e}})}})},[y,v,a]),(0,p.useEffect)(()=>{l.set({board:b,activity:w})},[b,w]),(0,p.useEffect)(()=>()=>l.set({board:n(),open:null}),[e]),null}export{_ as default};
|