@semalt-ai/code 1.8.5 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +7 -1
- package/.github/workflows/ci.yml +69 -0
- package/ARCHITECTURE.md +6 -95
- package/CLAUDE.md +196 -316
- package/README.md +148 -4
- package/docs/ARCHITECTURE.md +1321 -0
- package/docs/CONFIG.md +340 -0
- package/docs/HISTORY.md +245 -0
- package/examples/embed.js +74 -0
- package/index.js +251 -10
- package/lib/agent.js +856 -120
- package/lib/api.js +239 -50
- package/lib/args.js +74 -2
- package/lib/audit.js +23 -1
- package/lib/background.js +584 -0
- package/lib/checkpoints.js +757 -0
- package/lib/commands/auth.js +94 -0
- package/lib/commands/chat-session.js +489 -0
- package/lib/commands/chat-slash.js +415 -0
- package/lib/commands/chat-turn.js +669 -0
- package/lib/commands/chat.js +407 -0
- package/lib/commands/custom.js +157 -0
- package/lib/commands/history-utils.js +66 -0
- package/lib/commands/index.js +268 -0
- package/lib/commands/mcp.js +113 -0
- package/lib/commands/oneshot.js +193 -0
- package/lib/commands/registry.js +269 -0
- package/lib/commands/tasks.js +89 -0
- package/lib/compact.js +87 -0
- package/lib/config.js +360 -11
- package/lib/constants.js +401 -3
- package/lib/deny.js +199 -0
- package/lib/doctor.js +160 -0
- package/lib/headless.js +202 -0
- package/lib/hooks.js +286 -0
- package/lib/images.js +270 -0
- package/lib/internals.js +49 -0
- package/lib/mcp/boundary.js +131 -0
- package/lib/mcp/client.js +270 -0
- package/lib/mcp/oauth.js +134 -0
- package/lib/memory.js +209 -0
- package/lib/metrics.js +37 -2
- package/lib/payload.js +54 -0
- package/lib/permission-rules.js +401 -0
- package/lib/permissions.js +123 -26
- package/lib/pricing.js +67 -0
- package/lib/proc.js +62 -0
- package/lib/prompts.js +99 -8
- package/lib/sandbox.js +568 -0
- package/lib/sdk.js +328 -0
- package/lib/secrets.js +211 -0
- package/lib/skills.js +223 -0
- package/lib/subagents.js +516 -0
- package/lib/tool_registry.js +2862 -0
- package/lib/tool_specs.js +263 -9
- package/lib/tools.js +352 -1039
- package/lib/ui/anim.js +86 -0
- package/lib/ui/ansi.js +17 -27
- package/lib/ui/chat-history.js +253 -71
- package/lib/ui/create-ui.js +67 -24
- package/lib/ui/diff.js +90 -25
- package/lib/ui/file-activity.js +236 -0
- package/lib/ui/format.js +195 -29
- package/lib/ui/input-field.js +21 -11
- package/lib/ui/md-stream.js +234 -0
- package/lib/ui/render-operation.js +113 -0
- package/lib/ui/select.js +1 -4
- package/lib/ui/status-bar.js +146 -36
- package/lib/ui/stream.js +20 -13
- package/lib/ui/theme.js +190 -44
- package/lib/ui/tool-operation.js +190 -0
- package/lib/ui/utils.js +9 -5
- package/lib/ui/web-activity.js +270 -0
- package/lib/ui/writer.js +159 -45
- package/lib/ui.js +1 -1
- package/lib/verify.js +229 -0
- package/lib/web-extract.js +213 -0
- package/lib/web-summarize.js +68 -0
- package/package.json +19 -4
- package/scripts/lint.js +57 -0
- package/test/agent-loop.test.js +389 -0
- package/test/anim-driver.test.js +153 -0
- package/test/ask-user-display.test.js +226 -0
- package/test/ask-user-gate.test.js +231 -0
- package/test/background.test.js +414 -0
- package/test/chat-history-nocolor.test.js +155 -0
- package/test/chat-relogin.test.js +207 -0
- package/test/chat.test.js +114 -0
- package/test/checkpoints-agent.test.js +181 -0
- package/test/checkpoints.test.js +650 -0
- package/test/command-registry.test.js +160 -0
- package/test/compact.test.js +116 -0
- package/test/completion-lazy.test.js +52 -0
- package/test/config-merge.test.js +324 -0
- package/test/config-quarantine.test.js +128 -0
- package/test/config-write-guard-allow-anywhere.test.js +56 -0
- package/test/config-write-guard-skip.test.js +46 -0
- package/test/config-write-guard.test.js +153 -0
- package/test/context-split.test.js +215 -0
- package/test/cost-doctor.test.js +142 -0
- package/test/custom-commands-chat.test.js +106 -0
- package/test/custom-commands.test.js +230 -0
- package/test/defer-detail-band.test.js +403 -0
- package/test/deny-windows.test.js +120 -0
- package/test/deny.test.js +83 -0
- package/test/detail-band-tab-flatten.test.js +242 -0
- package/test/download-allow-anywhere.test.js +66 -0
- package/test/download-confine.test.js +153 -0
- package/test/exec-diff.test.js +268 -0
- package/test/executors.test.js +599 -0
- package/test/extract-tool-calls.test.js +349 -0
- package/test/fetch-url-validation.test.js +219 -0
- package/test/file-activity.test.js +522 -0
- package/test/fixtures/tool-calls.js +57 -0
- package/test/fixtures/web-page.js +91 -0
- package/test/git-tools.test.js +384 -0
- package/test/grep-glob-serialize.test.js +242 -0
- package/test/grep-glob.test.js +268 -0
- package/test/grep-path-target.test.js +227 -0
- package/test/harness/README.md +57 -0
- package/test/harness/chat-harness.js +143 -0
- package/test/harness/memwarn-headless-child.js +65 -0
- package/test/harness/mock-llm.js +120 -0
- package/test/harness/mock-mcp-server.js +142 -0
- package/test/harness/sse-server.js +69 -0
- package/test/headless.test.js +348 -0
- package/test/history-utils.test.js +88 -0
- package/test/hooks-agent.test.js +238 -0
- package/test/hooks-verify-sandbox.test.js +232 -0
- package/test/hooks.test.js +216 -0
- package/test/http-get-user-agent.test.js +142 -0
- package/test/images-api.test.js +208 -0
- package/test/images.test.js +238 -0
- package/test/input-field-ctrl-o.test.js +37 -0
- package/test/live-height-physical.test.js +281 -0
- package/test/max-iterations.test.js +218 -0
- package/test/mcp-boundary.test.js +57 -0
- package/test/mcp-client.test.js +267 -0
- package/test/mcp-oauth.test.js +86 -0
- package/test/md-stream.test.js +183 -0
- package/test/memory-truncation-warning.test.js +222 -0
- package/test/memory.test.js +198 -0
- package/test/native-dispatch.test.js +409 -0
- package/test/native-live-narration.test.js +254 -0
- package/test/output-chokepoint.test.js +188 -0
- package/test/output-heredoc-leak.test.js +195 -0
- package/test/output-preview.test.js +245 -0
- package/test/path-guards.test.js +134 -0
- package/test/payload.test.js +99 -0
- package/test/permission-rules-agent.test.js +210 -0
- package/test/permission-rules.test.js +297 -0
- package/test/permissions.test.js +362 -0
- package/test/plan-mode.test.js +167 -0
- package/test/read-paginate.test.js +275 -0
- package/test/readonly-tools.test.js +177 -0
- package/test/render-operation.test.js +317 -0
- package/test/replay-descriptor-xml.test.js +216 -0
- package/test/replay-descriptor.test.js +189 -0
- package/test/replay-web-aggregate.test.js +291 -0
- package/test/replay-web-persist.test.js +241 -0
- package/test/result-cap.test.js +233 -0
- package/test/running-glyph-anim.test.js +111 -0
- package/test/sandbox-agent.test.js +147 -0
- package/test/sandbox-integration.test.js +216 -0
- package/test/sandbox.test.js +408 -0
- package/test/sdk.test.js +234 -0
- package/test/shell-output-cap.test.js +181 -0
- package/test/skills-chat.test.js +110 -0
- package/test/skills.test.js +295 -0
- package/test/smoke.test.js +68 -0
- package/test/status-bar-driver.test.js +93 -0
- package/test/status-bar-pause.test.js +164 -0
- package/test/status-bar-resync.test.js +188 -0
- package/test/stream-parser.test.js +171 -0
- package/test/subagents-agent.test.js +178 -0
- package/test/subagents.test.js +222 -0
- package/test/theme-palette.test.js +166 -0
- package/test/tool-registry.test.js +85 -0
- package/test/trim-budget.test.js +101 -0
- package/test/truncate-visible.test.js +78 -0
- package/test/verify-agent.test.js +317 -0
- package/test/verify.test.js +141 -0
- package/test/view-image.test.js +199 -0
- package/test/web-activity-ordering.test.js +203 -0
- package/test/web-activity.test.js +207 -0
- package/test/web-data-extraction-guidance.test.js +71 -0
- package/test/web-extract.test.js +185 -0
- package/test/web-fetch-agent.test.js +291 -0
- package/test/web-fetch-mode.test.js +193 -0
- package/test/web-search.test.js +380 -0
- package/lib/commands.js +0 -1438
- package/path +0 -1
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// createCommands — the public command surface, wired from the cohesive modules
|
|
4
|
+
// this directory was split into in Task 1.5:
|
|
5
|
+
// registry.js — slash-command registry (source of truth)
|
|
6
|
+
// history-utils.js — pure saved-chat message helpers
|
|
7
|
+
// auth.js — login / whoami / logout / auth set-key
|
|
8
|
+
// oneshot.js — code / edit / shell / models / init (non-interactive)
|
|
9
|
+
// chat.js — the interactive chat command (cmdChat)
|
|
10
|
+
// chat-session.js / chat-slash.js / chat-turn.js — cmdChat internals
|
|
11
|
+
//
|
|
12
|
+
// This module keeps only the shared helpers (resolveTokenLimit, ensureDefaultModel,
|
|
13
|
+
// the catastrophic-shell confirm, the dry-run summary) and the dependency bags
|
|
14
|
+
// passed to each group.
|
|
15
|
+
|
|
16
|
+
const { CONFIG_PATH, DEFAULT_API_TIMEOUT_MS } = require('../constants');
|
|
17
|
+
const { getSkippedOps } = require('../tools');
|
|
18
|
+
const { keychainSet, ENV_VAR } = require('../secrets');
|
|
19
|
+
const writer = require('../ui/writer');
|
|
20
|
+
const msgs = require('../ui/messages');
|
|
21
|
+
const dbg = require('../debug');
|
|
22
|
+
const { createAuthCommands } = require('./auth');
|
|
23
|
+
const { createOneshotCommands } = require('./oneshot');
|
|
24
|
+
const { createTaskCommands } = require('./tasks');
|
|
25
|
+
const { createChatCommand } = require('./chat');
|
|
26
|
+
const { createMcpManager } = require('../mcp/client');
|
|
27
|
+
const mcpCmd = require('./mcp');
|
|
28
|
+
|
|
29
|
+
function createCommands({
|
|
30
|
+
getConfig,
|
|
31
|
+
setConfig,
|
|
32
|
+
permissionManager,
|
|
33
|
+
ui,
|
|
34
|
+
apiClient,
|
|
35
|
+
runAgentLoop,
|
|
36
|
+
readFileContext,
|
|
37
|
+
agentExecShell,
|
|
38
|
+
checkpointStore,
|
|
39
|
+
}) {
|
|
40
|
+
const {
|
|
41
|
+
BOLD,
|
|
42
|
+
BG_SELECTED,
|
|
43
|
+
FG_BLUE,
|
|
44
|
+
FG_CYAN,
|
|
45
|
+
FG_DARK,
|
|
46
|
+
FG_GRAY,
|
|
47
|
+
FG_GREEN,
|
|
48
|
+
FG_RED,
|
|
49
|
+
FG_TEAL,
|
|
50
|
+
FG_YELLOW,
|
|
51
|
+
RST,
|
|
52
|
+
approxTokens,
|
|
53
|
+
getCols,
|
|
54
|
+
boxLine,
|
|
55
|
+
interactiveSelect,
|
|
56
|
+
} = ui;
|
|
57
|
+
const {
|
|
58
|
+
chatStream,
|
|
59
|
+
chatSync,
|
|
60
|
+
dashboardCreateChat,
|
|
61
|
+
dashboardGetChat,
|
|
62
|
+
dashboardGetModelForCli,
|
|
63
|
+
dashboardListChats,
|
|
64
|
+
dashboardListModels,
|
|
65
|
+
dashboardLogout,
|
|
66
|
+
dashboardSaveMessages,
|
|
67
|
+
dashboardWhoAmI,
|
|
68
|
+
estimateTokens,
|
|
69
|
+
getCliLoginStatus,
|
|
70
|
+
requestCliLogin,
|
|
71
|
+
setActiveModelProfile,
|
|
72
|
+
} = apiClient;
|
|
73
|
+
|
|
74
|
+
const LOGIN_POLL_INTERVAL_MS = 2000;
|
|
75
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
76
|
+
|
|
77
|
+
function formatUserLine(label, value) {
|
|
78
|
+
return ` ${FG_CYAN}${label}:${RST} ${FG_GRAY}${value}${RST}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Typo-guard confirmation for the catastrophic subset of *user-initiated*
|
|
82
|
+
// shell commands (disk wipe / fork bomb). agentExecShell calls this via the
|
|
83
|
+
// `confirm` option only when classifyShellCommand returns 'confirm'. Defaults
|
|
84
|
+
// to No and refuses outright in non-TTY contexts (no way to ask). `navOpts`
|
|
85
|
+
// supplies captureNavigation when invoked from inside the chat TUI.
|
|
86
|
+
async function confirmCatastrophicShell(label, command, navOpts) {
|
|
87
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
88
|
+
const idx = await interactiveSelect(
|
|
89
|
+
['No, cancel', 'Yes, run it anyway'],
|
|
90
|
+
(item, isSelected) => {
|
|
91
|
+
const cursor = isSelected ? `${FG_YELLOW}❯${RST}` : ' ';
|
|
92
|
+
const style = isSelected ? FG_CYAN : FG_GRAY;
|
|
93
|
+
return ` ${cursor} ${style}${item}${RST}`;
|
|
94
|
+
},
|
|
95
|
+
{ initialIndex: 0, ...(navOpts || {}) },
|
|
96
|
+
);
|
|
97
|
+
return idx === 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function resolveTokenLimit(model) {
|
|
101
|
+
const config = getConfig();
|
|
102
|
+
if (config.auth_token && config.dashboard_model_id) {
|
|
103
|
+
try {
|
|
104
|
+
const resp = await dashboardGetModelForCli(config.dashboard_model_id);
|
|
105
|
+
const m = resp && resp.model ? resp.model : null;
|
|
106
|
+
if (m) {
|
|
107
|
+
const limit = (Number.isInteger(m.context_length) && m.context_length > 0 ? m.context_length : null)
|
|
108
|
+
|| (Number.isInteger(m.max_tokens) && m.max_tokens > 0 ? m.max_tokens : null);
|
|
109
|
+
if (limit) {
|
|
110
|
+
// Persist so chatStream's proactive trimming can use it without an extra API call.
|
|
111
|
+
if (config.context_length !== limit) {
|
|
112
|
+
setConfig({ ...config, context_length: limit });
|
|
113
|
+
}
|
|
114
|
+
return limit;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
const localModels = Array.isArray(config.models) ? config.models : [];
|
|
120
|
+
const match = localModels.find(
|
|
121
|
+
(m) => m.model === model || (m.api_base === config.api_base && m.model === config.default_model)
|
|
122
|
+
);
|
|
123
|
+
if (match && Number.isInteger(match.context_length) && match.context_length > 0) return match.context_length;
|
|
124
|
+
if (Number.isInteger(config.context_length) && config.context_length > 0) return config.context_length;
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Pick the first dashboard model when the user is authenticated but has
|
|
129
|
+
// not selected one yet. Persists credentials to config and returns
|
|
130
|
+
// { name, modelId } on success; null otherwise (not logged in, already
|
|
131
|
+
// selected, empty list, or API error).
|
|
132
|
+
async function ensureDefaultModel() {
|
|
133
|
+
const config = getConfig();
|
|
134
|
+
if (!config.auth_token) return null;
|
|
135
|
+
if (config.default_model && config.dashboard_model_id) return null;
|
|
136
|
+
let response;
|
|
137
|
+
try { response = await dashboardListModels(); } catch { return null; }
|
|
138
|
+
const models = Array.isArray(response && response.models) ? response.models : [];
|
|
139
|
+
if (!models.length) return null;
|
|
140
|
+
const first = models[0];
|
|
141
|
+
let credResp;
|
|
142
|
+
try { credResp = await dashboardGetModelForCli(first.id); } catch { return null; }
|
|
143
|
+
const model = credResp && credResp.model ? credResp.model : null;
|
|
144
|
+
if (!model) return null;
|
|
145
|
+
const contextLength = (Number.isInteger(model.context_length) && model.context_length > 0 ? model.context_length : null)
|
|
146
|
+
|| (Number.isInteger(model.max_tokens) && model.max_tokens > 0 ? model.max_tokens : null);
|
|
147
|
+
const updated = { ...config, api_base: model.base_url, api_key: model.api_key, default_model: model.model_id, dashboard_model_id: model.id };
|
|
148
|
+
if (contextLength !== null) updated.context_length = contextLength;
|
|
149
|
+
setConfig(updated);
|
|
150
|
+
return { name: model.name, modelId: model.model_id };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function printDryRunSummary() {
|
|
154
|
+
const ops = getSkippedOps();
|
|
155
|
+
const files = ops.filter((o) => o.category === 'file');
|
|
156
|
+
const cmds = ops.filter((o) => o.category === 'cmd');
|
|
157
|
+
const nets = ops.filter((o) => o.category === 'net');
|
|
158
|
+
const BOX_W = 40, INNER = BOX_W - 2;
|
|
159
|
+
const isTTY = process.stdout.isTTY;
|
|
160
|
+
const stripA = (s) => s.replace(/\x1b\[[^m]*m/g, '');
|
|
161
|
+
const row = (content) => { const visible = stripA(content).length; const pad = ' '.repeat(Math.max(0, INNER - visible)); return isTTY ? `${FG_TEAL}║${RST}${content}${pad}${FG_TEAL}║${RST}` : `║${stripA(content)}${pad}║`; };
|
|
162
|
+
const hr40 = (tl, fill, tr) => { const line = tl + fill.repeat(INNER) + tr; return isTTY ? `${FG_TEAL}${line}${RST}` : line; };
|
|
163
|
+
const out = [
|
|
164
|
+
'',
|
|
165
|
+
hr40('╔','═','╗'),
|
|
166
|
+
row(` ${isTTY ? BOLD : ''}DRY-RUN SUMMARY${isTTY ? RST : ''}`),
|
|
167
|
+
hr40('╠','═','╣'),
|
|
168
|
+
row(` ✎ Files that would change: ${files.length} `),
|
|
169
|
+
row(` ▶ Commands that would run: ${cmds.length} `),
|
|
170
|
+
row(` ↓ Network calls: ${nets.length} `),
|
|
171
|
+
hr40('╚','═','╝'),
|
|
172
|
+
];
|
|
173
|
+
if (ops.length > 0) {
|
|
174
|
+
out.push('');
|
|
175
|
+
for (const op of ops) {
|
|
176
|
+
out.push(isTTY ? ` ${op.symbol} ${FG_GRAY}${op.desc}${RST}` : ` ${op.symbol} ${op.desc}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
out.push('');
|
|
180
|
+
writer.scrollback(out.join('\n'));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Dependency bag shared with the non-chat command groups. printDryRunSummary
|
|
184
|
+
// is a function declaration above (hoisting not required, but kept consistent).
|
|
185
|
+
const shared = {
|
|
186
|
+
getConfig, setConfig, permissionManager, runAgentLoop, readFileContext, agentExecShell,
|
|
187
|
+
chatStream, chatSync, dashboardCreateChat, dashboardGetChat, dashboardGetModelForCli,
|
|
188
|
+
dashboardListChats, dashboardListModels, dashboardLogout, dashboardSaveMessages, dashboardWhoAmI,
|
|
189
|
+
estimateTokens, getCliLoginStatus, requestCliLogin, setActiveModelProfile,
|
|
190
|
+
BOLD, BG_SELECTED, FG_BLUE, FG_CYAN, FG_DARK, FG_GRAY, FG_GREEN, FG_RED, FG_TEAL, FG_YELLOW, RST,
|
|
191
|
+
approxTokens, getCols, boxLine, interactiveSelect,
|
|
192
|
+
writer, msgs, dbg,
|
|
193
|
+
keychainSet, ENV_VAR, CONFIG_PATH, DEFAULT_API_TIMEOUT_MS,
|
|
194
|
+
LOGIN_POLL_INTERVAL_MS, LOGIN_TIMEOUT_MS,
|
|
195
|
+
formatUserLine, confirmCatastrophicShell, resolveTokenLimit, ensureDefaultModel, printDryRunSummary,
|
|
196
|
+
};
|
|
197
|
+
const authCommands = createAuthCommands(shared);
|
|
198
|
+
const oneshotCommands = createOneshotCommands(shared);
|
|
199
|
+
const taskCommands = createTaskCommands(shared);
|
|
200
|
+
const { cmdChat } = createChatCommand({
|
|
201
|
+
getConfig, setConfig, permissionManager, ui, apiClient,
|
|
202
|
+
runAgentLoop, readFileContext, agentExecShell,
|
|
203
|
+
resolveTokenLimit, ensureDefaultModel, confirmCatastrophicShell,
|
|
204
|
+
checkpointStore,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// `semalt-code mcp <list|status|add|remove|auth>` — manage MCP servers and
|
|
208
|
+
// report connection status (Task 3.3).
|
|
209
|
+
async function cmdMcp(sub, argv = []) {
|
|
210
|
+
const cfg = getConfig();
|
|
211
|
+
const servers = (cfg.mcp && cfg.mcp.servers) || {};
|
|
212
|
+
|
|
213
|
+
if (!sub || sub === 'list') {
|
|
214
|
+
writer.scrollback(mcpCmd.formatServerList(servers));
|
|
215
|
+
} else if (sub === 'status') {
|
|
216
|
+
const mgr = createMcpManager({ getConfig, logger: (m) => writer.scrollback(` ⚠ ${m}`) });
|
|
217
|
+
try {
|
|
218
|
+
const status = await mgr.connectAll();
|
|
219
|
+
writer.scrollback(mcpCmd.formatStatus(status));
|
|
220
|
+
} finally {
|
|
221
|
+
await mgr.shutdown();
|
|
222
|
+
}
|
|
223
|
+
} else if (sub === 'add') {
|
|
224
|
+
try {
|
|
225
|
+
const { name, spec } = mcpCmd.parseAddArgs(argv);
|
|
226
|
+
setConfig(mcpCmd.withServerAdded(cfg, name, spec));
|
|
227
|
+
writer.scrollback(`✓ Added MCP server "${name}".`);
|
|
228
|
+
} catch (e) { writer.scrollback(`✗ ${e.message}`); }
|
|
229
|
+
} else if (sub === 'remove') {
|
|
230
|
+
try {
|
|
231
|
+
setConfig(mcpCmd.withServerRemoved(cfg, argv[0]));
|
|
232
|
+
try { require('../mcp/oauth').clearOAuth(argv[0]); } catch { /* ignore */ }
|
|
233
|
+
writer.scrollback(`✓ Removed MCP server "${argv[0]}".`);
|
|
234
|
+
} catch (e) { writer.scrollback(`✗ ${e.message}`); }
|
|
235
|
+
} else if (sub === 'auth') {
|
|
236
|
+
const name = argv[0];
|
|
237
|
+
const spec = servers[name];
|
|
238
|
+
if (!spec) { writer.scrollback(`✗ No such MCP server: ${name}`); }
|
|
239
|
+
else if (!spec.url) { writer.scrollback(`✗ "${name}" is a stdio server; OAuth applies to http/sse servers.`); }
|
|
240
|
+
else {
|
|
241
|
+
writer.scrollback(`Starting OAuth for "${name}" — follow the URL printed below to authorize…`);
|
|
242
|
+
const oneServer = { mcp: { servers: { [name]: { ...spec, oauth: true } } } };
|
|
243
|
+
const mgr = createMcpManager({ getConfig: () => oneServer, logger: (m) => writer.scrollback(` ⚠ ${m}`) });
|
|
244
|
+
try {
|
|
245
|
+
const status = await mgr.connectAll();
|
|
246
|
+
writer.scrollback(mcpCmd.formatStatus(status));
|
|
247
|
+
} finally {
|
|
248
|
+
await mgr.shutdown();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
writer.scrollback('Usage: semalt-code mcp <list|status|add|remove|auth>');
|
|
253
|
+
}
|
|
254
|
+
await writer.flush();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
cmdChat,
|
|
259
|
+
cmdMcp,
|
|
260
|
+
...oneshotCommands, // cmdCode, cmdEdit, cmdShell, cmdModels, cmdInit
|
|
261
|
+
...authCommands, // cmdLogin, cmdWhoAmI, cmdLogout, cmdAuthSetKey
|
|
262
|
+
...taskCommands, // cmdRun, cmdTasks (Task 5.3)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = {
|
|
267
|
+
createCommands,
|
|
268
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// MCP management commands (Task 3.3).
|
|
4
|
+
// ----------------------------------------------------------------------------
|
|
5
|
+
// Backs both the CLI `semalt-code mcp <list|add|remove|auth>` subcommands and
|
|
6
|
+
// the in-chat `/mcp` status view. Pure-ish: config mutation and status
|
|
7
|
+
// formatting are factored so they can be unit-tested, while the connect/auth
|
|
8
|
+
// side effects live in thin async wrappers.
|
|
9
|
+
//
|
|
10
|
+
// Config shape (under config.mcp.servers[name]):
|
|
11
|
+
// transport : 'stdio' | 'http' | 'sse' (default: stdio, or http if `url` set)
|
|
12
|
+
// command : string (stdio) args: string[] env: {} cwd: string
|
|
13
|
+
// url : string (http/sse) headers: {} oauth: bool
|
|
14
|
+
// allow : string[] | allowAll: bool — per-tool / whole-server approval opt-in
|
|
15
|
+
// disabled : bool
|
|
16
|
+
|
|
17
|
+
// One-line-per-server status, e.g. "● fs (stdio) — connected, 3 tools".
|
|
18
|
+
function formatStatus(status) {
|
|
19
|
+
if (!status || !status.length) {
|
|
20
|
+
return 'No MCP servers configured. Add one with `semalt-code mcp add <name> <command…>`.';
|
|
21
|
+
}
|
|
22
|
+
const glyph = { connected: '●', failed: '✗', disabled: '○', connecting: '◌' };
|
|
23
|
+
const lines = ['MCP servers:'];
|
|
24
|
+
for (const s of status) {
|
|
25
|
+
const g = glyph[s.state] || '?';
|
|
26
|
+
let line = ` ${g} ${s.name} (${s.transport}) — ${s.state}`;
|
|
27
|
+
if (s.state === 'connected') line += `, ${s.tools.length} tool${s.tools.length === 1 ? '' : 's'}`;
|
|
28
|
+
if (s.error) line += ` — ${s.error}`;
|
|
29
|
+
lines.push(line);
|
|
30
|
+
if (s.state === 'connected' && s.tools.length) {
|
|
31
|
+
lines.push(` ${s.tools.join(', ')}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// List servers straight from config (no connection), for a quick inventory.
|
|
38
|
+
function formatServerList(servers) {
|
|
39
|
+
const names = Object.keys(servers || {});
|
|
40
|
+
if (!names.length) return 'No MCP servers configured.';
|
|
41
|
+
const lines = ['Configured MCP servers:'];
|
|
42
|
+
for (const name of names) {
|
|
43
|
+
const s = servers[name] || {};
|
|
44
|
+
const transport = s.transport || (s.url ? 'http' : 'stdio');
|
|
45
|
+
const target = transport === 'stdio'
|
|
46
|
+
? `${s.command || '?'}${s.args && s.args.length ? ' ' + s.args.join(' ') : ''}`
|
|
47
|
+
: (s.url || '?');
|
|
48
|
+
const approval = s.allowAll ? ' [allowAll]' : (Array.isArray(s.allow) && s.allow.length ? ` [allow: ${s.allow.join(', ')}]` : '');
|
|
49
|
+
const flags = `${s.disabled ? ' [disabled]' : ''}${s.oauth ? ' [oauth]' : ''}${approval}`;
|
|
50
|
+
lines.push(` ${name} (${transport}) → ${target}${flags}`);
|
|
51
|
+
}
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Pure: produce the next config object with a server added. Throws on bad input.
|
|
56
|
+
function withServerAdded(cfg, name, spec) {
|
|
57
|
+
if (!name || /\s/.test(name)) throw new Error('server name must be a non-empty token with no spaces');
|
|
58
|
+
const next = { ...cfg, mcp: { servers: { ...((cfg.mcp && cfg.mcp.servers) || {}) } } };
|
|
59
|
+
if (next.mcp.servers[name]) throw new Error(`server "${name}" already exists (remove it first)`);
|
|
60
|
+
next.mcp.servers[name] = spec;
|
|
61
|
+
return next;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Pure: produce the next config object with a server removed. Throws if absent.
|
|
65
|
+
function withServerRemoved(cfg, name) {
|
|
66
|
+
const servers = { ...((cfg.mcp && cfg.mcp.servers) || {}) };
|
|
67
|
+
if (!servers[name]) throw new Error(`no such MCP server: ${name}`);
|
|
68
|
+
delete servers[name];
|
|
69
|
+
return { ...cfg, mcp: { servers } };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Parse `mcp add` argv into a server spec.
|
|
73
|
+
// stdio: mcp add <name> <command> [args...]
|
|
74
|
+
// remote: mcp add <name> --transport http --url <url> [--oauth] [--header K=V]...
|
|
75
|
+
// common: [--allow t1,t2 | --allow-all] [--env K=V]... [--cwd dir]
|
|
76
|
+
function parseAddArgs(argv) {
|
|
77
|
+
const a = argv.slice();
|
|
78
|
+
const name = a.shift();
|
|
79
|
+
const spec = {};
|
|
80
|
+
const env = {};
|
|
81
|
+
const headers = {};
|
|
82
|
+
const rest = [];
|
|
83
|
+
while (a.length) {
|
|
84
|
+
const tok = a.shift();
|
|
85
|
+
if (tok === '--transport') spec.transport = a.shift();
|
|
86
|
+
else if (tok === '--url') { spec.url = a.shift(); if (!spec.transport) spec.transport = 'http'; }
|
|
87
|
+
else if (tok === '--oauth') spec.oauth = true;
|
|
88
|
+
else if (tok === '--cwd') spec.cwd = a.shift();
|
|
89
|
+
else if (tok === '--allow-all') spec.allowAll = true;
|
|
90
|
+
else if (tok === '--allow') spec.allow = String(a.shift() || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
91
|
+
else if (tok === '--env') { const [k, ...v] = String(a.shift() || '').split('='); if (k) env[k] = v.join('='); }
|
|
92
|
+
else if (tok === '--header') { const [k, ...v] = String(a.shift() || '').split('='); if (k) headers[k] = v.join('='); }
|
|
93
|
+
else rest.push(tok);
|
|
94
|
+
}
|
|
95
|
+
if (Object.keys(env).length) spec.env = env;
|
|
96
|
+
if (Object.keys(headers).length) spec.headers = headers;
|
|
97
|
+
// Remaining positionals: command + args for stdio.
|
|
98
|
+
if (!spec.url) {
|
|
99
|
+
if (!rest.length) throw new Error('stdio server needs a command: mcp add <name> <command> [args...]');
|
|
100
|
+
spec.transport = spec.transport || 'stdio';
|
|
101
|
+
spec.command = rest.shift();
|
|
102
|
+
if (rest.length) spec.args = rest;
|
|
103
|
+
}
|
|
104
|
+
return { name, spec };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
formatStatus,
|
|
109
|
+
formatServerList,
|
|
110
|
+
withServerAdded,
|
|
111
|
+
withServerRemoved,
|
|
112
|
+
parseAddArgs,
|
|
113
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// One-shot / non-interactive CLI commands (code, edit, shell, models, init) and
|
|
4
|
+
// the dry-run summary, extracted from lib/commands.js in Task 1.5. Bodies are
|
|
5
|
+
// unchanged; collaborators come from the `shared` dependency bag.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const { resolveMaxIterations } = require('../config');
|
|
9
|
+
|
|
10
|
+
function createOneshotCommands(shared) {
|
|
11
|
+
const {
|
|
12
|
+
writer, getConfig, setConfig, runAgentLoop, readFileContext, agentExecShell,
|
|
13
|
+
chatStream, chatSync, dashboardListModels, dashboardGetModelForCli,
|
|
14
|
+
interactiveSelect, msgs, dbg,
|
|
15
|
+
ensureDefaultModel, confirmCatastrophicShell, printDryRunSummary,
|
|
16
|
+
DEFAULT_API_TIMEOUT_MS, CONFIG_PATH,
|
|
17
|
+
BOLD, BG_SELECTED, FG_CYAN, FG_DARK, FG_GRAY, FG_GREEN, FG_RED, FG_TEAL, FG_YELLOW, RST,
|
|
18
|
+
} = shared;
|
|
19
|
+
|
|
20
|
+
async function cmdCode(opts, promptArgs) {
|
|
21
|
+
if (!promptArgs.length) { writer.scrollback(` ${FG_RED}Usage: semalt-code code <prompt>${RST}`); return; }
|
|
22
|
+
if (!getConfig().auth_token) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Not logged in. Run semalt-code login first.${RST}\n`); return; }
|
|
23
|
+
await ensureDefaultModel();
|
|
24
|
+
// Fail-loud (memory truncation): if a loaded AGENTS.md/CLAUDE.md was cut at
|
|
25
|
+
// the cap, warn the user once — to STDERR so machine-mode stdout
|
|
26
|
+
// (json/stream-json) stays byte-pure. The memory content still loads; this
|
|
27
|
+
// only surfaces what was dropped, and never enters the model prompt.
|
|
28
|
+
try {
|
|
29
|
+
const { loadProjectMemory, memoryTruncationWarnings } = require('../memory');
|
|
30
|
+
for (const w of memoryTruncationWarnings(loadProjectMemory())) process.stderr.write(`${w}\n`);
|
|
31
|
+
} catch { /* best-effort; never block the run */ }
|
|
32
|
+
const model = opts.model || getConfig().default_model;
|
|
33
|
+
const userPrompt = promptArgs.join(' ');
|
|
34
|
+
const context = opts.file ? readFileContext(opts.file) : '';
|
|
35
|
+
const fullPrompt = context ? `Context files:\n${context}\n\nTask: ${userPrompt}` : userPrompt;
|
|
36
|
+
let resolvedSystemPrompt = null;
|
|
37
|
+
if (opts.systemPromptFile) {
|
|
38
|
+
try { resolvedSystemPrompt = fs.readFileSync(opts.systemPromptFile, 'utf8'); } catch {}
|
|
39
|
+
}
|
|
40
|
+
let messages = [{ role: 'user', content: fullPrompt }];
|
|
41
|
+
|
|
42
|
+
// Multimodal image input (Task 5.4). Read each --image through isPathSafe,
|
|
43
|
+
// size-check, base64-encode, and attach to the user turn. A clear error
|
|
44
|
+
// (unsafe path / oversize / unsupported format) aborts before any inference.
|
|
45
|
+
if (opts.image && opts.image.length) {
|
|
46
|
+
const { readImages, attachImagesToLastUser } = require('../images');
|
|
47
|
+
const { isPathSafe } = require('../tools');
|
|
48
|
+
try {
|
|
49
|
+
const imgs = readImages(opts.image, { maxBytes: getConfig().image_max_bytes, isPathSafe });
|
|
50
|
+
attachImagesToLastUser(messages, imgs);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
writer.scrollback(` ${FG_RED}✗ ${err.message}${RST}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Headless machine output (Task 2.4): json / stream-json suppress all chrome
|
|
58
|
+
// and print only the structured envelope. text (default) keeps human output.
|
|
59
|
+
const mode = opts.outputFormat || 'text';
|
|
60
|
+
const maxIter = resolveMaxIterations(getConfig().max_iterations);
|
|
61
|
+
if (mode === 'json' || mode === 'stream-json') {
|
|
62
|
+
const { runHeadless } = require('../headless');
|
|
63
|
+
await runHeadless({
|
|
64
|
+
runAgentLoop, messages, model, tokenLimit: null, mode, maxIterations: maxIter,
|
|
65
|
+
priceOverrides: getConfig().pricing || {},
|
|
66
|
+
agentOpts: {
|
|
67
|
+
debug: false,
|
|
68
|
+
systemPrompt: resolvedSystemPrompt,
|
|
69
|
+
systemPromptMode: getConfig().system_prompt_mode || 'system_role',
|
|
70
|
+
planMode: !!opts.plan,
|
|
71
|
+
noVerify: !!opts.noVerify,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
writer.scrollback(` ${FG_GRAY}◆ ${model}${RST}`);
|
|
78
|
+
const codeResult = await runAgentLoop(messages, model, maxIter, null, {
|
|
79
|
+
debug: dbg.isActive(),
|
|
80
|
+
systemPrompt: resolvedSystemPrompt,
|
|
81
|
+
systemPromptMode: getConfig().system_prompt_mode || 'system_role',
|
|
82
|
+
planMode: !!opts.plan,
|
|
83
|
+
noVerify: !!opts.noVerify,
|
|
84
|
+
});
|
|
85
|
+
messages = codeResult.messages;
|
|
86
|
+
writer.scrollback('\n');
|
|
87
|
+
if (codeResult.metrics) writer.scrollback(codeResult.metrics.summary());
|
|
88
|
+
if (opts.dryRun) printDryRunSummary();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function cmdEdit(opts, filePath, instructionArgs) {
|
|
92
|
+
if (!filePath) { writer.scrollback(` ${FG_RED}Usage: semalt-code edit <file> <instruction>${RST}`); return; }
|
|
93
|
+
if (!getConfig().auth_token) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Not logged in. Run semalt-code login first.${RST}\n`); return; }
|
|
94
|
+
if (!fs.existsSync(filePath)) { writer.scrollback(` ${FG_RED}✗ File not found: ${filePath}${RST}`); return; }
|
|
95
|
+
await ensureDefaultModel();
|
|
96
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
97
|
+
const instruction = instructionArgs.join(' ');
|
|
98
|
+
const messages = [
|
|
99
|
+
{ role: 'system', content: 'You are Semalt.AI. Output ONLY the modified file. No explanations, no fences.' },
|
|
100
|
+
{ role: 'user', content: `File: ${filePath}\n\n\`\`\`\n${content}\n\`\`\`\n\nInstruction: ${instruction}` },
|
|
101
|
+
];
|
|
102
|
+
writer.scrollback(` ${FG_GRAY}Editing ${filePath}...${RST}`);
|
|
103
|
+
let result = await chatSync(messages, { model: opts.model });
|
|
104
|
+
if (result && !opts.dryRun) {
|
|
105
|
+
if (result.startsWith('```')) { const lines = result.split('\n'); result = lines.at(-1).trim() === '```' ? lines.slice(1, -1).join('\n') : lines.slice(1).join('\n'); }
|
|
106
|
+
fs.writeFileSync(filePath, result);
|
|
107
|
+
writer.scrollback(` ${FG_GREEN}✓ Saved: ${filePath}${RST}`);
|
|
108
|
+
} else if (opts.dryRun) {
|
|
109
|
+
writer.scrollback(` ${FG_YELLOW}⚠ Dry run — not modified${RST}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function cmdShell(opts, commandArgs) {
|
|
114
|
+
const command = commandArgs.join(' ');
|
|
115
|
+
if (!command) { writer.scrollback(` ${FG_RED}Usage: semalt-code shell <command>${RST}`); return; }
|
|
116
|
+
const result = await agentExecShell(command, {
|
|
117
|
+
initiator: 'user',
|
|
118
|
+
confirm: (label, cmd) => {
|
|
119
|
+
writer.scrollback(`\n ${FG_YELLOW}${BOLD}⚠ Catastrophic command (${label})${RST}\n ${FG_GRAY}${cmd}${RST}`);
|
|
120
|
+
return confirmCatastrophicShell(label, cmd);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
if (opts.analyze) {
|
|
124
|
+
if (!getConfig().auth_token) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Not logged in. Run semalt-code login first.${RST}\n`); return; }
|
|
125
|
+
await ensureDefaultModel();
|
|
126
|
+
const messages = [
|
|
127
|
+
{ role: 'system', content: 'You are Semalt.AI. Analyze the command output concisely.' },
|
|
128
|
+
{ role: 'user', content: `Command: ${command}\nExit: ${result.exit_code}\nStdout:\n${result.stdout}\nStderr:\n${result.stderr}` },
|
|
129
|
+
];
|
|
130
|
+
writer.scrollback(`\n ${FG_TEAL}${BOLD}◆ Semalt.AI${RST}\n`);
|
|
131
|
+
// audit: allowed — non-TUI streaming prefix, must precede StreamRenderer sync writes.
|
|
132
|
+
process.stdout.write(' ');
|
|
133
|
+
try {
|
|
134
|
+
await chatStream(messages, { model: opts.model });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
msgs.netError(err.message);
|
|
137
|
+
}
|
|
138
|
+
writer.scrollback('\n');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function cmdModels() {
|
|
143
|
+
const config = getConfig();
|
|
144
|
+
let response;
|
|
145
|
+
try { response = await dashboardListModels(); }
|
|
146
|
+
catch (err) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}${err.message}${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
147
|
+
const models = Array.isArray(response && response.models) ? response.models : [];
|
|
148
|
+
if (!models.length) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}No models available.${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
149
|
+
writer.scrollback(`\n ${FG_TEAL}${BOLD}◆ Your Models${RST}\n ${FG_DARK}${'─'.repeat(60)}${RST}`);
|
|
150
|
+
const activeIndex = models.findIndex((m) => m.base_url === config.api_base && m.model_id === config.default_model);
|
|
151
|
+
const selectedIndex = await interactiveSelect(models, (model, isSelected, isFinal) => {
|
|
152
|
+
const active = model.base_url === config.api_base && model.model_id === config.default_model;
|
|
153
|
+
const marker = active ? `${FG_GREEN}●${RST}` : `${FG_DARK}○${RST}`;
|
|
154
|
+
const cursor = isSelected ? `${FG_TEAL}❯${RST}` : ' ';
|
|
155
|
+
const nameStyle = isSelected && !isFinal ? `${BG_SELECTED}${FG_CYAN}` : (isSelected ? FG_CYAN : FG_GRAY);
|
|
156
|
+
return ` ${marker} ${cursor} ${nameStyle}${model.name} · ${model.model_id} @ ${model.base_url}${RST}`;
|
|
157
|
+
}, { initialIndex: Math.max(0, activeIndex) });
|
|
158
|
+
if (selectedIndex === null) { writer.scrollback(` ${FG_DARK}Cancelled${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
159
|
+
const selectedModel = models[selectedIndex];
|
|
160
|
+
let credentialsResponse;
|
|
161
|
+
try { credentialsResponse = await dashboardGetModelForCli(selectedModel.id); }
|
|
162
|
+
catch (err) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}${err.message}${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
163
|
+
const model = credentialsResponse && credentialsResponse.model ? credentialsResponse.model : null;
|
|
164
|
+
if (!model) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Unable to load selected model.${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
165
|
+
const contextLength = (Number.isInteger(model.context_length) && model.context_length > 0 ? model.context_length : null) || (Number.isInteger(model.max_tokens) && model.max_tokens > 0 ? model.max_tokens : null);
|
|
166
|
+
const updatedConfig = { ...config, api_base: model.base_url, api_key: model.api_key, default_model: model.model_id, dashboard_model_id: model.id };
|
|
167
|
+
if (contextLength !== null) updatedConfig.context_length = contextLength;
|
|
168
|
+
setConfig(updatedConfig);
|
|
169
|
+
writer.scrollback(` ${FG_GREEN}✓${RST} ${FG_GRAY}Current model → ${model.name} (${model.model_id})${RST}\n`);
|
|
170
|
+
return { model: model.model_id, dbId: model.id };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function cmdInit(opts) {
|
|
174
|
+
const current = getConfig();
|
|
175
|
+
const cfg = {
|
|
176
|
+
api_base: opts.apiBase || 'http://127.0.0.1:8800',
|
|
177
|
+
api_key: opts.apiKey || 'any',
|
|
178
|
+
dashboard_url: opts.dashboardUrl || current.dashboard_url,
|
|
179
|
+
auth_token: current.auth_token || '',
|
|
180
|
+
default_model: opts.defaultModel || '',
|
|
181
|
+
temperature: 0.7,
|
|
182
|
+
request_timeout_ms: DEFAULT_API_TIMEOUT_MS,
|
|
183
|
+
stream: true,
|
|
184
|
+
models: current.models,
|
|
185
|
+
};
|
|
186
|
+
setConfig(cfg);
|
|
187
|
+
writer.scrollback(`\n ${FG_GREEN}✓${RST} Config saved to ${CONFIG_PATH}\n ${FG_GRAY}${JSON.stringify(cfg, null, 2)}${RST}\n`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { cmdCode, cmdEdit, cmdShell, cmdModels, cmdInit };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { createOneshotCommands };
|