@epoch-ai/cli 2.2.4 → 2.2.6
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/package.json +2 -2
- package/script/publish.ts +5 -7
- package/src/cli/cmd/tui/component/prompt/index.tsx +14 -2
- package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +30 -11
- package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +2 -2
- package/src/cli/cmd/tui/routes/home.tsx +11 -1
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +2 -2
- package/src/project/bootstrap.ts +6 -0
- package/src/project/init-files.ts +328 -0
- package/src/session/overflow.ts +0 -1
- /package/bin/{epochcli → epochcli.cjs} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.6",
|
|
4
4
|
"name": "@epoch-ai/cli",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"db": "bun drizzle-kit"
|
|
24
24
|
},
|
|
25
25
|
"bin": {
|
|
26
|
-
"epochcli": "bin/epochcli"
|
|
26
|
+
"epochcli": "bin/epochcli.cjs"
|
|
27
27
|
},
|
|
28
28
|
"randomField": "this-is-a-random-value-12345",
|
|
29
29
|
"exports": {
|
package/script/publish.ts
CHANGED
|
@@ -27,7 +27,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
|
|
27
27
|
{
|
|
28
28
|
name: pkg.name,
|
|
29
29
|
bin: {
|
|
30
|
-
epochcli: `./bin/epochcli`,
|
|
30
|
+
epochcli: `./bin/epochcli.cjs`,
|
|
31
31
|
},
|
|
32
32
|
scripts: {
|
|
33
33
|
postinstall: "node ./postinstall.mjs",
|
|
@@ -41,16 +41,14 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
|
|
41
41
|
),
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
const
|
|
44
|
+
for (const [name] of Object.entries(binaries)) {
|
|
45
45
|
const pkgDir = `./dist/${name}`
|
|
46
46
|
if (process.platform !== "win32") {
|
|
47
47
|
await $`chmod -R 755 .`.cwd(pkgDir)
|
|
48
48
|
}
|
|
49
|
-
await $`
|
|
50
|
-
|
|
51
|
-
})
|
|
52
|
-
await Promise.all(tasks)
|
|
53
|
-
await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag latest`.nothrow()
|
|
49
|
+
await $`npm publish --access public --tag latest`.cwd(pkgDir).nothrow()
|
|
50
|
+
}
|
|
51
|
+
await $`cd ./dist/${pkg.name} && npm publish --access public --tag latest`.nothrow()
|
|
54
52
|
|
|
55
53
|
// const image = "ghcr.io/benjamesmurray/epoch-cli"
|
|
56
54
|
// const platforms = "linux/amd64,linux/arm64"
|
|
@@ -1219,7 +1219,19 @@ export function Prompt(props: PromptProps) {
|
|
|
1219
1219
|
/>
|
|
1220
1220
|
</box>
|
|
1221
1221
|
<box flexDirection="row" justifyContent="space-between">
|
|
1222
|
-
<Show
|
|
1222
|
+
<Show
|
|
1223
|
+
when={status().type !== "idle"}
|
|
1224
|
+
fallback={
|
|
1225
|
+
<box flexDirection="row" gap={2}>
|
|
1226
|
+
{props.hint ?? <text />}
|
|
1227
|
+
<Show when={store.mode === "normal"}>
|
|
1228
|
+
<text fg={theme.text}>
|
|
1229
|
+
{keybind.print("yolo_toggle")} <span style={{ fg: theme.textMuted }}>mode</span>
|
|
1230
|
+
</text>
|
|
1231
|
+
</Show>
|
|
1232
|
+
</box>
|
|
1233
|
+
}
|
|
1234
|
+
>
|
|
1223
1235
|
<box
|
|
1224
1236
|
flexDirection="row"
|
|
1225
1237
|
gap={1}
|
|
@@ -1307,7 +1319,7 @@ export function Prompt(props: PromptProps) {
|
|
|
1307
1319
|
<Match when={usage()}>
|
|
1308
1320
|
{(item) => (
|
|
1309
1321
|
<text fg={theme.textMuted} wrapMode="none">
|
|
1310
|
-
{
|
|
1322
|
+
{item().context}
|
|
1311
1323
|
</text>
|
|
1312
1324
|
)}
|
|
1313
1325
|
</Match>
|
|
@@ -1,34 +1,46 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@epoch-ai/sdk/v2"
|
|
2
2
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@epoch-ai/plugin/tui"
|
|
3
|
-
import { createMemo } from "solid-js"
|
|
3
|
+
import { createMemo, Show } from "solid-js"
|
|
4
4
|
|
|
5
5
|
const id = "internal:sidebar-context"
|
|
6
6
|
|
|
7
|
-
const money = new Intl.NumberFormat("en-US", {
|
|
8
|
-
style: "currency",
|
|
9
|
-
currency: "USD",
|
|
10
|
-
})
|
|
11
|
-
|
|
12
7
|
function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
13
8
|
const theme = () => props.api.theme.current
|
|
14
9
|
const msg = createMemo(() => props.api.state.session.messages(props.session_id))
|
|
15
|
-
const cost = createMemo(() => msg().reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0))
|
|
16
10
|
|
|
17
11
|
const state = createMemo(() => {
|
|
18
12
|
const last = msg().findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
|
|
19
13
|
if (!last) {
|
|
20
14
|
return {
|
|
21
15
|
tokens: 0,
|
|
22
|
-
|
|
16
|
+
usable: null,
|
|
17
|
+
limit: null,
|
|
18
|
+
margin: null,
|
|
23
19
|
}
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
const tokens =
|
|
27
23
|
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
|
28
24
|
const model = props.api.state.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
|
|
25
|
+
|
|
26
|
+
let limit = null
|
|
27
|
+
let usable = null
|
|
28
|
+
let margin = null
|
|
29
|
+
|
|
30
|
+
if (model) {
|
|
31
|
+
limit = model.limit.context
|
|
32
|
+
if (limit > 0) {
|
|
33
|
+
const maxOutput = Math.min(model.limit.output || 32000, 32000)
|
|
34
|
+
margin = Math.min(1024, Math.max(500, maxOutput))
|
|
35
|
+
usable = model.limit.input ? model.limit.input : limit - margin
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
return {
|
|
30
40
|
tokens,
|
|
31
|
-
|
|
41
|
+
usable,
|
|
42
|
+
limit,
|
|
43
|
+
margin,
|
|
32
44
|
}
|
|
33
45
|
})
|
|
34
46
|
|
|
@@ -38,8 +50,15 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
|
38
50
|
<b>Context</b>
|
|
39
51
|
</text>
|
|
40
52
|
<text fg={theme().textMuted}>{state().tokens.toLocaleString()} tokens</text>
|
|
41
|
-
<
|
|
42
|
-
|
|
53
|
+
<Show when={state().usable !== null}>
|
|
54
|
+
<text fg={theme().textMuted}>{state().usable!.toLocaleString()} usable</text>
|
|
55
|
+
</Show>
|
|
56
|
+
<Show when={state().limit !== null}>
|
|
57
|
+
<text fg={theme().textMuted}>{state().limit!.toLocaleString()} limit</text>
|
|
58
|
+
</Show>
|
|
59
|
+
<Show when={state().margin !== null}>
|
|
60
|
+
<text fg={theme().textMuted}>{state().margin!.toLocaleString()} margin</text>
|
|
61
|
+
</Show>
|
|
43
62
|
</box>
|
|
44
63
|
)
|
|
45
64
|
}
|
|
@@ -64,9 +64,9 @@ function View(props: { api: TuiPluginApi }) {
|
|
|
64
64
|
<span style={{ fg: theme().text }}>{path().name}</span>
|
|
65
65
|
</text>
|
|
66
66
|
<text fg={theme().textMuted}>
|
|
67
|
-
<span style={{ fg: theme().success }}>•</span> <b>
|
|
67
|
+
<span style={{ fg: theme().success }}>•</span> <b>Epoch</b>
|
|
68
68
|
<span style={{ fg: theme().text }}>
|
|
69
|
-
<b>
|
|
69
|
+
<b> CLI</b>
|
|
70
70
|
</span>{" "}
|
|
71
71
|
<span>{props.api.app.version}</span>
|
|
72
72
|
</text>
|
|
@@ -8,11 +8,21 @@ import { useRouteData } from "@tui/context/route"
|
|
|
8
8
|
import { usePromptRef } from "../context/prompt"
|
|
9
9
|
import { useLocal } from "../context/local"
|
|
10
10
|
import { TuiPluginRuntime } from "../plugin"
|
|
11
|
+
import { execSync } from "child_process"
|
|
11
12
|
|
|
12
13
|
// TODO: what is the best way to do this?
|
|
13
14
|
let once = false
|
|
15
|
+
|
|
16
|
+
let userName = "there"
|
|
17
|
+
try {
|
|
18
|
+
const output = execSync("git config --global user.name", { stdio: "pipe" }).toString().trim()
|
|
19
|
+
if (output) userName = output
|
|
20
|
+
} catch {
|
|
21
|
+
// Ignore errors (e.g., git not found, config not set)
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
const placeholder = {
|
|
15
|
-
normal: [
|
|
25
|
+
normal: [`Hi ${userName}, let's go!`],
|
|
16
26
|
shell: ["ls -la", "git status", "pwd"],
|
|
17
27
|
}
|
|
18
28
|
|
|
@@ -56,9 +56,9 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
|
|
56
56
|
<box flexShrink={0} gap={1} paddingTop={1}>
|
|
57
57
|
<TuiPluginRuntime.Slot name="sidebar_footer" mode="single_winner" session_id={props.sessionID}>
|
|
58
58
|
<text fg={theme.textMuted}>
|
|
59
|
-
<span style={{ fg: theme.success }}>•</span> <b>
|
|
59
|
+
<span style={{ fg: theme.success }}>•</span> <b>Epoch</b>
|
|
60
60
|
<span style={{ fg: theme.text }}>
|
|
61
|
-
<b>
|
|
61
|
+
<b> CLI</b>
|
|
62
62
|
</span>{" "}
|
|
63
63
|
<span>{Installation.VERSION}</span>
|
|
64
64
|
</text>
|
package/src/project/bootstrap.ts
CHANGED
|
@@ -10,9 +10,15 @@ import { Bus } from "../bus"
|
|
|
10
10
|
import { Command } from "../command"
|
|
11
11
|
import { Instance } from "./instance"
|
|
12
12
|
import { Log } from "@/util/log"
|
|
13
|
+
import { initProjectFiles } from "./init-files"
|
|
13
14
|
|
|
14
15
|
export async function InstanceBootstrap() {
|
|
15
16
|
Log.Default.info("bootstrapping", { directory: Instance.directory })
|
|
17
|
+
|
|
18
|
+
if (Instance.project.vcs === "git" && Instance.worktree !== "/") {
|
|
19
|
+
await initProjectFiles(Instance.directory, Instance.worktree)
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
await Plugin.init()
|
|
17
23
|
Format.init()
|
|
18
24
|
await LSP.init()
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { zodToJsonSchema } from "zod-to-json-schema"
|
|
4
|
+
import { Config } from "../config/config"
|
|
5
|
+
import { Log } from "../util/log"
|
|
6
|
+
|
|
7
|
+
const log = Log.create({ service: "project-init" })
|
|
8
|
+
|
|
9
|
+
const AGENTS_MD_CONTENT = `# Epoch CLI Agent Guidelines
|
|
10
|
+
|
|
11
|
+
## 1. Mandatory Workflows
|
|
12
|
+
- **\`mcpx\` (Unified MCP Interface)**: Use for ALL MCP interactions (spec, map, ground, github).
|
|
13
|
+
- **Adding Servers**: When asked to install or configure a new MCP server, you MUST follow the SOP defined in \`docs/MCP_config_guide.md\`.
|
|
14
|
+
- **Syntax**: Pass arguments via standard flags, \`key=value\` strings, or structured JSON.
|
|
15
|
+
- **Self-Discovery**: Use \`tool="--help"\` or \`args=["--help"]\` to inspect servers/tools.
|
|
16
|
+
- **\`spec\` (State Management)**: Maintain project state via \`mcpx\` server="spec".
|
|
17
|
+
- **Linear Progression**: \`sc_init\` -> \`write Specification.md\` -> \`sc_approve\` -> \`write Tasks.json\` -> \`sc_approve\` -> \`Build\`.
|
|
18
|
+
- **Arguments**: \`sc_init\` MANDATES the \`name\` parameter (e.g., \`flags: {"name": "my-feature"}\`). \`sc_plan\` and \`sc_approve\` take NO arguments. \`sc_todo_*\` requires \`--id\`.
|
|
19
|
+
- **\`map\` (Architectural Discovery)**: Use \`mcpx\` server="map" for navigation. Prefer \`pm_status\` and \`pm_query\` over manual \`ls\` or \`glob\`.
|
|
20
|
+
|
|
21
|
+
## 2. Enforcement
|
|
22
|
+
- **Drafting Wall**: All template tags in \`Specification.md\` must be cleared and \`template_tags_present\` set to \`false\` before implementation.
|
|
23
|
+
- **Orientation**: Trust the provided context. If \`ORIENTATION_REQUIREMENTS: SATISFIED\` is present, do not repeat discovery tools.
|
|
24
|
+
- **YOLO Mode**: Proceed autonomously until \`task_complete\` is called.
|
|
25
|
+
|
|
26
|
+
## 3. Subagent Guardrails
|
|
27
|
+
- **No State Mutation**: Subagents (e.g., \`@explore\`, \`@general\`) are STRICTLY FORBIDDEN from calling \`sc_init\` or \`sc_approve\`. Only the primary \`plan\` or \`build\` agent may mutate the global project lifecycle state.
|
|
28
|
+
- **Architectural Discovery**: Subagents MUST prioritize \`mcpx map\` tools (\`pm_query\` for context, \`pm_fetch_symbol\` for precise code extraction) to navigate the codebase efficiently. Avoid wide \`glob\` or \`read\` loops on large files.
|
|
29
|
+
`
|
|
30
|
+
|
|
31
|
+
const MCP_CONFIG_GUIDE_CONTENT = `# Guide: Setting Up MCP Servers with \`mcpx-rust\`
|
|
32
|
+
|
|
33
|
+
This guide explains how to configure Model Context Protocol (MCP) servers within this project using the \`mcpx-rust\` CLI utility. By using \`mcpx\`, we reduce prompt bloat by replacing massive JSON schemas with a single, discoverable CLI interface.
|
|
34
|
+
|
|
35
|
+
## 1. Installation
|
|
36
|
+
|
|
37
|
+
\`mcpx-rust\` is the Rust-based binary used in this project to turn MCP servers into composable shell commands.
|
|
38
|
+
|
|
39
|
+
\`\`\`bash
|
|
40
|
+
# Via Cargo
|
|
41
|
+
cargo install mcpx-rust
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
## 2. Registering Servers
|
|
45
|
+
|
|
46
|
+
\`mcpx-rust\` stores its configuration in \`~/.config/mcpx/config.toml\`. You must edit this file manually to add or modify servers.
|
|
47
|
+
|
|
48
|
+
### Manual Configuration
|
|
49
|
+
Add entries to the \`[mcp_servers]\` section in \`~/.config/mcpx/config.toml\`:
|
|
50
|
+
|
|
51
|
+
\`\`\`toml
|
|
52
|
+
[mcp_servers.map]
|
|
53
|
+
command = "project-map-cli-rust"
|
|
54
|
+
args = ["mcp"]
|
|
55
|
+
|
|
56
|
+
[mcp_servers.spec]
|
|
57
|
+
command = "deliver-cli"
|
|
58
|
+
args = ["mcp"]
|
|
59
|
+
|
|
60
|
+
[mcp_servers.github]
|
|
61
|
+
command = "npx"
|
|
62
|
+
args = ["-y", "@modelcontextprotocol/server-github"]
|
|
63
|
+
env = { GITHUB_TOKEN = "\${GITHUB_TOKEN}" }
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
## 3. \`epochcli\` / \`gemini-cli\` Integration
|
|
67
|
+
|
|
68
|
+
To enable \`mcpx\` integration, update your configuration file (e.g., \`.epochcli/epochcli.jsonc\` or \`.gemini/settings.json\`):
|
|
69
|
+
|
|
70
|
+
\`\`\`jsonc
|
|
71
|
+
{
|
|
72
|
+
"mcpx": {
|
|
73
|
+
"enabled": true,
|
|
74
|
+
"binaryPath": "mcpx-rust" // Optional: defaults to 'mcpx-rust'
|
|
75
|
+
},
|
|
76
|
+
"mcp": {} // Leave empty to disable standard schema-based MCP tools
|
|
77
|
+
}
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
When enabled, the CLI will only expose a single \`mcpx\` tool to the LLM. The agent will discover capabilities dynamically by running \`mcpx <server> --help\`.
|
|
81
|
+
|
|
82
|
+
## 4. Usage and Composition
|
|
83
|
+
|
|
84
|
+
Once configured, tools can be called using standard shell composition:
|
|
85
|
+
|
|
86
|
+
\`\`\`bash
|
|
87
|
+
# List all servers
|
|
88
|
+
mcpx-rust list
|
|
89
|
+
|
|
90
|
+
# List tools for a server
|
|
91
|
+
mcpx-rust github
|
|
92
|
+
|
|
93
|
+
# Inspect a specific tool's schema
|
|
94
|
+
mcpx-rust github search-repositories --help
|
|
95
|
+
|
|
96
|
+
# Call a tool and pipe to jq
|
|
97
|
+
mcpx-rust github search-repositories query=mcp --json | jq -r '.content[0].text'
|
|
98
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
Note: \`mcpx-rust\` uses \`key=value\` syntax for positional arguments or standard \`--flag value\` syntax depending on the tool's implementation.
|
|
101
|
+
|
|
102
|
+
## 5. Project Servers
|
|
103
|
+
|
|
104
|
+
The following project-specific servers are pre-configured. Agents should use the unified \`mcpx\` tool for all operations:
|
|
105
|
+
|
|
106
|
+
- **\`spec\`**: Management of specification-driven development.
|
|
107
|
+
- \`mcpx spec sc_status\`: View project health and next steps.
|
|
108
|
+
- \`mcpx spec sc_todo_start\`: Mark a task as active.
|
|
109
|
+
- **\`map\`**: Architectural mapping and symbol analysis.
|
|
110
|
+
- \`mcpx map pm_query\`: Search for symbols or get file context.
|
|
111
|
+
- \`mcpx map pm_plan\`: Analyze the architectural impact of a change.
|
|
112
|
+
- **\`ground\`**: Synthesis of behavioral rules and operational facts.
|
|
113
|
+
- \`mcpx ground gt_status\`: Check current project rules.
|
|
114
|
+
- \`mcpx ground gt_refresh\`: Force a refresh of the project constitution.
|
|
115
|
+
|
|
116
|
+
## 6. Agent SOP: Adding a New Server
|
|
117
|
+
|
|
118
|
+
When an agent is instructed to add or install a new MCP server, it MUST follow this sequence to ensure the server is properly registered and discoverable:
|
|
119
|
+
|
|
120
|
+
1. **Identify the Server**: Determine the correct command (e.g., \`npx -y package-name\`) or binary path for the requested MCP server.
|
|
121
|
+
2. **Register the Server**: Update the configuration in \`~/.config/mcpx/config.toml\`.
|
|
122
|
+
- *Note*: Agents must use \`echo\` or \`cat <<EOF\` via \`run_shell_command\` to modify this file, as it lives outside the standard workspace directory.
|
|
123
|
+
3. **Verify Installation**: Run \`mcpx-rust list\` and \`mcpx-rust <new_server> --help\` to ensure the routing engine recognizes the new server and the tool schema is accessible.
|
|
124
|
+
4. **Update Project Knowledge**: Update \`AGENTS.md\` (or the relevant instruction file) with a brief summary of the new server's capabilities and its \`mcpx\` syntax so that future agent turns can utilize the new tools.
|
|
125
|
+
`
|
|
126
|
+
|
|
127
|
+
const MODEL_CONFIG_GUIDE_CONTENT = `# Model Configuration Guide
|
|
128
|
+
|
|
129
|
+
Epoch CLI utilizes a **Dual-Model Architecture** optimized for high-performance coding and reliable background supervision. This environment uses a unified proxy architecture (**llama-swap**) to manage model transitions efficiently.
|
|
130
|
+
|
|
131
|
+
## 1. Unified Architecture (llama-swap)
|
|
132
|
+
|
|
133
|
+
Instead of managing separate URLs and ports, all models are served through a single transparent proxy on one port. This allows the system to dynamically swap models in VRAM as needed while maintaining a consistent client configuration.
|
|
134
|
+
|
|
135
|
+
* **Unified API Endpoint:** \`http://localhost:8085/v1\`
|
|
136
|
+
* **Protocol:** OpenAI Compatible
|
|
137
|
+
* **Authentication:** Shared API Key (e.g., \`2250\`)
|
|
138
|
+
|
|
139
|
+
## 2. Configuration (\`epochcli.jsonc\`)
|
|
140
|
+
|
|
141
|
+
You can configure Epoch CLI to use either a single unified endpoint for both main and side roles, or a dual-model configuration that leverages the \`llama-swap\` proxy.
|
|
142
|
+
|
|
143
|
+
### Option A: Single Endpoint Configuration (Recommended for High Context)
|
|
144
|
+
|
|
145
|
+
This setup uses one model for both roles. It is ideal for maximizing the context window (e.g., 64k) and eliminating the 15-20 second "cold start" delay associated with swapping models.
|
|
146
|
+
|
|
147
|
+
\`\`\`jsonc
|
|
148
|
+
{
|
|
149
|
+
"model": "local-unified/qwen-unified",
|
|
150
|
+
"side_model": "local-unified/qwen-unified",
|
|
151
|
+
"provider": {
|
|
152
|
+
"local-unified": {
|
|
153
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
154
|
+
"name": "Local Unified (Qwen 64k)",
|
|
155
|
+
"options": {
|
|
156
|
+
"baseURL": "http://localhost:8085/v1",
|
|
157
|
+
"apiKey": "2250"
|
|
158
|
+
},
|
|
159
|
+
"models": {
|
|
160
|
+
"qwen-unified": {
|
|
161
|
+
"name": "Qwen Unified 64k",
|
|
162
|
+
"limit": { "context": 64000, "output": 4096 }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
\`\`\`
|
|
169
|
+
|
|
170
|
+
### Option B: Dual-Model Configuration (llama-swap)
|
|
171
|
+
|
|
172
|
+
This setup defines separate providers for main and side roles. The proxy handles routing based on the model ID. This is useful when you want a dedicated smaller model for clerk duties to save compute or when specific roles require different model capabilities.
|
|
173
|
+
|
|
174
|
+
\`\`\`jsonc
|
|
175
|
+
{
|
|
176
|
+
"model": "local-main/qwen3.6-35b-a3b-coding",
|
|
177
|
+
"side_model": "local-side/nemotron-3-nano",
|
|
178
|
+
"provider": {
|
|
179
|
+
"local-main": {
|
|
180
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
181
|
+
"name": "Local Main (Coding)",
|
|
182
|
+
"options": {
|
|
183
|
+
"baseURL": "http://localhost:8085/v1",
|
|
184
|
+
"apiKey": "2250"
|
|
185
|
+
},
|
|
186
|
+
"models": {
|
|
187
|
+
"qwen3.6-35b-a3b-coding": {
|
|
188
|
+
"name": "Qwen 3.6 35B Coding",
|
|
189
|
+
"limit": { "context": 64000, "output": 4096 }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"local-side": {
|
|
194
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
195
|
+
"name": "Local Side (Clerk)",
|
|
196
|
+
"options": {
|
|
197
|
+
"baseURL": "http://localhost:8085/v1",
|
|
198
|
+
"apiKey": "2250"
|
|
199
|
+
},
|
|
200
|
+
"models": {
|
|
201
|
+
"nemotron-3-nano": {
|
|
202
|
+
"name": "Nemotron 3 Nano",
|
|
203
|
+
"limit": { "context": 32000, "output": 2048 }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
\`\`\`
|
|
210
|
+
|
|
211
|
+
### Switching Between Modes
|
|
212
|
+
|
|
213
|
+
To switch modes, simply update the \`model\`, \`side_model\`, and \`provider\` fields in your \`.epochcli/epochcli.jsonc\` file to match the desired configuration block. The CLI will automatically pick up the changes on the next execution.
|
|
214
|
+
|
|
215
|
+
## 3. How Epoch CLI Identifies Models
|
|
216
|
+
|
|
217
|
+
Epoch CLI uses **keyword matching** on the Model ID string to apply specific architectural optimizations. You do not need to manually configure prompts or behaviors for different models as long as the ID is named correctly:
|
|
218
|
+
|
|
219
|
+
* **Qwen Optimized:** If the ID contains \`"qwen"\` (e.g., \`qwen3.6-35b-a3b-coding\`), the CLI automatically sets the temperature to \`0.55\` and Top-P to \`1.0\`.
|
|
220
|
+
* **Gemma Optimized:** If the ID contains \`"gemma-4"\`, the CLI enables reasoning token injection (\`<|think|>\`), specialized system prompts, and the Three-Stage Sanitizer to repair potential JSON errors.
|
|
221
|
+
|
|
222
|
+
## 4. Operational Considerations
|
|
223
|
+
|
|
224
|
+
### The "Cold Start" (Model Swapping)
|
|
225
|
+
The environment uses a **SWAP approach** to maximize VRAM for high-context models. Only one model is active in memory at a time.
|
|
226
|
+
* **Instant Response:** If you request the model that is already "hot" in memory.
|
|
227
|
+
* **Swap Delay:** If you request a model that is currently swapped out, the proxy will load it automatically. This adds a **15–20 second delay** to the first request.
|
|
228
|
+
|
|
229
|
+
### Context Persistence & TTL
|
|
230
|
+
Models stay active in VRAM for **60 minutes** of inactivity before being automatically unloaded. This ensures subsequent requests within the same hour are nearly instantaneous.
|
|
231
|
+
|
|
232
|
+
## 5. Monitoring & Maintenance
|
|
233
|
+
|
|
234
|
+
* **Dashboard:** You can monitor which model is currently active and view proxy status at \`http://localhost:8085/ui\`.
|
|
235
|
+
* **Restart Stack:** If you need to restart the entire dual-model stack, use the provided launch script:
|
|
236
|
+
\`\`\`bash
|
|
237
|
+
/home/llm/utils/launch/launch-dual.sh
|
|
238
|
+
\`\`\`
|
|
239
|
+
`
|
|
240
|
+
|
|
241
|
+
const DEFAULT_EPOCHCLI_JSONC = `{
|
|
242
|
+
"$schema": "./config.json",
|
|
243
|
+
"model": "local-unified/qwen-unified",
|
|
244
|
+
"side_model": "local-unified/qwen-unified",
|
|
245
|
+
"provider": {
|
|
246
|
+
"local-unified": {
|
|
247
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
248
|
+
"name": "Local Unified (Qwen 64k)",
|
|
249
|
+
"options": {
|
|
250
|
+
"baseURL": "http://localhost:8085/v1",
|
|
251
|
+
"apiKey": "2250"
|
|
252
|
+
},
|
|
253
|
+
"models": {
|
|
254
|
+
"qwen-unified": {
|
|
255
|
+
"name": "Qwen Unified 64k",
|
|
256
|
+
"limit": {
|
|
257
|
+
"context": 64000,
|
|
258
|
+
"output": 4096
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
"permission": {
|
|
265
|
+
"edit": {
|
|
266
|
+
"packages/opencode/migration/*": "deny"
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
"mcpx": {
|
|
270
|
+
"enabled": true
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
`
|
|
274
|
+
|
|
275
|
+
export async function initProjectFiles(directory: string, worktree: string) {
|
|
276
|
+
try {
|
|
277
|
+
const epochcliDir = path.join(worktree, ".epochcli")
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await fs.access(epochcliDir)
|
|
281
|
+
return // Already initialized
|
|
282
|
+
} catch {
|
|
283
|
+
// Doesn't exist, proceed with initialization
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
log.info("Initializing new project files", { worktree })
|
|
287
|
+
|
|
288
|
+
// Create .epochcli directory
|
|
289
|
+
await fs.mkdir(epochcliDir, { recursive: true })
|
|
290
|
+
|
|
291
|
+
// Generate JSON Schema
|
|
292
|
+
const schema = zodToJsonSchema(Config.Info, {
|
|
293
|
+
name: "Config",
|
|
294
|
+
$refStrategy: "none",
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// Write config.json (schema)
|
|
298
|
+
await fs.writeFile(
|
|
299
|
+
path.join(epochcliDir, "config.json"),
|
|
300
|
+
JSON.stringify(schema, null, 2)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
// Write default epochcli.jsonc
|
|
304
|
+
await fs.writeFile(
|
|
305
|
+
path.join(epochcliDir, "epochcli.jsonc"),
|
|
306
|
+
DEFAULT_EPOCHCLI_JSONC
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
// Setup docs
|
|
310
|
+
const docsDir = path.join(epochcliDir, "docs")
|
|
311
|
+
await fs.mkdir(docsDir, { recursive: true })
|
|
312
|
+
await fs.writeFile(path.join(docsDir, "MCP_config_guide.md"), MCP_CONFIG_GUIDE_CONTENT)
|
|
313
|
+
await fs.writeFile(path.join(docsDir, "model_config.md"), MODEL_CONFIG_GUIDE_CONTENT)
|
|
314
|
+
|
|
315
|
+
// Setup AGENTS.md in the current directory if it doesn't exist
|
|
316
|
+
const agentsFile = path.join(directory, "AGENTS.md")
|
|
317
|
+
try {
|
|
318
|
+
await fs.access(agentsFile)
|
|
319
|
+
} catch {
|
|
320
|
+
await fs.writeFile(agentsFile, AGENTS_MD_CONTENT)
|
|
321
|
+
log.info("Created AGENTS.md", { path: agentsFile })
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
log.info("Project initialized successfully", { dir: epochcliDir })
|
|
325
|
+
} catch (error) {
|
|
326
|
+
log.error("Failed to initialize project files", { error })
|
|
327
|
+
}
|
|
328
|
+
}
|
package/src/session/overflow.ts
CHANGED
|
@@ -30,6 +30,5 @@ export function isOverflow(input: {
|
|
|
30
30
|
(input.tokens.reasoning ?? 0) +
|
|
31
31
|
((input.tokens.cache?.read ?? 0) + (input.tokens.cache?.write ?? 0)))
|
|
32
32
|
|
|
33
|
-
console.log(`[OverflowCheck] tokens=${count} usable=${usable} (limit=${context} margin=${safetyMargin})`)
|
|
34
33
|
return count >= usable
|
|
35
34
|
}
|
|
File without changes
|