agentful 0.3.9 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +283 -20
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -3040,11 +3040,11 @@ function banner() {
|
|
|
3040
3040
|
paint.dim(brand.tagline)
|
|
3041
3041
|
].join("\n");
|
|
3042
3042
|
}
|
|
3043
|
-
var VERSION, brand, useColor, wrap, paint, sym, ui;
|
|
3043
|
+
var VERSION, brand, useColor, wrap, paint, tuiDesign, sym, ui;
|
|
3044
3044
|
var init_branding = __esm({
|
|
3045
3045
|
"src/branding.ts"() {
|
|
3046
3046
|
"use strict";
|
|
3047
|
-
VERSION = "0.
|
|
3047
|
+
VERSION = "0.4.1" ? "0.4.1" : null.version;
|
|
3048
3048
|
brand = {
|
|
3049
3049
|
name: "agentful",
|
|
3050
3050
|
// the command users type
|
|
@@ -3067,6 +3067,12 @@ var init_branding = __esm({
|
|
|
3067
3067
|
dim: wrap("2"),
|
|
3068
3068
|
bold: wrap("1")
|
|
3069
3069
|
};
|
|
3070
|
+
tuiDesign = {
|
|
3071
|
+
/** Sits in front of the input line. U+276F, the shell prompt sigil. */
|
|
3072
|
+
promptMark: "\u276F",
|
|
3073
|
+
/** Marks an item in progress in the sidebar's task list. U+203A. */
|
|
3074
|
+
activeMark: "\u203A"
|
|
3075
|
+
};
|
|
3070
3076
|
sym = {
|
|
3071
3077
|
ok: paint.success("\u2714"),
|
|
3072
3078
|
fail: paint.error("\u2716"),
|
|
@@ -3504,40 +3510,282 @@ function brandingPluginSource() {
|
|
|
3504
3510
|
const logoLiteral = JSON.stringify(logo_default.replace(/\n+$/, "").split("\n"));
|
|
3505
3511
|
return `// Generated by ${brand.name} v${VERSION} \u2014 do not edit; rewritten on every start.
|
|
3506
3512
|
const LOGO = ${logoLiteral}
|
|
3513
|
+
const MARK = ${JSON.stringify(tuiDesign.promptMark)}
|
|
3514
|
+
const ACTIVE = ${JSON.stringify(tuiDesign.activeMark)}
|
|
3515
|
+
const USD = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
|
|
3516
|
+
|
|
3517
|
+
// Ours, not the engine's generic examples ("Fix a TODO in the codebase").
|
|
3518
|
+
const PLACEHOLDERS = {
|
|
3519
|
+
normal: [
|
|
3520
|
+
'Build a landing page for my newsletter',
|
|
3521
|
+
'Add a contact form and push it live',
|
|
3522
|
+
'What changed since my last push?',
|
|
3523
|
+
],
|
|
3524
|
+
shell: ['agentful push', 'agentful open', 'git status'],
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
/** Engine state reads are best-effort: a renamed field must not blank the UI. */
|
|
3528
|
+
function safe(fn, fallback) {
|
|
3529
|
+
try {
|
|
3530
|
+
const value = fn()
|
|
3531
|
+
return value === undefined || value === null ? fallback : value
|
|
3532
|
+
} catch (err) {
|
|
3533
|
+
return fallback
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
/** Keep the tail of a path \u2014 the file name matters more than its parent. */
|
|
3538
|
+
function tail(text, max) {
|
|
3539
|
+
const s = String(text)
|
|
3540
|
+
return s.length <= max ? s : '\u2026' + s.slice(s.length - (max - 1))
|
|
3541
|
+
}
|
|
3507
3542
|
|
|
3508
3543
|
const plugin = {
|
|
3509
3544
|
id: 'agentful-branding',
|
|
3510
3545
|
tui(api) {
|
|
3546
|
+
const Prompt = api.ui.Prompt
|
|
3547
|
+
const HostSlot = api.ui.Slot
|
|
3548
|
+
// The slot context carries no colors; the theme lives on the plugin api.
|
|
3549
|
+
const t = () => api.theme.current
|
|
3550
|
+
|
|
3551
|
+
// \u2500\u2500 band pieces \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3552
|
+
// Sidebar dividers only. The prompt has no rule of its own: the engine
|
|
3553
|
+
// already draws one under it, and a second line above it made the input
|
|
3554
|
+
// look boxed in.
|
|
3555
|
+
function Rule() {
|
|
3556
|
+
return <box height={1} flexShrink={0} border={['top']} borderColor={t().borderSubtle} />
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
function Row(props) {
|
|
3560
|
+
return (
|
|
3561
|
+
<box flexDirection="row" justifyContent="space-between" gap={1}>
|
|
3562
|
+
<text fg={t().textMuted} wrapMode="none">{props.label}</text>
|
|
3563
|
+
<text fg={props.fg || t().text} flexShrink={0} wrapMode="none">{props.value}</text>
|
|
3564
|
+
</box>
|
|
3565
|
+
)
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
function Section(props) {
|
|
3569
|
+
return (
|
|
3570
|
+
<box flexDirection="column" flexShrink={0}>
|
|
3571
|
+
<Rule />
|
|
3572
|
+
<text fg={t().textMuted}>{props.label}</text>
|
|
3573
|
+
{props.children}
|
|
3574
|
+
</box>
|
|
3575
|
+
)
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
// \u2500\u2500 sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3579
|
+
function Sidebar(props) {
|
|
3580
|
+
const messages = () => safe(() => api.state.session.messages(props.sessionID), [])
|
|
3581
|
+
const usage = () => safe(() => {
|
|
3582
|
+
const last = messages().findLast((m) => m.role === 'assistant' && m.tokens && m.tokens.output > 0)
|
|
3583
|
+
if (!last) return { tokens: 0, percent: null }
|
|
3584
|
+
const tk = last.tokens
|
|
3585
|
+
const total = tk.input + tk.output + tk.reasoning + tk.cache.read + tk.cache.write
|
|
3586
|
+
const provider = api.state.provider.find((p) => p.id === last.providerID)
|
|
3587
|
+
const model = provider && provider.models ? provider.models[last.modelID] : undefined
|
|
3588
|
+
const limit = model && model.limit ? model.limit.context : 0
|
|
3589
|
+
return { tokens: total, percent: limit ? Math.round((total / limit) * 100) : null }
|
|
3590
|
+
}, { tokens: 0, percent: null })
|
|
3591
|
+
// Only shown once it is real: platform models bill credits server-side
|
|
3592
|
+
// and report 0 here, and a permanent "$0.00" reads like a promise.
|
|
3593
|
+
const cost = () => safe(() => api.state.session.get(props.sessionID).cost, 0)
|
|
3594
|
+
const lsp = () => safe(() => api.state.lsp(), [])
|
|
3595
|
+
const lspDisabled = () => safe(() => !api.state.config.lsp, true)
|
|
3596
|
+
const mcp = () => safe(() => api.state.mcp(), [])
|
|
3597
|
+
const todos = () => safe(() => api.state.session.todo(props.sessionID), [])
|
|
3598
|
+
const openTodos = () => todos().filter((item) => item.status !== 'completed')
|
|
3599
|
+
const files = () => safe(() => api.state.session.diff(props.sessionID), [])
|
|
3600
|
+
|
|
3601
|
+
return (
|
|
3602
|
+
<box flexDirection="column" gap={1} flexShrink={0}>
|
|
3603
|
+
<Section label="CONTEXT">
|
|
3604
|
+
<Row label="tokens" value={() => usage().tokens.toLocaleString('en-US')} />
|
|
3605
|
+
{() => usage().percent === null
|
|
3606
|
+
? null
|
|
3607
|
+
: <Row label="window" value={() => usage().percent + '%'} />}
|
|
3608
|
+
{() => cost() > 0
|
|
3609
|
+
? <Row label="cost" value={() => USD.format(cost())} />
|
|
3610
|
+
: null}
|
|
3611
|
+
</Section>
|
|
3612
|
+
|
|
3613
|
+
<Section label="TOOLS">
|
|
3614
|
+
<Row
|
|
3615
|
+
label="language server"
|
|
3616
|
+
value={() => lsp().length
|
|
3617
|
+
? lsp().filter((s) => s.status === 'connected').length + '/' + lsp().length
|
|
3618
|
+
: (lspDisabled() ? 'off' : 'idle')}
|
|
3619
|
+
/>
|
|
3620
|
+
{() => mcp().length
|
|
3621
|
+
? <Row
|
|
3622
|
+
label="mcp"
|
|
3623
|
+
value={() => mcp().filter((s) => s.status === 'connected').length + '/' + mcp().length}
|
|
3624
|
+
/>
|
|
3625
|
+
: null}
|
|
3626
|
+
</Section>
|
|
3627
|
+
|
|
3628
|
+
{() => openTodos().length === 0 ? null : (
|
|
3629
|
+
<Section label="TASKS">
|
|
3630
|
+
{() => todos().slice(0, 8).map((item) => (
|
|
3631
|
+
<box flexDirection="row" gap={1}>
|
|
3632
|
+
<text
|
|
3633
|
+
flexShrink={0}
|
|
3634
|
+
fg={item.status === 'completed'
|
|
3635
|
+
? t().success
|
|
3636
|
+
: item.status === 'in_progress' ? t().primary : t().textMuted}
|
|
3637
|
+
>
|
|
3638
|
+
{item.status === 'completed' ? '\u2713' : item.status === 'in_progress' ? ACTIVE : '\xB7'}
|
|
3639
|
+
</text>
|
|
3640
|
+
<text fg={item.status === 'completed' ? t().textMuted : t().text}>{item.content}</text>
|
|
3641
|
+
</box>
|
|
3642
|
+
))}
|
|
3643
|
+
{() => todos().length > 8
|
|
3644
|
+
? <text fg={t().textMuted}>{'+' + (todos().length - 8) + ' more'}</text>
|
|
3645
|
+
: null}
|
|
3646
|
+
</Section>
|
|
3647
|
+
)}
|
|
3648
|
+
|
|
3649
|
+
{() => files().length === 0 ? null : (
|
|
3650
|
+
<Section label="CHANGES">
|
|
3651
|
+
{() => files().slice(0, 8).map((file) => (
|
|
3652
|
+
<box flexDirection="row" gap={1} justifyContent="space-between">
|
|
3653
|
+
<text fg={t().text} wrapMode="none">{tail(file.file, 24)}</text>
|
|
3654
|
+
<box flexDirection="row" gap={1} flexShrink={0}>
|
|
3655
|
+
{file.additions ? <text fg={t().diffAdded}>{'+' + file.additions}</text> : null}
|
|
3656
|
+
{file.deletions ? <text fg={t().diffRemoved}>{'-' + file.deletions}</text> : null}
|
|
3657
|
+
</box>
|
|
3658
|
+
</box>
|
|
3659
|
+
))}
|
|
3660
|
+
{() => files().length > 8
|
|
3661
|
+
? <text fg={t().textMuted}>{'+' + (files().length - 8) + ' more'}</text>
|
|
3662
|
+
: null}
|
|
3663
|
+
</Section>
|
|
3664
|
+
)}
|
|
3665
|
+
</box>
|
|
3666
|
+
)
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// \u2500\u2500 prompt \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3670
|
+
/** Left half of the line under the input; replaces the engine's plain cwd. */
|
|
3671
|
+
function Hint(props) {
|
|
3672
|
+
const directory = () => safe(
|
|
3673
|
+
() => api.state.session.get(props.sessionID).directory,
|
|
3674
|
+
safe(() => api.state.path.directory, ''),
|
|
3675
|
+
)
|
|
3676
|
+
const branch = () => safe(() => api.state.vcs.branch, '')
|
|
3677
|
+
return (
|
|
3678
|
+
<box flexDirection="row" gap={1} marginLeft={1}>
|
|
3679
|
+
<text fg={t().textMuted} wrapMode="none">{() => directory()}</text>
|
|
3680
|
+
{() => branch() ? <text fg={t().textMuted} wrapMode="none">{'\xB7 ' + branch()}</text> : null}
|
|
3681
|
+
</box>
|
|
3682
|
+
)
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
/**
|
|
3686
|
+
* Marker plus the engine's own input, with the engine's chrome taken off.
|
|
3687
|
+
*
|
|
3688
|
+
* The engine wraps its input in a box with border ["left"] \u2014 the
|
|
3689
|
+
* agent-coloured rail. There is no prop, config key or theme token that
|
|
3690
|
+
* hides it (checked against v1.18.18 and against main), so we clip it
|
|
3691
|
+
* instead: the wrapper hides its overflow and the prompt hangs one column
|
|
3692
|
+
* to the left, which puts the rail outside the visible area. Everything
|
|
3693
|
+
* else keeps working because it is only a shift.
|
|
3694
|
+
*
|
|
3695
|
+
* \u26A0\uFE0F One column is the limit. The status line under the input (our hint
|
|
3696
|
+
* on the left) sits OUTSIDE that padded box and carries a margin of just
|
|
3697
|
+
* 1, so a wider clip eats the start of the working directory \u2014
|
|
3698
|
+
* "/Users/\u2026" came out as "sers/\u2026" at -3. The two columns left between
|
|
3699
|
+
* the marker and the text are the engine's own padding; taking those
|
|
3700
|
+
* would cost the status line.
|
|
3701
|
+
*/
|
|
3702
|
+
function Band(props) {
|
|
3703
|
+
return (
|
|
3704
|
+
<box flexDirection="row" width="100%">
|
|
3705
|
+
<box flexShrink={0} paddingTop={1}>
|
|
3706
|
+
<text fg={t().primary}>{MARK}</text>
|
|
3707
|
+
</box>
|
|
3708
|
+
<box flexGrow={1} overflow="hidden">
|
|
3709
|
+
<box marginLeft={-1} width="100%">{props.children}</box>
|
|
3710
|
+
</box>
|
|
3711
|
+
</box>
|
|
3712
|
+
)
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3511
3715
|
api.slots.register({
|
|
3512
3716
|
order: 1,
|
|
3513
3717
|
slots: {
|
|
3514
|
-
home_logo(
|
|
3515
|
-
const accent = ctx && ctx.theme ? ctx.theme.primary : undefined
|
|
3516
|
-
const muted = ctx && ctx.theme ? ctx.theme.textMuted : undefined
|
|
3718
|
+
home_logo() {
|
|
3517
3719
|
return (
|
|
3518
3720
|
<box flexDirection="column">
|
|
3519
3721
|
{LOGO.map((line) => (
|
|
3520
|
-
<text fg={
|
|
3722
|
+
<text fg={t().primary}>{line}</text>
|
|
3521
3723
|
))}
|
|
3522
3724
|
<text> </text>
|
|
3523
|
-
<text fg={
|
|
3725
|
+
<text fg={t().textMuted}>${brand.tagline}</text>
|
|
3726
|
+
</box>
|
|
3727
|
+
)
|
|
3728
|
+
},
|
|
3729
|
+
|
|
3730
|
+
home_footer() {
|
|
3731
|
+
return <text fg={t().textMuted}>${brand.name} v${VERSION}</text>
|
|
3732
|
+
},
|
|
3733
|
+
|
|
3734
|
+
home_prompt(ctx, data) {
|
|
3735
|
+
return (
|
|
3736
|
+
<Band>
|
|
3737
|
+
<Prompt
|
|
3738
|
+
ref={data.ref}
|
|
3739
|
+
placeholders={PLACEHOLDERS}
|
|
3740
|
+
right={<HostSlot name="home_prompt_right" />}
|
|
3741
|
+
/>
|
|
3742
|
+
</Band>
|
|
3743
|
+
)
|
|
3744
|
+
},
|
|
3745
|
+
|
|
3746
|
+
session_prompt(ctx, data) {
|
|
3747
|
+
return (
|
|
3748
|
+
<Band>
|
|
3749
|
+
<Prompt
|
|
3750
|
+
sessionID={data.session_id}
|
|
3751
|
+
visible={data.visible}
|
|
3752
|
+
disabled={data.disabled}
|
|
3753
|
+
onSubmit={data.on_submit}
|
|
3754
|
+
ref={data.ref}
|
|
3755
|
+
placeholders={PLACEHOLDERS}
|
|
3756
|
+
hint={<Hint sessionID={data.session_id} />}
|
|
3757
|
+
right={<HostSlot name="session_prompt_right" session_id={data.session_id} />}
|
|
3758
|
+
/>
|
|
3759
|
+
</Band>
|
|
3760
|
+
)
|
|
3761
|
+
},
|
|
3762
|
+
|
|
3763
|
+
sidebar_title(ctx, data) {
|
|
3764
|
+
return (
|
|
3765
|
+
<box flexDirection="column">
|
|
3766
|
+
<text fg={t().text}>{data.title}</text>
|
|
3767
|
+
{() => data.share_url
|
|
3768
|
+
? <text fg={t().accent} wrapMode="none">{data.share_url}</text>
|
|
3769
|
+
: null}
|
|
3524
3770
|
</box>
|
|
3525
3771
|
)
|
|
3526
3772
|
},
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
return <
|
|
3773
|
+
|
|
3774
|
+
sidebar_content(ctx, data) {
|
|
3775
|
+
return <Sidebar sessionID={data.session_id} />
|
|
3530
3776
|
},
|
|
3531
|
-
|
|
3532
|
-
//
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
const muted = ctx && ctx.theme ? ctx.theme.textMuted : undefined
|
|
3777
|
+
|
|
3778
|
+
// Bottom of the sidebar \u2014 where the engine prints its own name and
|
|
3779
|
+
// version (for example "OpenCode 1.18.18"); ours replaces it.
|
|
3780
|
+
sidebar_footer() {
|
|
3536
3781
|
return (
|
|
3537
|
-
<
|
|
3538
|
-
<
|
|
3539
|
-
<
|
|
3540
|
-
|
|
3782
|
+
<box flexDirection="column">
|
|
3783
|
+
<Rule />
|
|
3784
|
+
<text>
|
|
3785
|
+
<span fg={t().primary}>${brand.name}</span>
|
|
3786
|
+
<span fg={t().textMuted}> v${VERSION}</span>
|
|
3787
|
+
</text>
|
|
3788
|
+
</box>
|
|
3541
3789
|
)
|
|
3542
3790
|
},
|
|
3543
3791
|
},
|
|
@@ -6883,6 +7131,13 @@ ${backendSessionLines(backend)}
|
|
|
6883
7131
|
|
|
6884
7132
|
// src/lib/engineConfig.ts
|
|
6885
7133
|
var PROVIDER_ID = "agentful";
|
|
7134
|
+
var DISABLED_ENGINE_PLUGINS = {
|
|
7135
|
+
"internal:sidebar-context": false,
|
|
7136
|
+
"internal:sidebar-lsp": false,
|
|
7137
|
+
"internal:sidebar-mcp": false,
|
|
7138
|
+
"internal:sidebar-todo": false,
|
|
7139
|
+
"internal:sidebar-files": false
|
|
7140
|
+
};
|
|
6886
7141
|
var CLOUD_AGENT = "agentful-build";
|
|
6887
7142
|
var LOCAL_AGENT = "local-only";
|
|
6888
7143
|
var CLOUD_AGENT_PROMPT = `You are the Agentful Cloud build agent: everything you build in this directory targets deployment on Agentful Cloud via \`agentful push\`. Follow the platform contract in AGENTFUL_CLOUD.md strictly \u2014 static files only, no server process, no build-time env vars or secrets. When a push or cloud build fails, diagnose ONLY from the \`agentful push\` output and \`agentful build-status\`; quote the record and never invent causes or settings it does not show. If the user wants development that is not meant for the platform, suggest the ${LOCAL_AGENT} agent (Tab).`;
|
|
@@ -7012,7 +7267,15 @@ async function writeEngineSession(session, catalog, localProviders = {}, framewo
|
|
|
7012
7267
|
(0, import_node_fs12.writeFileSync)(pluginPath, brandingPluginSource2());
|
|
7013
7268
|
(0, import_node_fs12.writeFileSync)((0, import_node_path14.join)(configDir2, "tui.json"), JSON.stringify({
|
|
7014
7269
|
theme: "agentful",
|
|
7015
|
-
plugin: [`file://${pluginPath}`]
|
|
7270
|
+
plugin: [`file://${pluginPath}`],
|
|
7271
|
+
// The engine's own sidebar sections are plugins with fixed ids, and
|
|
7272
|
+
// sidebar_content composes rather than replaces — without switching them
|
|
7273
|
+
// off, ours would render UNDER theirs and the column would show every
|
|
7274
|
+
// number twice. `plugin_enabled` is keyed by plugin id and applies to the
|
|
7275
|
+
// built-in ones too (verified in the v1.18.18 binary); a user's own toggle
|
|
7276
|
+
// in the plugin manager still wins, because the engine merges its kv
|
|
7277
|
+
// state over this map.
|
|
7278
|
+
plugin_enabled: DISABLED_ENGINE_PLUGINS
|
|
7016
7279
|
}, null, 2));
|
|
7017
7280
|
(0, import_node_fs12.writeFileSync)((0, import_node_path14.join)(themesDir, "agentful.json"), JSON.stringify(await resolveTheme(), null, 2));
|
|
7018
7281
|
for (const name of Object.keys(SLASH_COMMANDS)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentful",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Agentful in your terminal — local development with push-to-cloud previews",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://agentful.dev",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"sync-starters": "node scripts/sync-starters.mjs",
|
|
24
24
|
"sync-skills": "node scripts/sync-skills.mjs",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
|
+
"check-engine": "node scripts/check-engine-contract.mjs",
|
|
26
27
|
"dev": "tsx src/index.ts",
|
|
27
28
|
"sync-theme": "cp src/branding/agentful-theme.json ../frontend/public/cli/theme.json",
|
|
28
29
|
"test": "node --import tsx --test src/lib/*.test.ts"
|