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.
Files changed (56) hide show
  1. package/dist/context/compaction.js +2 -1
  2. package/dist/context/compaction.js.map +1 -1
  3. package/dist/context/context-manager.d.ts +2 -1
  4. package/dist/context/context-manager.js +24 -11
  5. package/dist/context/context-manager.js.map +1 -1
  6. package/dist/context/smoke-context.js +14 -6
  7. package/dist/context/smoke-context.js.map +1 -1
  8. package/dist/core/context-metrics.d.ts +2 -1
  9. package/dist/core/context-metrics.js +3 -1
  10. package/dist/core/context-metrics.js.map +1 -1
  11. package/dist/core/image-registry.js +3 -3
  12. package/dist/core/image-registry.js.map +1 -1
  13. package/dist/core/image-storage.d.ts +14 -0
  14. package/dist/core/image-storage.js +31 -0
  15. package/dist/core/image-storage.js.map +1 -1
  16. package/dist/core/message-pipeline.d.ts +6 -0
  17. package/dist/core/message-pipeline.js +89 -10
  18. package/dist/core/message-pipeline.js.map +1 -1
  19. package/dist/core/prompt-cache-telemetry.d.ts +11 -0
  20. package/dist/core/prompt-cache-telemetry.js +71 -0
  21. package/dist/core/prompt-cache-telemetry.js.map +1 -0
  22. package/dist/core/query-engine.js +19 -6
  23. package/dist/core/query-engine.js.map +1 -1
  24. package/dist/core/query.js +18 -4
  25. package/dist/core/query.js.map +1 -1
  26. package/dist/core/smoke-core-loop.js +10 -1
  27. package/dist/core/smoke-core-loop.js.map +1 -1
  28. package/dist/model/anthropic-mapper.js +2 -1
  29. package/dist/model/anthropic-mapper.js.map +1 -1
  30. package/dist/model/openai-mappers.js +15 -6
  31. package/dist/model/openai-mappers.js.map +1 -1
  32. package/dist/repl/commands.d.ts +6 -0
  33. package/dist/repl/commands.js +20 -0
  34. package/dist/repl/commands.js.map +1 -1
  35. package/dist/repl/index.js +301 -14
  36. package/dist/repl/index.js.map +1 -1
  37. package/dist/session/session-export.js +2 -1
  38. package/dist/session/session-export.js.map +1 -1
  39. package/dist/skills/skill-filesystem.js +1 -1
  40. package/dist/skills/skill-filesystem.js.map +1 -1
  41. package/dist/skills/skill-tool.js +86 -22
  42. package/dist/skills/skill-tool.js.map +1 -1
  43. package/dist/tools/builtins/exec-tool.js +4 -1
  44. package/dist/tools/builtins/exec-tool.js.map +1 -1
  45. package/dist/tools/builtins/image-generation-tool.js +22 -4
  46. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  47. package/dist/tools/smoke-tool-system.js +69 -0
  48. package/dist/tools/smoke-tool-system.js.map +1 -1
  49. package/dist/tools/tool.d.ts +1 -0
  50. package/dist/tools/tool.js.map +1 -1
  51. package/dist/types/events.d.ts +17 -0
  52. package/dist/ui/display-message.js +8 -4
  53. package/dist/ui/display-message.js.map +1 -1
  54. package/dist/web/index.js +23 -6
  55. package/dist/web/index.js.map +1 -1
  56. package/package.json +2 -1
@@ -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(command.text, submitAttachments);
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
- function slashCommandCompletions(text, cursor) {
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}`,