@causyne/agent 1.10.19 → 1.10.21
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/docs/ARCHITECTURE.md +20 -0
- package/docs/THIRD_PARTY_NOTICES.md +44 -0
- package/native-skills/media/creative-studio-pipeline/SKILL.md +52 -0
- package/native-skills/media/photo-studio-automation/SKILL.md +53 -0
- package/native-skills/media/video-timeline-assembly/SKILL.md +52 -0
- package/package.json +1 -1
- package/src/agentLoop.js +4 -0
- package/src/appChannel.js +7 -3
- package/src/cliMode.js +33 -67
- package/src/commands.js +56 -8
- package/src/compiledSkills.js +15 -0
- package/src/computerUse/visionGrounding.js +249 -0
- package/src/computerUse/visionGrounding.ts +331 -0
- package/src/config.js +80 -11
- package/src/creative/apps.js +69 -0
- package/src/creative/apps.ts +58 -0
- package/src/creative/blenderBatch.js +233 -0
- package/src/creative/blenderBatch.ts +217 -0
- package/src/creative/pipeline.js +56 -0
- package/src/creative/pipeline.ts +82 -0
- package/src/creative/studioScripts.js +374 -0
- package/src/creative/studioScripts.ts +378 -0
- package/src/decisionPolicy.js +2 -0
- package/src/decisionPolicy.ts +1 -0
- package/src/dynamicCatalogue.js +270 -0
- package/src/dynamicCatalogue.ts +283 -0
- package/src/entrypoint.js +2 -1
- package/src/modelCatalogue.d.ts +5 -0
- package/src/modelCatalogue.js +49 -18
- package/src/providers/anthropic.js +4 -0
- package/src/sandbox.js +8 -1
- package/src/toolPacks.js +6 -3
- package/src/tools/computerUse.js +139 -0
- package/src/tools/computerUse.ts +147 -0
- package/src/tools/creativeApps.js +106 -0
- package/src/tools/creativeApps.ts +99 -0
- package/src/tools/desktopAct.d.ts +10 -0
- package/src/tools/registry.js +5 -1
- package/src/tools/uiForge.js +132 -0
- package/src/tools/uiForge.ts +145 -0
- package/src/tui/banner.js +3 -1
- package/src/tui/banner.ts +3 -1
- package/src/tui/dialogs.js +10 -0
- package/src/tui/home.js +29 -72
- package/src/tui/setup.js +104 -0
- package/src/tui/setup.ts +98 -0
- package/src/uiEngine/uiForge.js +211 -0
- package/src/uiEngine/uiForge.ts +258 -0
package/src/tui/banner.js
CHANGED
|
@@ -237,7 +237,9 @@ function shortPath(p, maxw) {
|
|
|
237
237
|
}
|
|
238
238
|
function statusLine(info, maxw, unicode) {
|
|
239
239
|
const sep = unicode ? ' · ' : ' | ';
|
|
240
|
-
const
|
|
240
|
+
const providerModel = [info.provider, info.model].filter(Boolean).join('/');
|
|
241
|
+
const providerPart = providerModel || 'select provider';
|
|
242
|
+
const parts = [info.version ? 'v' + String(info.version).replace(/^v/, '') : '', providerPart, info.mode ?? '', info.cwd ?? ''].filter(Boolean);
|
|
241
243
|
const fits = () => parts.join(sep).length <= maxw;
|
|
242
244
|
if (!fits() && info.cwd) {
|
|
243
245
|
const others = parts.slice(0, -1).join(sep).length + (parts.length > 1 ? sep.length : 0);
|
package/src/tui/banner.ts
CHANGED
|
@@ -240,7 +240,9 @@ function shortPath(p: string, maxw: number): string {
|
|
|
240
240
|
}
|
|
241
241
|
function statusLine(info: BannerInfo, maxw: number, unicode: boolean): string {
|
|
242
242
|
const sep = unicode ? ' · ' : ' | ';
|
|
243
|
-
const
|
|
243
|
+
const providerModel = [info.provider, info.model].filter(Boolean).join('/');
|
|
244
|
+
const providerPart = providerModel || 'select provider';
|
|
245
|
+
const parts = [info.version ? 'v' + String(info.version).replace(/^v/, '') : '', providerPart, info.mode ?? '', info.cwd ?? ''].filter(Boolean);
|
|
244
246
|
const fits = (): boolean => parts.join(sep).length <= maxw;
|
|
245
247
|
if (!fits() && info.cwd) {
|
|
246
248
|
const others = parts.slice(0, -1).join(sep).length + (parts.length > 1 ? sep.length : 0);
|
package/src/tui/dialogs.js
CHANGED
|
@@ -38,9 +38,17 @@ export class SelectLayer extends Layer {
|
|
|
38
38
|
emptyText: options.emptyText
|
|
39
39
|
});
|
|
40
40
|
this.onPick = options.onPick || null;
|
|
41
|
+
this.settled = false;
|
|
41
42
|
this.footer = options.footer || dialogHint();
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/** Dismissal must resolve pending picker flows, just like text inputs. */
|
|
46
|
+
answer() {
|
|
47
|
+
if (this.settled) return;
|
|
48
|
+
this.settled = true;
|
|
49
|
+
if (this.onPick) this.onPick(null);
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
preferredHeight() {
|
|
45
53
|
// filter row + rule + one row per item (+ its category header) + frame.
|
|
46
54
|
const categories = new Set(this.list.filtered.map((entry) => entry.item.category).filter(Boolean));
|
|
@@ -66,7 +74,9 @@ export class SelectLayer extends Layer {
|
|
|
66
74
|
|
|
67
75
|
if (action === 'select') {
|
|
68
76
|
const item = this.list.current;
|
|
77
|
+
this.settled = true;
|
|
69
78
|
const result = this.onPick ? this.onPick(item) : null;
|
|
79
|
+
if (result === 'keep') this.settled = false;
|
|
70
80
|
return result === 'keep' ? 'handled' : 'close';
|
|
71
81
|
}
|
|
72
82
|
if (action === 'cancel') return 'close';
|
package/src/tui/home.js
CHANGED
|
@@ -2,15 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
import { SelectLayer, InputLayer } from './dialogs.js';
|
|
4
4
|
import { paint, dim, bold, badge, GLYPHS } from './theme.js';
|
|
5
|
+
import { runSetup } from './setup.js';
|
|
5
6
|
import { DEFAULT_MODEL_BY_PROVIDER } from '../modelCatalogue.js';
|
|
6
7
|
|
|
7
8
|
/** Local runtime and provider summary. */
|
|
8
9
|
export function homeStatusLine({ config = {} } = {}) {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
const hasProvider = Boolean(config.provider && config.provider !== 'unconfigured');
|
|
11
|
+
const model = hasProvider
|
|
12
|
+
? (config.model ? `${config.provider}/${config.model}` : config.provider)
|
|
13
|
+
: 'no provider configured';
|
|
14
|
+
const configured = Boolean((hasProvider && config.apiKey) || config.provider === 'ollama');
|
|
11
15
|
return ' ' + [
|
|
12
16
|
paint('success', `${GLYPHS.bullet} local desktop`),
|
|
13
|
-
configured
|
|
17
|
+
configured
|
|
18
|
+
? dim(model)
|
|
19
|
+
: (hasProvider ? paint('warn', `${model} — no key`) : paint('warn', 'no provider configured — run /setup'))
|
|
14
20
|
].join(dim(` ${GLYPHS.dot} `));
|
|
15
21
|
}
|
|
16
22
|
|
|
@@ -41,7 +47,7 @@ export const PRIMARY_ACTIONS = [
|
|
|
41
47
|
kind: 'stay',
|
|
42
48
|
run: async () => (await import('../reporting.js')).buildUsageText()
|
|
43
49
|
},
|
|
44
|
-
{ id: 'config', label: '
|
|
50
|
+
{ id: 'config', label: 'setup', hint: 'provider · endpoint · model · permissions', kind: 'config' },
|
|
45
51
|
{ id: 'more', label: 'more…', hint: 'sessions · model · approvals · tools', kind: 'more' }
|
|
46
52
|
];
|
|
47
53
|
|
|
@@ -70,6 +76,13 @@ export const MORE_ACTIONS = [
|
|
|
70
76
|
// be the one path around the gate.
|
|
71
77
|
command: '/undo'
|
|
72
78
|
},
|
|
79
|
+
{
|
|
80
|
+
id: 'creative', label: 'creative apps', category: 'tools', hint: 'check installed apps and adapter readiness', kind: 'stay',
|
|
81
|
+
run: async () => {
|
|
82
|
+
const { creativeAppStatus } = await import('../creative/apps.js');
|
|
83
|
+
return (await creativeAppStatus()).map(app => `${app.label}: ${app.available ? 'available' : 'not connected'}\n ${app.detail}`).join('\n');
|
|
84
|
+
}
|
|
85
|
+
},
|
|
73
86
|
{ id: 'mcp', label: 'mcp', category: 'tools', hint: 'manage MCP servers', kind: 'mcp' },
|
|
74
87
|
{ id: 'help', label: 'commands', category: 'tools', hint: 'show every slash command', kind: 'command', command: '/help' },
|
|
75
88
|
{
|
|
@@ -111,16 +124,8 @@ export function homeHint() {
|
|
|
111
124
|
export { badge };
|
|
112
125
|
|
|
113
126
|
// ---------------------------------------------------------------------------
|
|
114
|
-
//
|
|
127
|
+
// Provider setup and MCP navigation
|
|
115
128
|
//
|
|
116
|
-
// Both of these used inquirer, which takes stdin while ChatPrompt is already
|
|
117
|
-
// holding it in raw mode — the two contend and the question lands on a
|
|
118
|
-
// half-drawn prompt. Inside home there IS a live region and a modal stack, so
|
|
119
|
-
// they can be asked properly. `causyne config` invoked on its own still uses the
|
|
120
|
-
// inquirer wizard, because standalone there is no live region to draw into and
|
|
121
|
-
// building a second one is how the deleted alt-screen shell started.
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
|
|
124
129
|
/** Provider choices for local CLI configuration. */
|
|
125
130
|
// Suggested models come from src/modelCatalogue.js rather than being repeated
|
|
126
131
|
// here. This list had drifted to `gpt-4o` and `gemini-2.5-pro` — two
|
|
@@ -140,65 +145,17 @@ export const PROVIDERS = [
|
|
|
140
145
|
{ id: 'ollama', label: 'Ollama', hint: 'local / offline' }
|
|
141
146
|
].map((entry) => ({ ...entry, model: DEFAULT_MODEL_BY_PROVIDER[entry.id] ?? '' }));
|
|
142
147
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
prompt.openLayer(new SelectLayer({
|
|
156
|
-
title: 'provider',
|
|
157
|
-
items: PROVIDERS.map((p) => ({ id: p.id, label: p.label, description: p.hint, value: p })),
|
|
158
|
-
onPick: (item) => {
|
|
159
|
-
if (!item) { resolve(null); return; }
|
|
160
|
-
const provider = item.value;
|
|
161
|
-
askModel(provider);
|
|
162
|
-
}
|
|
163
|
-
}));
|
|
164
|
-
|
|
165
|
-
function askModel(provider) {
|
|
166
|
-
const suggested = (config.provider === provider.id && config.model) ? config.model : provider.model;
|
|
167
|
-
prompt.openLayer(new InputLayer({
|
|
168
|
-
title: `model for ${provider.label}`,
|
|
169
|
-
lines: [
|
|
170
|
-
'Model ids change often. Type the one you want, or accept the suggestion.'
|
|
171
|
-
],
|
|
172
|
-
value: suggested,
|
|
173
|
-
placeholder: 'model id',
|
|
174
|
-
onSubmit: (model) => askKey(provider, (model || '').trim() || suggested),
|
|
175
|
-
onCancel: () => resolve(null)
|
|
176
|
-
}));
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function askKey(provider, model) {
|
|
180
|
-
prompt.openLayer(new InputLayer({
|
|
181
|
-
title: 'API key',
|
|
182
|
-
lines: [
|
|
183
|
-
`${provider.label} ${GLYPHS.arrow} ${model}`,
|
|
184
|
-
'',
|
|
185
|
-
'Stored encrypted in ~/.causyne/config.json. It never leaves this machine.'
|
|
186
|
-
],
|
|
187
|
-
mask: true,
|
|
188
|
-
value: '',
|
|
189
|
-
placeholder: 'paste your key (blank keeps the current one)',
|
|
190
|
-
onSubmit: (apiKey) => {
|
|
191
|
-
const next = loadConfig();
|
|
192
|
-
next.provider = provider.id;
|
|
193
|
-
next.model = model;
|
|
194
|
-
if (apiKey) next.apiKey = apiKey;
|
|
195
|
-
saveConfig(next);
|
|
196
|
-
resolve({ provider: provider.id, model });
|
|
197
|
-
},
|
|
198
|
-
onCancel: () => resolve(null)
|
|
199
|
-
}));
|
|
200
|
-
}
|
|
201
|
-
});
|
|
148
|
+
/** Run the shared setup transaction inside the existing prompt. */
|
|
149
|
+
export function configureInPrompt(prompt, store) {
|
|
150
|
+
return runSetup({
|
|
151
|
+
select: (options) => new Promise((resolve) => {
|
|
152
|
+
const layer = new SelectLayer({ ...options, onPick: resolve });
|
|
153
|
+
prompt.openLayer(layer);
|
|
154
|
+
}),
|
|
155
|
+
text: (options) => new Promise((resolve) => {
|
|
156
|
+
prompt.openLayer(new InputLayer({ ...options, onSubmit: resolve, onCancel: () => resolve(null) }));
|
|
157
|
+
})
|
|
158
|
+
}, PROVIDERS, store);
|
|
202
159
|
}
|
|
203
160
|
|
|
204
161
|
/** The MCP server list, as something you can act on rather than read. */
|
package/src/tui/setup.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const string = (value) => typeof value === 'string' ? value : '';
|
|
2
|
+
const customProviders = new Set(['openai-compatible', 'dashscope-byok', 'ollama']);
|
|
3
|
+
const defaults = {
|
|
4
|
+
ollama: 'http://localhost:11434/api/chat',
|
|
5
|
+
'dashscope-byok': 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions'
|
|
6
|
+
};
|
|
7
|
+
export function validateEndpoint(value) {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(value);
|
|
10
|
+
if (url.username || url.password || url.search || url.hash)
|
|
11
|
+
return 'Use an endpoint without credentials, query parameters, or a fragment.';
|
|
12
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
13
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback))
|
|
14
|
+
return 'Use HTTPS, or HTTP on localhost for a local model.';
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return 'Enter a complete endpoint URL.';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function runSetup(ui, providers, store, workspace = process.cwd()) {
|
|
22
|
+
const original = store.loadConfig();
|
|
23
|
+
let draft = { ...original };
|
|
24
|
+
async function text(title, lines, value = '', mask = false, validate = () => null) {
|
|
25
|
+
let error = null;
|
|
26
|
+
for (;;) {
|
|
27
|
+
const answer = await ui.text({ title, lines: error ? [...lines, error] : lines, value,
|
|
28
|
+
placeholder: mask ? 'paste key' : '', mask });
|
|
29
|
+
if (answer === null)
|
|
30
|
+
return null;
|
|
31
|
+
const cleaned = answer.trim();
|
|
32
|
+
error = /[\x00-\x1f\x7f]/.test(cleaned) ? 'Use a single line without control characters.' : validate(cleaned);
|
|
33
|
+
if (!error)
|
|
34
|
+
return cleaned;
|
|
35
|
+
value = mask ? '' : cleaned;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (;;) {
|
|
39
|
+
const selected = await ui.select({ title: 'Setup 1/5 · provider', items: providers.map(p => ({ id: p.id, label: p.label, description: p.hint })) });
|
|
40
|
+
if (!selected)
|
|
41
|
+
return null;
|
|
42
|
+
const provider = providers.find(p => p.id === selected.id);
|
|
43
|
+
if (!provider)
|
|
44
|
+
return null;
|
|
45
|
+
const sameProvider = draft.provider === provider.id;
|
|
46
|
+
let baseUrl = '';
|
|
47
|
+
if (customProviders.has(provider.id)) {
|
|
48
|
+
const answer = await text('Setup 2/5 · endpoint', ['Full chat endpoint URL, including /v1/chat/completions or /api/chat.',
|
|
49
|
+
'Credentials are sent to this endpoint when you use the model.'], (sameProvider ? string(draft.baseUrl) : '') || defaults[provider.id] || '', false, validateEndpoint);
|
|
50
|
+
// No network request is made by setup.
|
|
51
|
+
if (answer === null)
|
|
52
|
+
return null;
|
|
53
|
+
baseUrl = answer;
|
|
54
|
+
}
|
|
55
|
+
const model = await text('Setup 3/5 · model', ['Enter the model ID supported by your provider.'], (sameProvider ? string(draft.model) : '') || provider.model, false, value => value ? null : 'A model ID is required.');
|
|
56
|
+
if (model === null)
|
|
57
|
+
return null;
|
|
58
|
+
const unchangedEndpoint = sameProvider && (string(draft.baseUrl) || defaults[provider.id] || '') === baseUrl;
|
|
59
|
+
// Fixed endpoints have no editable URL. A changed endpoint never inherits a key.
|
|
60
|
+
const keepKey = sameProvider && (!customProviders.has(provider.id) || unchangedEndpoint);
|
|
61
|
+
let apiKey = '';
|
|
62
|
+
if (provider.id !== 'ollama') {
|
|
63
|
+
const oldKey = keepKey ? string(draft.apiKey) : '';
|
|
64
|
+
const answer = await text('Setup 3/5 · credentials', ['Stored encrypted locally; used to authenticate with your selected provider.',
|
|
65
|
+
oldKey ? 'Blank keeps the key for this same provider and endpoint.' : 'Enter a key for this provider.',
|
|
66
|
+
provider.id === 'openai-compatible' ? 'Blank is allowed for an endpoint that needs no authentication.' : ''], '', true, value => value || oldKey || provider.id === 'openai-compatible' ? null : 'An API key is required.');
|
|
67
|
+
if (answer === null)
|
|
68
|
+
return null;
|
|
69
|
+
apiKey = answer || oldKey;
|
|
70
|
+
}
|
|
71
|
+
const mode = await ui.select({ title: 'Setup 4/5 · autonomy', items: [
|
|
72
|
+
{ id: 'B', label: 'B · Supervised (Standard)', description: 'Recommended. Approval for dangerous actions; routine work continues.' },
|
|
73
|
+
{ id: 'A', label: 'A · Ask first (Strict)', description: 'Explicit confirmation before every tool call.' },
|
|
74
|
+
{ id: 'C', label: 'C · Autonomous (High Velocity)', description: 'Fewer interruptions; mandatory safety and decision gates remain.' }
|
|
75
|
+
] });
|
|
76
|
+
if (!mode || !['A', 'B', 'C'].includes(mode.id))
|
|
77
|
+
return null;
|
|
78
|
+
draft = { ...draft, provider: provider.id, model, apiKey, autonomyMode: mode.id };
|
|
79
|
+
if (baseUrl)
|
|
80
|
+
draft.baseUrl = baseUrl;
|
|
81
|
+
else
|
|
82
|
+
delete draft.baseUrl;
|
|
83
|
+
const review = await ui.select({ title: 'Setup 5/5 · review', items: [
|
|
84
|
+
{ id: 'save', label: 'Save configuration', description: `${provider.label} · ${model} · Mode ${mode.id}` },
|
|
85
|
+
{ id: 'edit', label: 'Edit choices', description: baseUrl || 'Provider default endpoint' },
|
|
86
|
+
{ id: 'cancel', label: 'Cancel without saving', description: `Workspace: ${workspace}` }
|
|
87
|
+
] });
|
|
88
|
+
if (!review || review.id === 'cancel')
|
|
89
|
+
return null;
|
|
90
|
+
if (review.id === 'edit')
|
|
91
|
+
continue;
|
|
92
|
+
if (review.id !== 'save')
|
|
93
|
+
return null;
|
|
94
|
+
// Preserve unrelated settings changed while the dialog was open.
|
|
95
|
+
const next = { ...store.loadConfig(), provider: draft.provider, model, apiKey, autonomyMode: mode.id };
|
|
96
|
+
const saved = next;
|
|
97
|
+
if (baseUrl)
|
|
98
|
+
saved.baseUrl = baseUrl;
|
|
99
|
+
else
|
|
100
|
+
delete saved.baseUrl;
|
|
101
|
+
store.saveConfig(saved);
|
|
102
|
+
return { provider: provider.id, model };
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/tui/setup.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Shared setup transaction for the home panel and standalone CLI. */
|
|
2
|
+
export interface SetupChoice { id: string; label: string; description?: string }
|
|
3
|
+
export interface SetupProvider { id: string; label: string; hint: string; model: string }
|
|
4
|
+
export interface SetupUI {
|
|
5
|
+
select(options: { title: string; items: SetupChoice[] }): Promise<SetupChoice | null>;
|
|
6
|
+
text(options: { title: string; lines: string[]; value: string; placeholder: string; mask: boolean }): Promise<string | null>;
|
|
7
|
+
}
|
|
8
|
+
type Config = Record<string, unknown>;
|
|
9
|
+
const string = (value: unknown): string => typeof value === 'string' ? value : '';
|
|
10
|
+
const customProviders = new Set(['openai-compatible', 'dashscope-byok', 'ollama']);
|
|
11
|
+
const defaults: Record<string, string> = {
|
|
12
|
+
ollama: 'http://localhost:11434/api/chat',
|
|
13
|
+
'dashscope-byok': 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function validateEndpoint(value: string): string | null {
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(value);
|
|
19
|
+
if (url.username || url.password || url.search || url.hash) return 'Use an endpoint without credentials, query parameters, or a fragment.';
|
|
20
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
21
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) return 'Use HTTPS, or HTTP on localhost for a local model.';
|
|
22
|
+
return null;
|
|
23
|
+
} catch { return 'Enter a complete endpoint URL.'; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runSetup(ui: SetupUI, providers: SetupProvider[], store: {
|
|
27
|
+
loadConfig(): Config; saveConfig(config: Config): void;
|
|
28
|
+
}, workspace = process.cwd()): Promise<{ provider: string; model: string } | null> {
|
|
29
|
+
const original = store.loadConfig();
|
|
30
|
+
let draft: Config = { ...original };
|
|
31
|
+
async function text(title: string, lines: string[], value = '', mask = false,
|
|
32
|
+
validate: (answer: string) => string | null = () => null): Promise<string | null> {
|
|
33
|
+
let error: string | null = null;
|
|
34
|
+
for (;;) {
|
|
35
|
+
const answer = await ui.text({ title, lines: error ? [...lines, error] : lines, value,
|
|
36
|
+
placeholder: mask ? 'paste key' : '', mask });
|
|
37
|
+
if (answer === null) return null;
|
|
38
|
+
const cleaned = answer.trim();
|
|
39
|
+
error = /[\x00-\x1f\x7f]/.test(cleaned) ? 'Use a single line without control characters.' : validate(cleaned);
|
|
40
|
+
if (!error) return cleaned;
|
|
41
|
+
value = mask ? '' : cleaned;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (;;) {
|
|
45
|
+
const selected = await ui.select({ title: 'Setup 1/5 · provider', items: providers.map(p => ({ id: p.id, label: p.label, description: p.hint })) });
|
|
46
|
+
if (!selected) return null;
|
|
47
|
+
const provider = providers.find(p => p.id === selected.id);
|
|
48
|
+
if (!provider) return null;
|
|
49
|
+
const sameProvider = draft.provider === provider.id;
|
|
50
|
+
let baseUrl = '';
|
|
51
|
+
if (customProviders.has(provider.id)) {
|
|
52
|
+
const answer = await text('Setup 2/5 · endpoint', ['Full chat endpoint URL, including /v1/chat/completions or /api/chat.',
|
|
53
|
+
'Credentials are sent to this endpoint when you use the model.'],
|
|
54
|
+
(sameProvider ? string(draft.baseUrl) : '') || defaults[provider.id] || '', false, validateEndpoint);
|
|
55
|
+
// No network request is made by setup.
|
|
56
|
+
if (answer === null) return null;
|
|
57
|
+
baseUrl = answer;
|
|
58
|
+
}
|
|
59
|
+
const model = await text('Setup 3/5 · model', ['Enter the model ID supported by your provider.'],
|
|
60
|
+
(sameProvider ? string(draft.model) : '') || provider.model, false, value => value ? null : 'A model ID is required.');
|
|
61
|
+
if (model === null) return null;
|
|
62
|
+
const unchangedEndpoint = sameProvider && (string(draft.baseUrl) || defaults[provider.id] || '') === baseUrl;
|
|
63
|
+
// Fixed endpoints have no editable URL. A changed endpoint never inherits a key.
|
|
64
|
+
const keepKey = sameProvider && (!customProviders.has(provider.id) || unchangedEndpoint);
|
|
65
|
+
let apiKey = '';
|
|
66
|
+
if (provider.id !== 'ollama') {
|
|
67
|
+
const oldKey = keepKey ? string(draft.apiKey) : '';
|
|
68
|
+
const answer = await text('Setup 3/5 · credentials', ['Stored encrypted locally; used to authenticate with your selected provider.',
|
|
69
|
+
oldKey ? 'Blank keeps the key for this same provider and endpoint.' : 'Enter a key for this provider.',
|
|
70
|
+
provider.id === 'openai-compatible' ? 'Blank is allowed for an endpoint that needs no authentication.' : ''], '', true,
|
|
71
|
+
value => value || oldKey || provider.id === 'openai-compatible' ? null : 'An API key is required.');
|
|
72
|
+
if (answer === null) return null;
|
|
73
|
+
apiKey = answer || oldKey;
|
|
74
|
+
}
|
|
75
|
+
const mode = await ui.select({ title: 'Setup 4/5 · autonomy', items: [
|
|
76
|
+
{ id: 'B', label: 'B · Supervised (Standard)', description: 'Recommended. Approval for dangerous actions; routine work continues.' },
|
|
77
|
+
{ id: 'A', label: 'A · Ask first (Strict)', description: 'Explicit confirmation before every tool call.' },
|
|
78
|
+
{ id: 'C', label: 'C · Autonomous (High Velocity)', description: 'Fewer interruptions; mandatory safety and decision gates remain.' }
|
|
79
|
+
] });
|
|
80
|
+
if (!mode || !['A', 'B', 'C'].includes(mode.id)) return null;
|
|
81
|
+
draft = { ...draft, provider: provider.id, model, apiKey, autonomyMode: mode.id };
|
|
82
|
+
if (baseUrl) draft.baseUrl = baseUrl; else delete draft.baseUrl;
|
|
83
|
+
const review = await ui.select({ title: 'Setup 5/5 · review', items: [
|
|
84
|
+
{ id: 'save', label: 'Save configuration', description: `${provider.label} · ${model} · Mode ${mode.id}` },
|
|
85
|
+
{ id: 'edit', label: 'Edit choices', description: baseUrl || 'Provider default endpoint' },
|
|
86
|
+
{ id: 'cancel', label: 'Cancel without saving', description: `Workspace: ${workspace}` }
|
|
87
|
+
] });
|
|
88
|
+
if (!review || review.id === 'cancel') return null;
|
|
89
|
+
if (review.id === 'edit') continue;
|
|
90
|
+
if (review.id !== 'save') return null;
|
|
91
|
+
// Preserve unrelated settings changed while the dialog was open.
|
|
92
|
+
const next = { ...store.loadConfig(), provider: draft.provider, model, apiKey, autonomyMode: mode.id };
|
|
93
|
+
const saved: Config = next;
|
|
94
|
+
if (baseUrl) saved.baseUrl = baseUrl; else delete saved.baseUrl;
|
|
95
|
+
store.saveConfig(saved);
|
|
96
|
+
return { provider: provider.id, model };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2011-2024 Thesys Inc.
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Adapted for Causyne UI Forge — Generative Component & Design Engine.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Generate semantic, modern HTML + CSS component code under Causyne UI Forge.
|
|
9
|
+
*/
|
|
10
|
+
export function generateHtmlComponent(spec) {
|
|
11
|
+
const isDark = spec.theme !== 'light';
|
|
12
|
+
const cardBg = isDark ? 'rgba(30, 41, 59, 0.75)' : 'rgba(255, 255, 255, 0.95)';
|
|
13
|
+
const border = isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.1)';
|
|
14
|
+
const text = isDark ? '#f8fafc' : '#0f172a';
|
|
15
|
+
const textMuted = isDark ? '#94a3b8' : '#64748b';
|
|
16
|
+
const accent = '#6366f1';
|
|
17
|
+
const accentHover = '#4f46e5';
|
|
18
|
+
const css = `
|
|
19
|
+
/* Causyne UI Forge Generated Styles - Responsive, Accessible & Performant */
|
|
20
|
+
.causyne-ui-root {
|
|
21
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
22
|
+
color: ${text};
|
|
23
|
+
line-height: 1.5;
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
}
|
|
26
|
+
.causyne-ui-root * { box-sizing: border-box; margin: 0; padding: 0; }
|
|
27
|
+
.causyne-ui-card {
|
|
28
|
+
background: ${cardBg};
|
|
29
|
+
backdrop-filter: blur(12px);
|
|
30
|
+
border: 1px solid ${border};
|
|
31
|
+
border-radius: 12px;
|
|
32
|
+
padding: 24px;
|
|
33
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
34
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
35
|
+
}
|
|
36
|
+
.causyne-ui-card:hover {
|
|
37
|
+
transform: translateY(-2px);
|
|
38
|
+
box-shadow: 0 20px 30px -10px rgba(0, 0, 0, 0.3);
|
|
39
|
+
}
|
|
40
|
+
.causyne-ui-header { margin-bottom: 16px; }
|
|
41
|
+
.causyne-ui-title { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.02em; }
|
|
42
|
+
.causyne-ui-desc { font-size: 0.875rem; color: ${textMuted}; margin-top: 4px; }
|
|
43
|
+
.causyne-ui-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin: 16px 0; }
|
|
44
|
+
.causyne-ui-stat-box {
|
|
45
|
+
background: rgba(255, 255, 255, 0.03);
|
|
46
|
+
border: 1px solid ${border};
|
|
47
|
+
border-radius: 8px;
|
|
48
|
+
padding: 16px;
|
|
49
|
+
}
|
|
50
|
+
.causyne-ui-stat-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: ${textMuted}; }
|
|
51
|
+
.causyne-ui-stat-value { font-size: 1.75rem; font-weight: 700; color: ${accent}; margin-top: 4px; }
|
|
52
|
+
.causyne-ui-btn {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
padding: 8px 16px;
|
|
57
|
+
border-radius: 6px;
|
|
58
|
+
font-size: 0.875rem;
|
|
59
|
+
font-weight: 500;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
border: 1px solid transparent;
|
|
62
|
+
transition: all 0.15s ease;
|
|
63
|
+
}
|
|
64
|
+
.causyne-ui-btn-primary { background: ${accent}; color: #ffffff; }
|
|
65
|
+
.causyne-ui-btn-primary:hover { background: ${accentHover}; }
|
|
66
|
+
.causyne-ui-btn-secondary { background: transparent; border-color: ${border}; color: ${text}; }
|
|
67
|
+
.causyne-ui-btn-secondary:hover { background: rgba(255, 255, 255, 0.05); }
|
|
68
|
+
.causyne-ui-actions { display: flex; gap: 8px; margin-top: 20px; justify-content: flex-end; }
|
|
69
|
+
`.trim();
|
|
70
|
+
let html = '';
|
|
71
|
+
const title = spec.title || 'Component';
|
|
72
|
+
const desc = spec.description || '';
|
|
73
|
+
if (spec.type === 'dashboard-stats' || (spec.items && spec.items.length > 0)) {
|
|
74
|
+
const itemsHtml = (spec.items || [])
|
|
75
|
+
.map((item) => `
|
|
76
|
+
<div class="causyne-ui-stat-box">
|
|
77
|
+
<div class="causyne-ui-stat-label">${escapeHtml(item.label)}</div>
|
|
78
|
+
<div class="causyne-ui-stat-value">${escapeHtml(String(item.value ?? '—'))}</div>
|
|
79
|
+
${item.description ? `<div class="causyne-ui-desc">${escapeHtml(item.description)}</div>` : ''}
|
|
80
|
+
</div>`)
|
|
81
|
+
.join('\n');
|
|
82
|
+
html = `
|
|
83
|
+
<div class="causyne-ui-root">
|
|
84
|
+
<div class="causyne-ui-card">
|
|
85
|
+
<div class="causyne-ui-header">
|
|
86
|
+
<h3 class="causyne-ui-title">${escapeHtml(title)}</h3>
|
|
87
|
+
${desc ? `<p class="causyne-ui-desc">${escapeHtml(desc)}</p>` : ''}
|
|
88
|
+
</div>
|
|
89
|
+
<div class="causyne-ui-grid">
|
|
90
|
+
${itemsHtml}
|
|
91
|
+
</div>
|
|
92
|
+
${renderActions(spec.actions)}
|
|
93
|
+
</div>
|
|
94
|
+
</div>`.trim();
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
html = `
|
|
98
|
+
<div class="causyne-ui-root">
|
|
99
|
+
<div class="causyne-ui-card">
|
|
100
|
+
<div class="causyne-ui-header">
|
|
101
|
+
<h3 class="causyne-ui-title">${escapeHtml(title)}</h3>
|
|
102
|
+
${desc ? `<p class="causyne-ui-desc">${escapeHtml(desc)}</p>` : ''}
|
|
103
|
+
</div>
|
|
104
|
+
${renderActions(spec.actions)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>`.trim();
|
|
107
|
+
}
|
|
108
|
+
return { html, css };
|
|
109
|
+
}
|
|
110
|
+
function renderActions(actions) {
|
|
111
|
+
if (!actions || actions.length === 0)
|
|
112
|
+
return '';
|
|
113
|
+
const btns = actions
|
|
114
|
+
.map((a) => `<button class="causyne-ui-btn ${a.primary ? 'causyne-ui-btn-primary' : 'causyne-ui-btn-secondary'}" data-action="${escapeHtml(a.action)}">${escapeHtml(a.label)}</button>`)
|
|
115
|
+
.join('\n ');
|
|
116
|
+
return `
|
|
117
|
+
<div class="causyne-ui-actions">
|
|
118
|
+
${btns}
|
|
119
|
+
</div>`;
|
|
120
|
+
}
|
|
121
|
+
function escapeHtml(str) {
|
|
122
|
+
return String(str || '')
|
|
123
|
+
.replace(/&/g, '&')
|
|
124
|
+
.replace(/</g, '<')
|
|
125
|
+
.replace(/>/g, '>')
|
|
126
|
+
.replace(/"/g, '"');
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Generate full self-contained preview document.
|
|
130
|
+
*/
|
|
131
|
+
export function generatePreviewHtml(html, css, theme = 'dark') {
|
|
132
|
+
const bg = theme === 'dark' ? '#0b0f19' : '#f1f5f9';
|
|
133
|
+
return `<!DOCTYPE html>
|
|
134
|
+
<html lang="en">
|
|
135
|
+
<head>
|
|
136
|
+
<meta charset="UTF-8">
|
|
137
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
138
|
+
<title>Causyne UI Forge Preview</title>
|
|
139
|
+
<style>
|
|
140
|
+
body {
|
|
141
|
+
margin: 0;
|
|
142
|
+
padding: 40px 20px;
|
|
143
|
+
min-height: 100vh;
|
|
144
|
+
background: ${bg};
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
box-sizing: border-box;
|
|
149
|
+
}
|
|
150
|
+
${css}
|
|
151
|
+
</style>
|
|
152
|
+
</head>
|
|
153
|
+
<body>
|
|
154
|
+
${html}
|
|
155
|
+
</body>
|
|
156
|
+
</html>`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Generate React + Tailwind component code under Causyne UI Forge.
|
|
160
|
+
*/
|
|
161
|
+
export function generateReactComponent(spec) {
|
|
162
|
+
const compName = (spec.name || 'CausyneCard').replace(/[^a-zA-Z0-9]/g, '');
|
|
163
|
+
const title = spec.title || 'Component';
|
|
164
|
+
const desc = spec.description || '';
|
|
165
|
+
return `import React from 'react';
|
|
166
|
+
|
|
167
|
+
export interface ${compName}Props {
|
|
168
|
+
title?: string;
|
|
169
|
+
description?: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const ${compName}: React.FC<${compName}Props> = ({
|
|
173
|
+
title = "${title}",
|
|
174
|
+
description = "${desc}"
|
|
175
|
+
}) => {
|
|
176
|
+
return (
|
|
177
|
+
<div className="rounded-xl border border-slate-700/60 bg-slate-900/80 p-6 backdrop-blur-md shadow-xl transition-all hover:shadow-2xl">
|
|
178
|
+
<div className="mb-4">
|
|
179
|
+
<h3 className="text-xl font-semibold tracking-tight text-white">{title}</h3>
|
|
180
|
+
{description && <p className="mt-1 text-sm text-slate-400">{description}</p>}
|
|
181
|
+
</div>
|
|
182
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 my-4">
|
|
183
|
+
${(spec.items || [])
|
|
184
|
+
.map((item) => `
|
|
185
|
+
<div key="${item.id || item.label}" className="rounded-lg border border-slate-800 bg-slate-800/40 p-4">
|
|
186
|
+
<div className="text-xs font-medium uppercase tracking-wider text-slate-400">${item.label}</div>
|
|
187
|
+
<div className="mt-1 text-2xl font-bold text-indigo-400">${item.value ?? '—'}</div>
|
|
188
|
+
${item.description ? `<p className="mt-1 text-xs text-slate-500">${item.description}</p>` : ''}
|
|
189
|
+
</div>`)
|
|
190
|
+
.join('')}
|
|
191
|
+
</div>
|
|
192
|
+
${(spec.actions && spec.actions.length > 0)
|
|
193
|
+
? `
|
|
194
|
+
<div className="mt-6 flex justify-end gap-3">
|
|
195
|
+
${spec.actions
|
|
196
|
+
.map((a) => `
|
|
197
|
+
<button className="px-4 py-2 text-sm font-medium rounded-lg transition-colors ${a.primary
|
|
198
|
+
? 'bg-indigo-600 text-white hover:bg-indigo-500'
|
|
199
|
+
: 'border border-slate-700 text-slate-300 hover:bg-slate-800'}">
|
|
200
|
+
${a.label}
|
|
201
|
+
</button>`)
|
|
202
|
+
.join('')}
|
|
203
|
+
</div>`
|
|
204
|
+
: ''}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export default ${compName};
|
|
210
|
+
`;
|
|
211
|
+
}
|