agentlas 0.4.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/LICENSE +201 -0
- package/README.md +107 -0
- package/bin/agentlas.cjs +176 -0
- package/engine/ENGINE_META.json +8 -0
- package/engine/agentlas-api-agent.cjs +451 -0
- package/engine/agentlas-banner.cjs +108 -0
- package/engine/agentlas-capabilities.cjs +72 -0
- package/engine/agentlas-cloud-runtime.cjs +199 -0
- package/engine/agentlas-composer.cjs +256 -0
- package/engine/agentlas-config.cjs +29 -0
- package/engine/agentlas-doctor.cjs +173 -0
- package/engine/agentlas-i18n.cjs +317 -0
- package/engine/agentlas-input.cjs +437 -0
- package/engine/agentlas-native-host.cjs +612 -0
- package/engine/agentlas-onboard.cjs +71 -0
- package/engine/agentlas-parity.cjs +994 -0
- package/engine/agentlas-repl.cjs +1258 -0
- package/engine/agentlas-style.cjs +100 -0
- package/engine/agentlas-tools.cjs +196 -0
- package/engine/agentlas-ui.cjs +266 -0
- package/engine/agentlas.cjs +7503 -0
- package/engine/architecture.data.json +139 -0
- package/engine/bootstrap-schema.sql +568 -0
- package/install.ps1 +26 -0
- package/install.sh +53 -0
- package/package.json +49 -0
- package/scripts/gen-bootstrap-schema.sh +23 -0
- package/test/smoke.sh +56 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* agentlas-input: terminal input ergonomics for the REPL.
|
|
4
|
+
* - persistent command history (load/save across sessions, per machine)
|
|
5
|
+
* - tab autocomplete (slash commands, agent/firm slugs, runtime kinds, perm levels, @paths, /cwd /import paths)
|
|
6
|
+
* - multiline composer (trailing backslash continues the line)
|
|
7
|
+
* Self-contained, zero-dependency, TTY-aware. Pure functions are unit-testable under plain node.
|
|
8
|
+
* (Ctrl-R reverse-i-search needs TTY keypress + rl internals → tracked separately; /history bridges it.)
|
|
9
|
+
*/
|
|
10
|
+
const fs = require("node:fs");
|
|
11
|
+
const os = require("node:os");
|
|
12
|
+
const path = require("node:path");
|
|
13
|
+
const readline = require("node:readline");
|
|
14
|
+
|
|
15
|
+
function userDataDir() {
|
|
16
|
+
const override = process.env.AGENTLAS_USER_DATA_DIR;
|
|
17
|
+
if (override) return override;
|
|
18
|
+
const home = os.homedir();
|
|
19
|
+
if (process.platform === "darwin") return path.join(home, "Library", "Application Support", "Agentlas");
|
|
20
|
+
if (process.platform === "win32") return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "Agentlas");
|
|
21
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(home, ".config"), "Agentlas");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const HISTORY_MAX = 500;
|
|
25
|
+
function historyPath() {
|
|
26
|
+
return path.join(userDataDir(), "cli-history.json");
|
|
27
|
+
}
|
|
28
|
+
// readline keeps history with index 0 = most-recent. We persist that array verbatim.
|
|
29
|
+
function loadHistory() {
|
|
30
|
+
try {
|
|
31
|
+
const a = JSON.parse(fs.readFileSync(historyPath(), "utf8"));
|
|
32
|
+
return Array.isArray(a) ? a.filter((x) => typeof x === "string").slice(0, HISTORY_MAX) : [];
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function saveHistory(list) {
|
|
38
|
+
try {
|
|
39
|
+
fs.mkdirSync(userDataDir(), { recursive: true });
|
|
40
|
+
const clean = (list || []).filter((x) => typeof x === "string" && x.trim()).slice(0, HISTORY_MAX);
|
|
41
|
+
fs.writeFileSync(historyPath(), JSON.stringify(clean), "utf8");
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Seed an interactive readline with saved history (no-op on non-TTY).
|
|
48
|
+
function attachHistory(rl) {
|
|
49
|
+
try {
|
|
50
|
+
if (rl && rl.terminal && Array.isArray(rl.history)) rl.history = loadHistory();
|
|
51
|
+
} catch {
|
|
52
|
+
/* ignore */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function persistHistory(rl) {
|
|
56
|
+
try {
|
|
57
|
+
if (rl && Array.isArray(rl.history)) saveHistory(rl.history);
|
|
58
|
+
} catch {
|
|
59
|
+
/* ignore */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── multiline ─────────────────────────────────────────────
|
|
64
|
+
// A line ending in an odd number of trailing backslashes is a continuation.
|
|
65
|
+
function isContinuation(line) {
|
|
66
|
+
const m = /\\+$/.exec(line || "");
|
|
67
|
+
return !!m && m[0].length % 2 === 1;
|
|
68
|
+
}
|
|
69
|
+
function stripContinuation(line) {
|
|
70
|
+
return (line || "").replace(/\\$/, "");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── completion ────────────────────────────────────────────
|
|
74
|
+
const SLASH_COMMAND_META = [
|
|
75
|
+
{ command: "/help", description: "Show Agentlas terminal commands", category: "Help", usage: "/help", detail: "Open the command reference, shortcuts, and common flows." },
|
|
76
|
+
{ command: "/status", description: "Show model/runtime, agent, permission, and directory", category: "Session", usage: "/status", detail: "Print the current runtime, active agent or company, permission level, and cwd." },
|
|
77
|
+
{ command: "/skills", description: "List available Agentlas terminal skills", category: "Discovery", usage: "/skills", detail: "Show the slash-command skills Agentlas can run inside this terminal." },
|
|
78
|
+
{ command: "/ontology", description: "Turn on, list, or add project ontology sources", category: "Knowledge", usage: "/ontology add ./docs", detail: "Also understands natural text like /ontology use ./docs as company knowledge.", examples: ["/ontology list", "/ontology use ./docs as company knowledge", "/ontology open"] },
|
|
79
|
+
{ command: "/agents", description: "List installed agents", category: "Routing", usage: "/agents", detail: "Show local agents and their routed runtime." },
|
|
80
|
+
{ command: "/team", description: "View or pin each agent runtime", category: "Routing", usage: "/team <agent> <runtime|auto>", detail: "Pin one agent to claude-code, codex, gemini, or automatic routing." },
|
|
81
|
+
{ command: "/agent", description: "Switch to another agent", category: "Routing", usage: "/agent <name>", detail: "Switch the current conversation to an installed agent." },
|
|
82
|
+
{ command: "/firms", description: "List installed companies", category: "Routing", usage: "/firms", detail: "Show company CEOs available in this terminal." },
|
|
83
|
+
{ command: "/firm", description: "Switch to a company CEO", category: "Routing", usage: "/firm <name>", detail: "Switch the current conversation to a company CEO agent." },
|
|
84
|
+
{ command: "/runtime", description: "Switch runtime: claude-code, codex, gemini, BYOK, or Ollama", category: "Settings", usage: "/runtime codex", detail: "Change the engine Agentlas uses for subsequent turns." },
|
|
85
|
+
{ command: "/model", description: "Set the model for the current runtime", category: "Settings", usage: "/model <id>", detail: "Works for claude/codex/gemini (alias like sonnet/opus, or full id) and BYOK/Ollama." },
|
|
86
|
+
{ command: "/effort", description: "Set reasoning effort (low/medium/high/max)", category: "Settings", usage: "/effort high", detail: "Higher effort = deeper reasoning. Maps to codex model_reasoning_effort and claude think-depth." },
|
|
87
|
+
{ command: "/permission", description: "Set read/write/full permission", category: "Settings", usage: "/permission full", detail: "No argument shows what read, write, and full mean.", aliases: ["/perm"] },
|
|
88
|
+
{ command: "/permissions", description: "Show or set current permission", category: "Settings", usage: "/permissions", detail: "Codex-style permission screen for Agentlas read/write/full." },
|
|
89
|
+
{ command: "/setup", description: "Run first-time setup again", category: "Settings", usage: "/setup", detail: "Re-run language, runtime, and default permission setup in-place." },
|
|
90
|
+
{ command: "/cwd", description: "Show or change the working folder", category: "Files", usage: "/cwd <path>", detail: "Change the folder used for tools, file mentions, and local commands." },
|
|
91
|
+
{ command: "/memory", description: "Show the memory injected into this run", category: "Context", usage: "/memory", detail: "Print the project memory that Agentlas adds to agent turns." },
|
|
92
|
+
{ command: "/side", description: "Ask a side question without saving it to chat context", category: "Context", usage: "/side <question>", detail: "Runs a one-off answer using current context, then returns without appending to chat history.", aliases: ["/btw"] },
|
|
93
|
+
{ command: "/multimodal", description: "Show or set image, video, and audio fallback providers", category: "Settings", usage: "/multimodal", detail: "Inspect or change fallback providers for media work." },
|
|
94
|
+
{ command: "/mcp", description: "List configured MCP servers", category: "Settings", usage: "/mcp", detail: "Show MCP servers and which enabled stdio servers the terminal wires into write/full turns." },
|
|
95
|
+
{ command: "/diff", description: "Show the current git diff", category: "Files", usage: "/diff", detail: "Print the working-tree diff for the current cwd." },
|
|
96
|
+
{ command: "/history", description: "Show recent inputs", category: "Session", usage: "/history", detail: "Show persisted terminal input history." },
|
|
97
|
+
{ command: "/resume", description: "Resume a recent runtime session", category: "Session", usage: "/resume [n]", detail: "List recent agent/runtime sessions and continue one (restores the native session thread)." },
|
|
98
|
+
{ command: "/compact", description: "Drop older transcript turns and keep recent context", category: "Context", usage: "/compact", detail: "Keep the newest conversation turns and discard older in-session context." },
|
|
99
|
+
{ command: "/cost", description: "Show session usage and cost by runtime", category: "Session", usage: "/cost", detail: "Show usage captured by Agentlas across routed runtimes." },
|
|
100
|
+
{ command: "/keybindings", description: "Show terminal shortcuts", category: "Help", usage: "/keybindings", detail: "Show slash, file mention, shell, multiline, history, and Ctrl-C controls." },
|
|
101
|
+
{ command: "/clear", description: "Clear the chat and redraw", category: "Session", usage: "/clear", detail: "Clear local conversation state and redraw the Agentlas banner." },
|
|
102
|
+
{ command: "/import", description: "Import a local agent or team folder", category: "Files", usage: "/import <path>", detail: "Install a local agent or team into Agentlas." },
|
|
103
|
+
{ command: "/marketplace", description: "Browse/install marketplace agents", category: "Routing", usage: "/marketplace", detail: "Show how to install agents from the Agentlas cloud marketplace or a local folder.", aliases: ["/market"] },
|
|
104
|
+
{ command: "/install", description: "Install a cloud agent by slug", category: "Routing", usage: "/install <slug>", detail: "Download and install an agent from the Agentlas cloud marketplace by slug." },
|
|
105
|
+
{ command: "/storm", description: "Run a force-robust Stormbreaker pipeline on a goal", category: "Engine", usage: "/storm <goal> [--research]", detail: "Route the goal through Hephaestus Stormbreaker and execute the verified pipeline; --research grounds it with Research Engine evidence." },
|
|
106
|
+
{ command: "/swarm", description: "Fan out an emergent agent swarm on a goal", category: "Engine", usage: "/swarm <goal> [--parallel N]", detail: "Parallel workers share a blackboard and spawn subtasks with ## Spawn; a synthesizer merges results into one answer." },
|
|
107
|
+
{ command: "/build", description: "Build/repair/package an agent or team (Hephaestus)", category: "Engine", usage: "/build <what to build>", detail: "Runs Hephaestus hep-build natively — deep interview, scaffolding, packaging." },
|
|
108
|
+
{ command: "/route", description: "Preview which agent/pipeline would take a request", category: "Engine", usage: "/route <request>", detail: "Runs the Hephaestus router without executing — shows the selected agent, candidates, and reasons." },
|
|
109
|
+
{ command: "/research", description: "Run the Hephaestus Research Engine", category: "Engine", usage: "/research search \"query\"", detail: "status|gather|search|read|plan — evidence-grade web research from the terminal." },
|
|
110
|
+
{ command: "/doctor", description: "Check runtimes and local data", category: "Health", usage: "/doctor", detail: "Run local checks for runtimes, data, credentials, and setup." },
|
|
111
|
+
{ command: "/exit", description: "Quit Agentlas", category: "Session", usage: "/exit", detail: "Close the terminal session.", aliases: ["/quit"] },
|
|
112
|
+
];
|
|
113
|
+
const SLASH_COMMANDS = SLASH_COMMAND_META.flatMap((entry) => [entry.command].concat(entry.aliases || []));
|
|
114
|
+
const RUNTIME_SPECS = ["claude-code", "codex", "gemini", "anthropic", "openai", "google", "ollama", "upstage"];
|
|
115
|
+
const PERM_LEVELS = ["read", "write", "full"];
|
|
116
|
+
|
|
117
|
+
function uniqStartsWith(cands, token) {
|
|
118
|
+
const hits = cands.filter((c) => c.startsWith(token));
|
|
119
|
+
return hits.length ? hits : cands;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function slashCommandEntries() {
|
|
123
|
+
const rows = [];
|
|
124
|
+
for (const entry of SLASH_COMMAND_META) {
|
|
125
|
+
rows.push({ ...entry, aliasOf: null });
|
|
126
|
+
for (const alias of entry.aliases || []) {
|
|
127
|
+
rows.push({
|
|
128
|
+
command: alias,
|
|
129
|
+
description: `Alias for ${entry.command}`,
|
|
130
|
+
category: entry.category,
|
|
131
|
+
usage: alias + (entry.usage && entry.usage.includes(" ") ? entry.usage.slice(entry.usage.indexOf(" ")) : ""),
|
|
132
|
+
detail: entry.detail,
|
|
133
|
+
examples: entry.examples,
|
|
134
|
+
aliasOf: entry.command,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return rows;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function slashCommandQuery(line) {
|
|
142
|
+
const value = String(line || "");
|
|
143
|
+
if (!value.startsWith("/")) return null;
|
|
144
|
+
if (isAbsolutePathTask(value)) return null;
|
|
145
|
+
if (/\s/.test(value)) return null;
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function slashCommandSuggestions(line, limit = 12) {
|
|
150
|
+
const query = slashCommandQuery(line);
|
|
151
|
+
if (query == null) return [];
|
|
152
|
+
const q = query.toLowerCase();
|
|
153
|
+
const entries = slashCommandEntries();
|
|
154
|
+
const starts = entries.filter((entry) => entry.command.toLowerCase().startsWith(q));
|
|
155
|
+
const contains = entries.filter(
|
|
156
|
+
(entry) =>
|
|
157
|
+
!entry.command.toLowerCase().startsWith(q) &&
|
|
158
|
+
(entry.command.toLowerCase().includes(q.slice(1)) || entry.description.toLowerCase().includes(q.slice(1))),
|
|
159
|
+
);
|
|
160
|
+
return (starts.length ? starts.concat(contains) : entries).slice(0, limit);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function padVisible(value, width) {
|
|
164
|
+
const clean = stripAnsiLite(value);
|
|
165
|
+
if (clean.length >= width) return value;
|
|
166
|
+
return value + " ".repeat(width - clean.length);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function stripAnsiLite(value) {
|
|
170
|
+
// eslint-disable-next-line no-control-regex
|
|
171
|
+
return String(value || "").replace(/\x1b\[[0-9;]*m/g, "");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function truncateVisible(value, width) {
|
|
175
|
+
const clean = stripAnsiLite(value);
|
|
176
|
+
if (clean.length <= width) return value;
|
|
177
|
+
return clean.slice(0, Math.max(0, width - 1)) + "…";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function renderSlashPalette(rows, selectedIndex, opts = {}) {
|
|
181
|
+
if (!rows.length) return "";
|
|
182
|
+
const columns = Math.max(48, Number(opts.columns || 88));
|
|
183
|
+
const fallbackColors = {
|
|
184
|
+
faint: (s) => String(s),
|
|
185
|
+
dim: (s) => String(s),
|
|
186
|
+
text: (s) => String(s),
|
|
187
|
+
blue: (s) => String(s),
|
|
188
|
+
inverse: (s) => String(s),
|
|
189
|
+
};
|
|
190
|
+
const c = { ...fallbackColors, ...(opts.colors || {}) };
|
|
191
|
+
const commandWidth = Math.min(24, Math.max(16, rows.reduce((n, row) => Math.max(n, row.command.length), 0) + 2));
|
|
192
|
+
const descWidth = Math.max(12, columns - commandWidth - 8);
|
|
193
|
+
const lineWidth = Math.min(columns - 1, commandWidth + descWidth + 5);
|
|
194
|
+
const selected = rows[Math.max(0, Math.min(selectedIndex, rows.length - 1))] || rows[0];
|
|
195
|
+
const out = [
|
|
196
|
+
c.faint("Slash commands") + c.dim(" type to search"),
|
|
197
|
+
c.faint("─".repeat(lineWidth)),
|
|
198
|
+
];
|
|
199
|
+
rows.forEach((row, index) => {
|
|
200
|
+
const command = padVisible(row.command, commandWidth);
|
|
201
|
+
const desc = truncateVisible(row.description, descWidth);
|
|
202
|
+
const body = " " + c.blue(command) + c.text(desc);
|
|
203
|
+
out.push(index === selectedIndex ? c.inverse(body.padEnd(lineWidth)) : body);
|
|
204
|
+
});
|
|
205
|
+
out.push(c.faint("─".repeat(lineWidth)));
|
|
206
|
+
if (selected) {
|
|
207
|
+
const usage = truncateVisible(selected.usage || selected.command, lineWidth - 2);
|
|
208
|
+
const detail = truncateVisible(selected.detail || selected.description || "", lineWidth - 2);
|
|
209
|
+
const category = selected.category ? `category: ${selected.category}` : "";
|
|
210
|
+
out.push(" " + c.text(usage) + (category ? c.dim(" " + category) : ""));
|
|
211
|
+
if (detail) out.push(" " + c.dim(detail));
|
|
212
|
+
if (selected.examples && selected.examples.length) {
|
|
213
|
+
out.push(" " + c.dim("examples: " + selected.examples.slice(0, 2).join(" | ")));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
out.push(c.dim(" ↑↓ move Enter run Tab complete Esc close"));
|
|
217
|
+
return out.join("\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function attachSlashPalette(rl, opts = {}) {
|
|
221
|
+
const stream = opts.stream || rl.output || process.stdout;
|
|
222
|
+
const inputStream = rl.input || process.stdin;
|
|
223
|
+
const paletteEnabled = opts.force || process.env.AGENTLAS_SLASH_PALETTE === "1";
|
|
224
|
+
const isTty = paletteEnabled && Boolean(rl.terminal && inputStream.isTTY && stream.isTTY);
|
|
225
|
+
if (!rl || !inputStream || !stream || !isTty) {
|
|
226
|
+
return { clear() {}, detach() {}, setEnabled() {}, active: () => false };
|
|
227
|
+
}
|
|
228
|
+
const colors = opts.colors || (opts.ui && opts.ui.c) || {};
|
|
229
|
+
const state = {
|
|
230
|
+
enabled: true,
|
|
231
|
+
selected: 0,
|
|
232
|
+
selectedCommand: null,
|
|
233
|
+
visible: false,
|
|
234
|
+
dismissedForLine: null,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
readline.emitKeypressEvents(inputStream, rl);
|
|
238
|
+
|
|
239
|
+
function rows() {
|
|
240
|
+
if (!state.enabled) return [];
|
|
241
|
+
return slashCommandSuggestions(rl.line || "");
|
|
242
|
+
}
|
|
243
|
+
function active() {
|
|
244
|
+
return rows().length > 0 && state.dismissedForLine !== (rl.line || "");
|
|
245
|
+
}
|
|
246
|
+
function replaceLine(value) {
|
|
247
|
+
rl.write(null, { ctrl: true, name: "u" });
|
|
248
|
+
rl.write(value);
|
|
249
|
+
}
|
|
250
|
+
function clear() {
|
|
251
|
+
if (!state.visible) return;
|
|
252
|
+
stream.write("\x1b7\x1b[E\x1b[0J\x1b8");
|
|
253
|
+
state.visible = false;
|
|
254
|
+
}
|
|
255
|
+
function render() {
|
|
256
|
+
if (!state.enabled) {
|
|
257
|
+
clear();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const list = rows();
|
|
261
|
+
if (!list.length || state.dismissedForLine === (rl.line || "")) {
|
|
262
|
+
clear();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const selectedByCommand = state.selectedCommand
|
|
266
|
+
? list.findIndex((entry) => entry.command === state.selectedCommand)
|
|
267
|
+
: -1;
|
|
268
|
+
if (selectedByCommand >= 0) state.selected = selectedByCommand;
|
|
269
|
+
if (state.selected < 0 || state.selected >= list.length) state.selected = 0;
|
|
270
|
+
const body = renderSlashPalette(list, state.selected, {
|
|
271
|
+
columns: stream.columns || process.stdout.columns || 88,
|
|
272
|
+
colors,
|
|
273
|
+
});
|
|
274
|
+
stream.write("\x1b7\x1b[E\x1b[0J" + body + "\x1b8");
|
|
275
|
+
state.visible = true;
|
|
276
|
+
}
|
|
277
|
+
function move(delta) {
|
|
278
|
+
const list = rows();
|
|
279
|
+
if (!list.length) return false;
|
|
280
|
+
state.selected = (state.selected + delta + list.length) % list.length;
|
|
281
|
+
state.selectedCommand = list[state.selected].command;
|
|
282
|
+
setImmediate(() => {
|
|
283
|
+
replaceLine(state.selectedCommand);
|
|
284
|
+
render();
|
|
285
|
+
});
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
function select() {
|
|
289
|
+
const list = rows();
|
|
290
|
+
if (!list.length) return false;
|
|
291
|
+
if (state.selected < 0 || state.selected >= list.length) state.selected = 0;
|
|
292
|
+
state.selectedCommand = list[state.selected].command;
|
|
293
|
+
replaceLine(state.selectedCommand);
|
|
294
|
+
clear();
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
function onKeypress(_str, key = {}) {
|
|
298
|
+
if (!state.enabled) return;
|
|
299
|
+
const name = key.name || "";
|
|
300
|
+
if (name === "escape" && state.visible) {
|
|
301
|
+
state.dismissedForLine = rl.line || "";
|
|
302
|
+
clear();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (active() && (name === "down" || name === "up")) {
|
|
306
|
+
move(name === "down" ? 1 : -1);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (active() && (name === "tab" || name === "return")) {
|
|
310
|
+
select();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
state.dismissedForLine = null;
|
|
314
|
+
setImmediate(render);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
inputStream.prependListener("keypress", onKeypress);
|
|
318
|
+
rl.on("line", clear);
|
|
319
|
+
rl.on("close", clear);
|
|
320
|
+
setImmediate(render);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
active,
|
|
324
|
+
clear,
|
|
325
|
+
setEnabled(value) {
|
|
326
|
+
state.enabled = Boolean(value);
|
|
327
|
+
if (!state.enabled) clear();
|
|
328
|
+
},
|
|
329
|
+
detach() {
|
|
330
|
+
inputStream.removeListener("keypress", onKeypress);
|
|
331
|
+
clear();
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// List filesystem entries under the partial path `token` relative to `cwd`.
|
|
337
|
+
// Returns candidates in the SAME shape as the token (so readline substitutes the last word).
|
|
338
|
+
function completePath(token, cwd, prefixChar) {
|
|
339
|
+
let p = token;
|
|
340
|
+
const lead = prefixChar || "";
|
|
341
|
+
try {
|
|
342
|
+
const hasSlash = p.includes("/");
|
|
343
|
+
const dirPart = hasSlash ? p.slice(0, p.lastIndexOf("/") + 1) : "";
|
|
344
|
+
const basePart = hasSlash ? p.slice(p.lastIndexOf("/") + 1) : p;
|
|
345
|
+
const absDir = path.resolve(cwd || ".", dirPart || ".");
|
|
346
|
+
const entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
347
|
+
const hits = entries
|
|
348
|
+
.filter((e) => e.name.startsWith(basePart) && !e.name.startsWith("."))
|
|
349
|
+
.slice(0, 100)
|
|
350
|
+
.map((e) => lead + dirPart + e.name + (e.isDirectory() ? "/" : ""))
|
|
351
|
+
.sort();
|
|
352
|
+
return hits;
|
|
353
|
+
} catch {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function isAbsolutePathTask(line) {
|
|
359
|
+
const value = String(line || "").trim();
|
|
360
|
+
if (!value.startsWith("/")) return false;
|
|
361
|
+
const first = value.split(/\s+/)[0] || "";
|
|
362
|
+
if (!first || SLASH_COMMANDS.includes(first)) return false;
|
|
363
|
+
if (!path.isAbsolute(first)) return false;
|
|
364
|
+
if (fs.existsSync(first)) return true;
|
|
365
|
+
const parts = first.split("/").filter(Boolean);
|
|
366
|
+
if (parts.length >= 2) return true;
|
|
367
|
+
return /^(?:\/Users|\/Volumes|\/Applications|\/tmp|\/private|\/var|\/opt|\/home)(?:\/|$)/.test(first);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// makeCompleter({ getAgentSlugs, getFirmSlugs, getCwd }) → readline completer(line) → [hits, token]
|
|
371
|
+
function makeCompleter(ctx) {
|
|
372
|
+
const getAgents = ctx.getAgentSlugs || (() => []);
|
|
373
|
+
const getFirms = ctx.getFirmSlugs || (() => []);
|
|
374
|
+
const getCwd = ctx.getCwd || (() => process.cwd());
|
|
375
|
+
return function completer(line) {
|
|
376
|
+
const lineStr = line || "";
|
|
377
|
+
const tokens = lineStr.split(/\s+/);
|
|
378
|
+
const last = tokens[tokens.length - 1] || "";
|
|
379
|
+
|
|
380
|
+
// @file mention anywhere in the last token
|
|
381
|
+
if (last.startsWith("@")) {
|
|
382
|
+
return [completePath(last.slice(1), getCwd(), "@"), last];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// first token = the command itself
|
|
386
|
+
if (tokens.length === 1) {
|
|
387
|
+
if (isAbsolutePathTask(lineStr)) return [completePath(lineStr, getCwd(), ""), last];
|
|
388
|
+
if (lineStr.startsWith("/")) return [uniqStartsWith(SLASH_COMMANDS, last), last];
|
|
389
|
+
return [[], last]; // free-text prompt — no completion
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const cmd = tokens[0];
|
|
393
|
+
switch (cmd) {
|
|
394
|
+
case "/runtime":
|
|
395
|
+
return [uniqStartsWith(RUNTIME_SPECS, last), last];
|
|
396
|
+
case "/permission":
|
|
397
|
+
case "/perm":
|
|
398
|
+
case "/permissions":
|
|
399
|
+
return [uniqStartsWith(PERM_LEVELS, last), last];
|
|
400
|
+
case "/agent":
|
|
401
|
+
return [uniqStartsWith(getAgents(), last), last];
|
|
402
|
+
case "/firm":
|
|
403
|
+
return [uniqStartsWith(getFirms(), last), last];
|
|
404
|
+
case "/team":
|
|
405
|
+
if (tokens.length === 2) return [uniqStartsWith(getAgents(), last), last];
|
|
406
|
+
return [uniqStartsWith(RUNTIME_SPECS.concat(["auto"]), last), last];
|
|
407
|
+
case "/cwd":
|
|
408
|
+
case "/import":
|
|
409
|
+
case "/ontology":
|
|
410
|
+
return [completePath(last, getCwd(), ""), last];
|
|
411
|
+
default:
|
|
412
|
+
return [[], last];
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
userDataDir,
|
|
419
|
+
historyPath,
|
|
420
|
+
loadHistory,
|
|
421
|
+
saveHistory,
|
|
422
|
+
attachHistory,
|
|
423
|
+
persistHistory,
|
|
424
|
+
attachSlashPalette,
|
|
425
|
+
isContinuation,
|
|
426
|
+
stripContinuation,
|
|
427
|
+
isAbsolutePathTask,
|
|
428
|
+
makeCompleter,
|
|
429
|
+
completePath,
|
|
430
|
+
slashCommandEntries,
|
|
431
|
+
slashCommandSuggestions,
|
|
432
|
+
renderSlashPalette,
|
|
433
|
+
SLASH_COMMANDS,
|
|
434
|
+
RUNTIME_SPECS,
|
|
435
|
+
PERM_LEVELS,
|
|
436
|
+
HISTORY_MAX,
|
|
437
|
+
};
|