neoctl 0.2.4 → 0.2.5
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/context/compaction.js +2 -1
- package/dist/context/compaction.js.map +1 -1
- package/dist/context/context-manager.d.ts +2 -1
- package/dist/context/context-manager.js +24 -11
- package/dist/context/context-manager.js.map +1 -1
- package/dist/context/smoke-context.js +14 -6
- package/dist/context/smoke-context.js.map +1 -1
- package/dist/core/context-metrics.d.ts +2 -1
- package/dist/core/context-metrics.js +3 -1
- package/dist/core/context-metrics.js.map +1 -1
- package/dist/core/image-registry.js +3 -3
- package/dist/core/image-registry.js.map +1 -1
- package/dist/core/image-storage.d.ts +14 -0
- package/dist/core/image-storage.js +31 -0
- package/dist/core/image-storage.js.map +1 -1
- package/dist/core/message-pipeline.d.ts +6 -0
- package/dist/core/message-pipeline.js +89 -10
- package/dist/core/message-pipeline.js.map +1 -1
- package/dist/core/prompt-cache-telemetry.d.ts +11 -0
- package/dist/core/prompt-cache-telemetry.js +71 -0
- package/dist/core/prompt-cache-telemetry.js.map +1 -0
- package/dist/core/query-engine.js +19 -6
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query.js +18 -4
- package/dist/core/query.js.map +1 -1
- package/dist/core/smoke-core-loop.js +10 -1
- package/dist/core/smoke-core-loop.js.map +1 -1
- package/dist/model/anthropic-mapper.js +2 -1
- package/dist/model/anthropic-mapper.js.map +1 -1
- package/dist/model/openai-mappers.js +15 -6
- package/dist/model/openai-mappers.js.map +1 -1
- package/dist/repl/commands.d.ts +6 -0
- package/dist/repl/commands.js +20 -0
- package/dist/repl/commands.js.map +1 -1
- package/dist/repl/index.js +301 -14
- package/dist/repl/index.js.map +1 -1
- package/dist/session/session-export.js +2 -1
- package/dist/session/session-export.js.map +1 -1
- package/dist/skills/skill-filesystem.js +1 -1
- package/dist/skills/skill-filesystem.js.map +1 -1
- package/dist/skills/skill-tool.js +86 -22
- package/dist/skills/skill-tool.js.map +1 -1
- package/dist/tools/builtins/exec-tool.js +4 -1
- package/dist/tools/builtins/exec-tool.js.map +1 -1
- package/dist/tools/builtins/image-generation-tool.js +22 -4
- package/dist/tools/builtins/image-generation-tool.js.map +1 -1
- package/dist/tools/smoke-tool-system.js +69 -0
- package/dist/tools/smoke-tool-system.js.map +1 -1
- package/dist/tools/tool.d.ts +1 -0
- package/dist/tools/tool.js.map +1 -1
- package/dist/types/events.d.ts +17 -0
- package/dist/ui/display-message.js +8 -4
- package/dist/ui/display-message.js.map +1 -1
- package/dist/web/index.js +23 -6
- package/dist/web/index.js.map +1 -1
- package/package.json +2 -1
package/dist/repl/index.js
CHANGED
|
@@ -27,11 +27,17 @@ import { createTaskTools } from "../tasks/task-tools.js";
|
|
|
27
27
|
import { TaskStore } from "../tasks/task-store.js";
|
|
28
28
|
import { cliHelpText, isModelReasoningArgument, isValidReplCommandLine, parseCliReplCommandArgs, parseReplCommand, helpText, replCommandDefinitions } from "./commands.js";
|
|
29
29
|
import { estimateMarkdownLineCount, markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
|
|
30
|
+
import { DefaultContextManager } from "../context/context-manager.js";
|
|
31
|
+
import { buildEffectiveSystemPrompt } from "../context/prompts.js";
|
|
30
32
|
import { writeSessionMarkdownExport } from "../session/session-export.js";
|
|
31
33
|
import { readClipboard } from "./clipboard.js";
|
|
32
34
|
import { formatTipLine, initialTipIndex, tipAt } from "../tips.js";
|
|
33
35
|
import { openDirectory } from "../open-directory.js";
|
|
34
36
|
import { runWebServer } from "../web/index.js";
|
|
37
|
+
import { getNeoctlHome } from "../paths.js";
|
|
38
|
+
import { FileSystemSkillCatalog } from "../skills/skill-filesystem.js";
|
|
39
|
+
import { createSkillAwareCanUseTool, createSkillTool, requireSkillName } from "../skills/skill-tool.js";
|
|
40
|
+
import { createSkillManagementTools } from "../skills/skill-management-tools.js";
|
|
35
41
|
const e = React.createElement;
|
|
36
42
|
class SessionUsageTracker {
|
|
37
43
|
totals = emptyUsageTotals();
|
|
@@ -128,6 +134,50 @@ function binaryName() {
|
|
|
128
134
|
const name = parsed.name || "neo";
|
|
129
135
|
return name === "index" ? "neo" : name;
|
|
130
136
|
}
|
|
137
|
+
class SkillCatalogContextManager {
|
|
138
|
+
catalog;
|
|
139
|
+
base;
|
|
140
|
+
constructor(catalog, base = new DefaultContextManager()) {
|
|
141
|
+
this.catalog = catalog;
|
|
142
|
+
this.base = base;
|
|
143
|
+
}
|
|
144
|
+
async build(input) {
|
|
145
|
+
const runtimeContext = await this.base.build(input);
|
|
146
|
+
const skillSection = await buildSkillCatalogPromptSection(this.catalog);
|
|
147
|
+
if (!skillSection)
|
|
148
|
+
return runtimeContext;
|
|
149
|
+
const promptSections = [...runtimeContext.promptSections, skillSection];
|
|
150
|
+
return {
|
|
151
|
+
...runtimeContext,
|
|
152
|
+
promptSections,
|
|
153
|
+
systemPrompt: buildEffectiveSystemPrompt(promptSections, input),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function buildSkillCatalogPromptSection(catalog) {
|
|
158
|
+
const skills = await catalog.list();
|
|
159
|
+
if (skills.length === 0)
|
|
160
|
+
return undefined;
|
|
161
|
+
const visible = skills.slice(0, 80);
|
|
162
|
+
const lines = visible.map((skill) => {
|
|
163
|
+
const tags = skill.tags?.length ? `; tags=${skill.tags.join(",")}` : "";
|
|
164
|
+
const tools = skill.allowedTools?.length ? `; allowedTools=${skill.allowedTools.join(",")}` : "";
|
|
165
|
+
return `- ${skill.name}: ${skill.description} (execution=${skill.execution}${tags}${tools})`;
|
|
166
|
+
});
|
|
167
|
+
if (skills.length > visible.length)
|
|
168
|
+
lines.push(`- ... ${skills.length - visible.length} more skills available; use skill_list for the full catalog.`);
|
|
169
|
+
return {
|
|
170
|
+
name: "Available Skills",
|
|
171
|
+
cacheStable: false,
|
|
172
|
+
content: [
|
|
173
|
+
"Reusable skills are available through the `skill` tool and the `/skill` REPL command.",
|
|
174
|
+
"When the user's task matches a skill name, description, tags, or domain capability, proactively call the `skill` tool before doing the work directly.",
|
|
175
|
+
"Do not wait for the user to explicitly say 'use skill'. Use skill_list/skill_read if you need to inspect details.",
|
|
176
|
+
"Available skill catalog:",
|
|
177
|
+
...lines,
|
|
178
|
+
].join("\n"),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
131
181
|
function createTaskNotificationSource(taskStore) {
|
|
132
182
|
return {
|
|
133
183
|
collectUnnotifiedCompletions() {
|
|
@@ -151,6 +201,14 @@ async function createRuntime() {
|
|
|
151
201
|
const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
|
|
152
202
|
const taskStore = new TaskStore();
|
|
153
203
|
const tools = new ToolRegistry();
|
|
204
|
+
const skillWorkspaceRoot = path.resolve(process.cwd(), ".neo", "skills");
|
|
205
|
+
const skills = new FileSystemSkillCatalog({
|
|
206
|
+
roots: [
|
|
207
|
+
{ root: skillWorkspaceRoot, kind: "workspace" },
|
|
208
|
+
{ root: path.resolve(getNeoctlHome(), "skills"), kind: "user" },
|
|
209
|
+
],
|
|
210
|
+
createRoot: skillWorkspaceRoot,
|
|
211
|
+
});
|
|
154
212
|
tools.register(editTool);
|
|
155
213
|
tools.register(writeTool);
|
|
156
214
|
tools.register(createExecTool({ taskStore }));
|
|
@@ -162,6 +220,9 @@ async function createRuntime() {
|
|
|
162
220
|
if (modelConfig?.provider === "openai")
|
|
163
221
|
tools.register(createOpenAIImageGenerationTool());
|
|
164
222
|
tools.register(planTool);
|
|
223
|
+
tools.register(createSkillTool(skills));
|
|
224
|
+
for (const tool of createSkillManagementTools(skills, { requireApproval: true, allowDelete: false }))
|
|
225
|
+
tools.register(tool);
|
|
165
226
|
const agentRuntime = { modelGateway, tools, taskStore };
|
|
166
227
|
tools.register(createAgentTool(agentRuntime));
|
|
167
228
|
const resumeHandler = async (taskId, directive) => {
|
|
@@ -183,6 +244,8 @@ async function createRuntime() {
|
|
|
183
244
|
reasoning: modelConfig?.defaultReasoning,
|
|
184
245
|
modelGateway,
|
|
185
246
|
tools,
|
|
247
|
+
contextManager: new SkillCatalogContextManager(skills),
|
|
248
|
+
canUseTool: createSkillAwareCanUseTool(skills),
|
|
186
249
|
taskNotificationSource,
|
|
187
250
|
commands: replCommandDefinitions.map((command) => command.usage),
|
|
188
251
|
session: {
|
|
@@ -205,6 +268,8 @@ async function createRuntime() {
|
|
|
205
268
|
usage: new SessionUsageTracker(),
|
|
206
269
|
taskStore,
|
|
207
270
|
tools,
|
|
271
|
+
skills,
|
|
272
|
+
skillWorkspaceRoot,
|
|
208
273
|
initialMetrics,
|
|
209
274
|
defaultReasoning: modelConfig?.defaultReasoning,
|
|
210
275
|
envPath: process.env.NEO_ENV_FILE?.trim() ? path.resolve(process.env.NEO_ENV_FILE.trim()) : envLoad.userDotEnvPath,
|
|
@@ -418,6 +483,7 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
418
483
|
const [pasteStatus, setPasteStatus] = useState(undefined);
|
|
419
484
|
const pasteStatusTimerRef = useRef(undefined);
|
|
420
485
|
const [slashCompletionIndex, setSlashCompletionIndex] = useState(0);
|
|
486
|
+
const [skillCompletions, setSkillCompletions] = useState([]);
|
|
421
487
|
const [loginForm, setLoginForm] = useState(undefined);
|
|
422
488
|
const loginFormRef = useRef(undefined);
|
|
423
489
|
useEffect(() => {
|
|
@@ -488,6 +554,18 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
488
554
|
loginFormRef.current = next;
|
|
489
555
|
setLoginForm(next);
|
|
490
556
|
};
|
|
557
|
+
const refreshSkillCompletions = useCallback(async () => {
|
|
558
|
+
try {
|
|
559
|
+
const skills = await runtime.skills.list();
|
|
560
|
+
setSkillCompletions(skills.map((skill) => ({ name: skill.name, description: skill.description, execution: skill.execution, tags: skill.tags })));
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
setSkillCompletions([]);
|
|
564
|
+
}
|
|
565
|
+
}, [runtime]);
|
|
566
|
+
useEffect(() => {
|
|
567
|
+
void refreshSkillCompletions();
|
|
568
|
+
}, [refreshSkillCompletions]);
|
|
491
569
|
const syncAttachmentsForText = (text) => {
|
|
492
570
|
const next = attachmentsRef.current.filter((attachment) => text.includes(attachment.label));
|
|
493
571
|
if (next.length === attachmentsRef.current.length)
|
|
@@ -855,6 +933,7 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
855
933
|
};
|
|
856
934
|
const handleCommandOrPrompt = async (text, submitAttachments = []) => {
|
|
857
935
|
const command = parseReplCommand(text);
|
|
936
|
+
let promptText = command.type === "input" ? command.text : text;
|
|
858
937
|
if (command.type === "exit") {
|
|
859
938
|
app.exit();
|
|
860
939
|
return;
|
|
@@ -993,6 +1072,16 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
993
1072
|
await handleSessionsCommand(runtime, runningSessionIds(backgroundSessionRunsRef.current), setSessionsBrowser, (line) => append(line));
|
|
994
1073
|
return;
|
|
995
1074
|
}
|
|
1075
|
+
if (command.type === "skill") {
|
|
1076
|
+
if (command.action !== "invoke" || !command.name || !command.args) {
|
|
1077
|
+
append(await handleSkillCommand(command, runtime));
|
|
1078
|
+
if (command.action === "import" || command.action === "delete")
|
|
1079
|
+
void refreshSkillCompletions();
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
promptText = renderSkillInvocationPrompt(command.name, command.args);
|
|
1083
|
+
text = `/skill ${command.name} ${command.args}`;
|
|
1084
|
+
}
|
|
996
1085
|
if (command.type === "login") {
|
|
997
1086
|
setSessionsBrowser(undefined);
|
|
998
1087
|
setLoginFormState(createLoginFormState(runtime.envPath));
|
|
@@ -1023,11 +1112,11 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
1023
1112
|
}
|
|
1024
1113
|
return;
|
|
1025
1114
|
}
|
|
1026
|
-
if (text.trimStart().startsWith("/")) {
|
|
1115
|
+
if (command.type !== "skill" && text.trimStart().startsWith("/")) {
|
|
1027
1116
|
append({ kind: "error", text: `Unknown or incomplete command: ${text.trim()}\nType /help for commands.` });
|
|
1028
1117
|
return;
|
|
1029
1118
|
}
|
|
1030
|
-
const promptPayload = buildPromptPayload(
|
|
1119
|
+
const promptPayload = buildPromptPayload(promptText, submitAttachments);
|
|
1031
1120
|
append({ kind: "user", text });
|
|
1032
1121
|
const runToken = ++foregroundRunTokenRef.current;
|
|
1033
1122
|
const abortController = new AbortController();
|
|
@@ -1131,7 +1220,7 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
1131
1220
|
const promptDisplayCursor = cursor;
|
|
1132
1221
|
const promptLayoutText = activePlaceholder ? ` ${activePlaceholder}` : promptDisplayText;
|
|
1133
1222
|
const promptLayoutCursor = activePlaceholder ? 0 : promptDisplayCursor;
|
|
1134
|
-
const slashCompletions = inputLockedByQueue || (input.length === 0 && promptPlaceholder !== undefined) || loginForm ? [] : slashCommandCompletions(input, cursor);
|
|
1223
|
+
const slashCompletions = inputLockedByQueue || (input.length === 0 && promptPlaceholder !== undefined) || loginForm ? [] : slashCommandCompletions(input, cursor, skillCompletions);
|
|
1135
1224
|
const visibleSlashCompletionCount = slashCompletions.length;
|
|
1136
1225
|
const selectedSlashCompletionIndex = visibleSlashCompletionCount === 0
|
|
1137
1226
|
? 0
|
|
@@ -1248,12 +1337,21 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
1248
1337
|
if (key.return) {
|
|
1249
1338
|
const currentText = inputRef.current;
|
|
1250
1339
|
const currentCursor = cursorRef.current;
|
|
1251
|
-
const completion = selectedSlashCommandCompletion(currentText, currentCursor, slashCompletionIndexRef.current);
|
|
1340
|
+
const completion = selectedSlashCommandCompletion(currentText, currentCursor, slashCompletionIndexRef.current, skillCompletions);
|
|
1252
1341
|
if (completion !== undefined && completion.kind === "command" && completion.arguments !== "none") {
|
|
1253
1342
|
const nextText = `${completion.insertText} ${currentText.slice(currentCursor)}`;
|
|
1254
1343
|
setPromptState(nextText, completion.insertText.length + 1);
|
|
1255
1344
|
return;
|
|
1256
1345
|
}
|
|
1346
|
+
if (currentText.trimEnd() === "/skill") {
|
|
1347
|
+
void submitLine(currentText);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
if (completion !== undefined && completion.kind === "skill-action") {
|
|
1351
|
+
const nextText = `${completion.insertText}${currentText.slice(currentCursor)}`;
|
|
1352
|
+
setPromptState(nextText, completion.insertText.length);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1257
1355
|
if (currentText.trimEnd() === "/model" && completion?.kind !== "command") {
|
|
1258
1356
|
void submitLine(currentText);
|
|
1259
1357
|
return;
|
|
@@ -1274,7 +1372,7 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
1274
1372
|
return;
|
|
1275
1373
|
}
|
|
1276
1374
|
if (key.leftArrow) {
|
|
1277
|
-
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
1375
|
+
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions);
|
|
1278
1376
|
if (completionCount > SLASH_COMPLETION_PAGE_SIZE) {
|
|
1279
1377
|
setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - SLASH_COMPLETION_PAGE_SIZE) % completionCount);
|
|
1280
1378
|
return;
|
|
@@ -1287,7 +1385,7 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
1287
1385
|
return;
|
|
1288
1386
|
}
|
|
1289
1387
|
if (key.rightArrow) {
|
|
1290
|
-
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
1388
|
+
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions);
|
|
1291
1389
|
if (completionCount > SLASH_COMPLETION_PAGE_SIZE) {
|
|
1292
1390
|
setSlashCompletionSelection((slashCompletionIndexRef.current + SLASH_COMPLETION_PAGE_SIZE) % completionCount);
|
|
1293
1391
|
return;
|
|
@@ -1318,7 +1416,7 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
1318
1416
|
setTipIndex((current) => current - 1);
|
|
1319
1417
|
return;
|
|
1320
1418
|
}
|
|
1321
|
-
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
1419
|
+
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions);
|
|
1322
1420
|
if (completionCount > 0) {
|
|
1323
1421
|
setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - 1) % completionCount);
|
|
1324
1422
|
return;
|
|
@@ -1335,7 +1433,7 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
1335
1433
|
setTipIndex((current) => current + 1);
|
|
1336
1434
|
return;
|
|
1337
1435
|
}
|
|
1338
|
-
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
1436
|
+
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions);
|
|
1339
1437
|
if (completionCount > 0) {
|
|
1340
1438
|
setSlashCompletionSelection((slashCompletionIndexRef.current + 1) % completionCount);
|
|
1341
1439
|
return;
|
|
@@ -1361,7 +1459,7 @@ function InkRepl({ runtime, initialCommandLine }) {
|
|
|
1361
1459
|
return;
|
|
1362
1460
|
}
|
|
1363
1461
|
const currentCursor = cursorRef.current;
|
|
1364
|
-
const completions = slashCommandCompletions(currentText, currentCursor);
|
|
1462
|
+
const completions = slashCommandCompletions(currentText, currentCursor, skillCompletions);
|
|
1365
1463
|
const completion = completions[Math.min(slashCompletionIndexRef.current, completions.length - 1)];
|
|
1366
1464
|
if (completion !== undefined) {
|
|
1367
1465
|
const nextText = `${completion.insertText}${currentText.slice(currentCursor)}`;
|
|
@@ -1806,7 +1904,11 @@ function fitStatusSegments(segments, width) {
|
|
|
1806
1904
|
const SLASH_COMPLETION_PAGE_SIZE = 10;
|
|
1807
1905
|
const MODEL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1808
1906
|
const MODEL_REASONING_CONTROL_CHOICES = ["default", "off"];
|
|
1809
|
-
|
|
1907
|
+
const SKILL_COMMAND_ACTIONS = [
|
|
1908
|
+
{ name: "import", description: "Import by linking a skill directory" },
|
|
1909
|
+
{ name: "delete", description: "Delete a workspace skill link/directory", aliases: ["remove", "rm"] },
|
|
1910
|
+
];
|
|
1911
|
+
function slashCommandCompletions(text, cursor, skills = []) {
|
|
1810
1912
|
const safeCursor = Math.max(0, Math.min(cursor, text.length));
|
|
1811
1913
|
const prefix = text.slice(0, safeCursor);
|
|
1812
1914
|
if (!prefix.startsWith("/") || /\r|\n/.test(prefix))
|
|
@@ -1819,6 +1921,9 @@ function slashCommandCompletions(text, cursor) {
|
|
|
1819
1921
|
if (prefix.startsWith("/model") && (prefix.length === "/model".length || prefix["/model".length] === " ")) {
|
|
1820
1922
|
return modelCommandCompletions(prefix);
|
|
1821
1923
|
}
|
|
1924
|
+
if (prefix.startsWith("/skill") && (prefix.length === "/skill".length || prefix["/skill".length] === " ")) {
|
|
1925
|
+
return skillCommandCompletions(prefix, skills);
|
|
1926
|
+
}
|
|
1822
1927
|
if (prefix.length > 1 && !/^\/[\w-]*$/.test(prefix))
|
|
1823
1928
|
return [];
|
|
1824
1929
|
const normalizedPrefix = prefix.toLowerCase();
|
|
@@ -1826,6 +1931,56 @@ function slashCommandCompletions(text, cursor) {
|
|
|
1826
1931
|
.flatMap((command) => [command.name, ...(command.aliases ?? [])].map((name) => ({ value: name, insertText: name, description: command.description, arguments: command.arguments, kind: "command" })))
|
|
1827
1932
|
.filter((command) => command.value.toLowerCase().startsWith(normalizedPrefix));
|
|
1828
1933
|
}
|
|
1934
|
+
function skillCommandCompletions(prefix, skills) {
|
|
1935
|
+
const hasTrailingSpace = /\s$/.test(prefix);
|
|
1936
|
+
const tokens = prefix.trim().split(/\s+/).filter(Boolean);
|
|
1937
|
+
const argumentTokens = tokens.slice(1);
|
|
1938
|
+
if (!hasTrailingSpace && argumentTokens.length === 0 && !"/skill".startsWith(prefix.toLowerCase()))
|
|
1939
|
+
return [];
|
|
1940
|
+
if (argumentTokens.length === 0) {
|
|
1941
|
+
return [...skillActionCompletions(""), ...skillNameCompletions(skills, "")];
|
|
1942
|
+
}
|
|
1943
|
+
const [first = "", second = ""] = argumentTokens;
|
|
1944
|
+
if (first === "import")
|
|
1945
|
+
return [];
|
|
1946
|
+
if (first === "delete" || first === "remove" || first === "rm") {
|
|
1947
|
+
if (argumentTokens.length > 1 && hasTrailingSpace)
|
|
1948
|
+
return [];
|
|
1949
|
+
return skillNameCompletions(skills, hasTrailingSpace ? "" : second, "delete");
|
|
1950
|
+
}
|
|
1951
|
+
if (argumentTokens.length > 1)
|
|
1952
|
+
return [];
|
|
1953
|
+
if (hasTrailingSpace)
|
|
1954
|
+
return [];
|
|
1955
|
+
return [...skillActionCompletions(first), ...skillNameCompletions(skills, first)];
|
|
1956
|
+
}
|
|
1957
|
+
function skillActionCompletions(current) {
|
|
1958
|
+
return SKILL_COMMAND_ACTIONS
|
|
1959
|
+
.flatMap((action) => [action.name, ...("aliases" in action ? action.aliases ?? [] : [])].map((name) => ({ name, description: action.description })))
|
|
1960
|
+
.filter((action) => action.name.startsWith(current.toLowerCase()))
|
|
1961
|
+
.map((action) => ({
|
|
1962
|
+
value: action.name,
|
|
1963
|
+
insertText: `/skill ${action.name} `,
|
|
1964
|
+
description: action.description,
|
|
1965
|
+
arguments: "optional",
|
|
1966
|
+
kind: "skill-action",
|
|
1967
|
+
}));
|
|
1968
|
+
}
|
|
1969
|
+
function skillNameCompletions(skills, current, action) {
|
|
1970
|
+
return skills
|
|
1971
|
+
.filter((skill) => skill.name.toLowerCase().includes(current.toLowerCase()))
|
|
1972
|
+
.map((skill) => ({
|
|
1973
|
+
value: skill.name,
|
|
1974
|
+
insertText: action === "delete" ? `/skill delete ${skill.name}` : `/skill ${skill.name}`,
|
|
1975
|
+
description: formatSkillCompletionDescription(skill),
|
|
1976
|
+
arguments: "optional",
|
|
1977
|
+
kind: "skill",
|
|
1978
|
+
}));
|
|
1979
|
+
}
|
|
1980
|
+
function formatSkillCompletionDescription(skill) {
|
|
1981
|
+
const tags = skill.tags?.length ? ` · ${skill.tags.join(",")}` : "";
|
|
1982
|
+
return `${skill.description}${skill.execution ? ` · ${skill.execution}` : ""}${tags}`;
|
|
1983
|
+
}
|
|
1829
1984
|
function modelCommandCompletions(prefix) {
|
|
1830
1985
|
const hasTrailingSpace = /\s$/.test(prefix);
|
|
1831
1986
|
const tokens = prefix.trim().split(/\s+/).filter(Boolean);
|
|
@@ -1911,11 +2066,11 @@ function slashCompletionViewHeight(completions) {
|
|
|
1911
2066
|
return 0;
|
|
1912
2067
|
return Math.min(completions.length, SLASH_COMPLETION_PAGE_SIZE) + 2;
|
|
1913
2068
|
}
|
|
1914
|
-
function slashCompletionSelectableCount(text, cursor) {
|
|
1915
|
-
return slashCommandCompletions(text, cursor).length;
|
|
2069
|
+
function slashCompletionSelectableCount(text, cursor, skills = []) {
|
|
2070
|
+
return slashCommandCompletions(text, cursor, skills).length;
|
|
1916
2071
|
}
|
|
1917
|
-
function selectedSlashCommandCompletion(text, cursor, selectedIndex) {
|
|
1918
|
-
const completions = slashCommandCompletions(text, cursor);
|
|
2072
|
+
function selectedSlashCommandCompletion(text, cursor, selectedIndex, skills = []) {
|
|
2073
|
+
const completions = slashCommandCompletions(text, cursor, skills);
|
|
1919
2074
|
if (completions.length === 0)
|
|
1920
2075
|
return undefined;
|
|
1921
2076
|
return completions[Math.max(0, Math.min(selectedIndex, completions.length - 1))];
|
|
@@ -1988,6 +2143,126 @@ function SlashCompletionLines({ completions, width, prompt, selectedIndex }) {
|
|
|
1988
2143
|
e(Text, { key: "slash-completion-footer", color: "gray" }, fitToWidth(footer, contentWidth)),
|
|
1989
2144
|
].map((line, index) => e(Box, { key: `slash-completion-line-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: "gray" }, " ".repeat(prompt.length)), line));
|
|
1990
2145
|
}
|
|
2146
|
+
async function handleSkillCommand(command, runtime) {
|
|
2147
|
+
if (command.action === "import")
|
|
2148
|
+
return handleSkillImportCommand(command, runtime);
|
|
2149
|
+
if (command.action === "delete")
|
|
2150
|
+
return handleSkillDeleteCommand(command, runtime);
|
|
2151
|
+
if (!command.name) {
|
|
2152
|
+
const skills = await runtime.skills.list();
|
|
2153
|
+
return systemLine(formatSkillList(skills), EXPANDED_SUMMARY_MAX_LINES);
|
|
2154
|
+
}
|
|
2155
|
+
const skill = await runtime.skills.get(command.name);
|
|
2156
|
+
if (!skill)
|
|
2157
|
+
return { kind: "error", text: `Unknown skill: ${command.name}\nUse /skill to list available skills.` };
|
|
2158
|
+
return systemLine(formatSkillDetails(skill), EXPANDED_SUMMARY_MAX_LINES);
|
|
2159
|
+
}
|
|
2160
|
+
async function handleSkillImportCommand(command, runtime) {
|
|
2161
|
+
if (!command.path)
|
|
2162
|
+
return { kind: "error", text: "Usage: /skill import <path-to-skill-directory> [name]" };
|
|
2163
|
+
const sourceDirectory = path.resolve(command.path);
|
|
2164
|
+
const skillFile = path.join(sourceDirectory, "SKILL.md");
|
|
2165
|
+
try {
|
|
2166
|
+
const stat = await fs.stat(skillFile);
|
|
2167
|
+
if (!stat.isFile())
|
|
2168
|
+
return { kind: "error", text: `SKILL.md is not a file: ${skillFile}` };
|
|
2169
|
+
}
|
|
2170
|
+
catch (error) {
|
|
2171
|
+
return { kind: "error", text: `Invalid skill path: ${skillFile}\n${error instanceof Error ? error.message : String(error)}` };
|
|
2172
|
+
}
|
|
2173
|
+
const name = requireSkillName(command.name ?? path.basename(sourceDirectory));
|
|
2174
|
+
const linkPath = path.join(runtime.skillWorkspaceRoot, name);
|
|
2175
|
+
const relativeTarget = path.relative(path.dirname(linkPath), sourceDirectory) || sourceDirectory;
|
|
2176
|
+
try {
|
|
2177
|
+
await fs.mkdir(runtime.skillWorkspaceRoot, { recursive: true });
|
|
2178
|
+
const existing = await safeLstat(linkPath);
|
|
2179
|
+
if (existing)
|
|
2180
|
+
return { kind: "error", text: `Skill already exists at ${linkPath}. Delete it first with /skill delete ${name}.` };
|
|
2181
|
+
await fs.symlink(relativeTarget, linkPath, "junction");
|
|
2182
|
+
const imported = await runtime.skills.get(name);
|
|
2183
|
+
return systemLine(`Imported skill ${name}\nLink: ${linkPath}\nTarget: ${sourceDirectory}${imported ? `\nDescription: ${imported.description}` : ""}`);
|
|
2184
|
+
}
|
|
2185
|
+
catch (error) {
|
|
2186
|
+
return { kind: "error", text: `Failed to import skill ${name}: ${error instanceof Error ? error.message : String(error)}` };
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
async function handleSkillDeleteCommand(command, runtime) {
|
|
2190
|
+
if (!command.name)
|
|
2191
|
+
return { kind: "error", text: "Usage: /skill delete <name>" };
|
|
2192
|
+
const name = requireSkillName(command.name);
|
|
2193
|
+
const skillPath = path.join(runtime.skillWorkspaceRoot, name);
|
|
2194
|
+
const existing = await safeLstat(skillPath);
|
|
2195
|
+
if (!existing)
|
|
2196
|
+
return { kind: "error", text: `No workspace skill named ${name} at ${skillPath}` };
|
|
2197
|
+
try {
|
|
2198
|
+
if (existing.isSymbolicLink())
|
|
2199
|
+
await fs.unlink(skillPath);
|
|
2200
|
+
else
|
|
2201
|
+
await fs.rm(skillPath, { recursive: true, force: true });
|
|
2202
|
+
return systemLine(`Deleted workspace skill ${name}: ${skillPath}`);
|
|
2203
|
+
}
|
|
2204
|
+
catch (error) {
|
|
2205
|
+
return { kind: "error", text: `Failed to delete skill ${name}: ${error instanceof Error ? error.message : String(error)}` };
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
async function safeLstat(file) {
|
|
2209
|
+
try {
|
|
2210
|
+
return await fs.lstat(file);
|
|
2211
|
+
}
|
|
2212
|
+
catch {
|
|
2213
|
+
return undefined;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
function renderSkillInvocationPrompt(name, args) {
|
|
2217
|
+
return `Use skill ${JSON.stringify(name)} with these arguments:\n${args}`;
|
|
2218
|
+
}
|
|
2219
|
+
function formatSkillList(skills) {
|
|
2220
|
+
if (skills.length === 0) {
|
|
2221
|
+
return [
|
|
2222
|
+
"No skills found.",
|
|
2223
|
+
"Skill roots:",
|
|
2224
|
+
" - .neo/skills/<name>/SKILL.md",
|
|
2225
|
+
" - ~/.neoctl/skills/<name>/SKILL.md",
|
|
2226
|
+
"Create one with the skill_create tool or add a SKILL.md file.",
|
|
2227
|
+
].join("\n");
|
|
2228
|
+
}
|
|
2229
|
+
const width = Math.max(...skills.map((skill) => skill.name.length));
|
|
2230
|
+
return [
|
|
2231
|
+
"Available skills:",
|
|
2232
|
+
...skills.map((skill) => {
|
|
2233
|
+
const tags = skill.tags?.length ? ` [${skill.tags.join(", ")}]` : "";
|
|
2234
|
+
const execution = skill.execution ? ` (${skill.execution})` : "";
|
|
2235
|
+
return ` ${skill.name.padEnd(width)} ${skill.description}${execution}${tags}`;
|
|
2236
|
+
}),
|
|
2237
|
+
"",
|
|
2238
|
+
"Usage:",
|
|
2239
|
+
" /skill <name> Show skill details",
|
|
2240
|
+
" /skill <name> <args> Invoke skill with arguments",
|
|
2241
|
+
" /skill import <path> [name] Import by linking a skill directory",
|
|
2242
|
+
" /skill delete <name> Delete workspace skill link/directory",
|
|
2243
|
+
].join("\n");
|
|
2244
|
+
}
|
|
2245
|
+
function formatSkillDetails(skill) {
|
|
2246
|
+
const lines = [
|
|
2247
|
+
`Skill: ${skill.name}`,
|
|
2248
|
+
skill.title ? `Title: ${skill.title}` : undefined,
|
|
2249
|
+
`Description: ${skill.description}`,
|
|
2250
|
+
`Execution: ${skill.execution}`,
|
|
2251
|
+
skill.version ? `Version: ${skill.version}` : undefined,
|
|
2252
|
+
skill.tags?.length ? `Tags: ${skill.tags.join(", ")}` : undefined,
|
|
2253
|
+
skill.allowedTools?.length ? `Allowed tools: ${skill.allowedTools.join(", ")}` : undefined,
|
|
2254
|
+
skill.model ? `Model: ${skill.model}` : undefined,
|
|
2255
|
+
skill.effort ? `Effort: ${skill.effort}` : undefined,
|
|
2256
|
+
skill.trustLevel ? `Trust: ${skill.trustLevel}` : undefined,
|
|
2257
|
+
skill.source?.path ? `Path: ${skill.source.path}` : undefined,
|
|
2258
|
+
"",
|
|
2259
|
+
"Entrypoint:",
|
|
2260
|
+
skill.entrypoint,
|
|
2261
|
+
"",
|
|
2262
|
+
`Invoke: /skill ${skill.name} <args>`,
|
|
2263
|
+
].filter((line) => line !== undefined);
|
|
2264
|
+
return lines.join("\n");
|
|
2265
|
+
}
|
|
1991
2266
|
async function handleModelCommand(command, runtime) {
|
|
1992
2267
|
const current = runtime.engine.getModelSettings();
|
|
1993
2268
|
const nextModel = command.model ?? current.model;
|
|
@@ -3034,9 +3309,11 @@ function formatToolUse(toolUse) {
|
|
|
3034
3309
|
text: formatPlanToolPayload(toolUse.input),
|
|
3035
3310
|
};
|
|
3036
3311
|
}
|
|
3312
|
+
const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
|
|
3037
3313
|
return {
|
|
3038
3314
|
kind: "tool",
|
|
3039
3315
|
title: toolTitle(toolUse.name, "running"),
|
|
3316
|
+
bodyTitle: description,
|
|
3040
3317
|
text: formatJson(toolUse.input, 1200),
|
|
3041
3318
|
previewStyle: "summary",
|
|
3042
3319
|
};
|
|
@@ -3063,9 +3340,11 @@ function formatToolResultLine(toolName, output, ok) {
|
|
|
3063
3340
|
}
|
|
3064
3341
|
function formatToolFinishedWithoutResult(toolUse, ok) {
|
|
3065
3342
|
const inputText = formatJson(toolUse.input, 1200);
|
|
3343
|
+
const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
|
|
3066
3344
|
return {
|
|
3067
3345
|
kind: ok ? "tool" : "error",
|
|
3068
3346
|
title: toolTitle(toolUse.name, "finished"),
|
|
3347
|
+
bodyTitle: description,
|
|
3069
3348
|
titleStatus: ok ? "success" : "failure",
|
|
3070
3349
|
text: inputText ? `${ok ? "finished" : "failed"}\n${inputText}` : ok ? "finished" : "failed",
|
|
3071
3350
|
previewStyle: "summary",
|
|
@@ -3078,6 +3357,12 @@ function toolTitle(toolName, phase) {
|
|
|
3078
3357
|
return `${phase === "running" ? "◇" : "◆"} plan`;
|
|
3079
3358
|
return `${phase === "running" ? "◇" : "◆"} ${toolName}`;
|
|
3080
3359
|
}
|
|
3360
|
+
function execDescriptionFromInput(input) {
|
|
3361
|
+
if (!isRecord(input))
|
|
3362
|
+
return undefined;
|
|
3363
|
+
const description = typeof input.description === "string" ? input.description.trim() : "";
|
|
3364
|
+
return description || undefined;
|
|
3365
|
+
}
|
|
3081
3366
|
function isPlanToolPayload(value) {
|
|
3082
3367
|
if (!isRecord(value) || !Array.isArray(value.items))
|
|
3083
3368
|
return false;
|
|
@@ -3301,8 +3586,10 @@ function formatExecToolResult(output, ok) {
|
|
|
3301
3586
|
: output.exitCode === 0
|
|
3302
3587
|
? "exit 0"
|
|
3303
3588
|
: `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
|
|
3589
|
+
const description = typeof output.description === "string" ? output.description.trim() : "";
|
|
3304
3590
|
const lines = [
|
|
3305
3591
|
"exec result",
|
|
3592
|
+
...(description ? [`purpose: ${description}`] : []),
|
|
3306
3593
|
`status: ${status}`,
|
|
3307
3594
|
`duration: ${output.durationMs}ms`,
|
|
3308
3595
|
`command: ${output.command}`,
|