longer-agent 0.1.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.
Files changed (289) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/README.zh-CN.md +227 -0
  4. package/agent_templates/executor/agent.yaml +22 -0
  5. package/agent_templates/executor/system_prompt.md +17 -0
  6. package/agent_templates/explorer/agent.yaml +13 -0
  7. package/agent_templates/explorer/system_prompt.md +19 -0
  8. package/agent_templates/main/agent.yaml +7 -0
  9. package/agent_templates/main/system_prompt.md +45 -0
  10. package/configExample.yaml +83 -0
  11. package/dist/agents/agent.d.ts +79 -0
  12. package/dist/agents/agent.d.ts.map +1 -0
  13. package/dist/agents/agent.js +156 -0
  14. package/dist/agents/agent.js.map +1 -0
  15. package/dist/agents/tool-loop.d.ts +140 -0
  16. package/dist/agents/tool-loop.d.ts.map +1 -0
  17. package/dist/agents/tool-loop.js +465 -0
  18. package/dist/agents/tool-loop.js.map +1 -0
  19. package/dist/ask.d.ts +81 -0
  20. package/dist/ask.d.ts.map +1 -0
  21. package/dist/ask.js +34 -0
  22. package/dist/ask.js.map +1 -0
  23. package/dist/auth/openai-oauth.d.ts +66 -0
  24. package/dist/auth/openai-oauth.d.ts.map +1 -0
  25. package/dist/auth/openai-oauth.js +640 -0
  26. package/dist/auth/openai-oauth.js.map +1 -0
  27. package/dist/cli.d.ts +14 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +254 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/commands.d.ts +118 -0
  32. package/dist/commands.d.ts.map +1 -0
  33. package/dist/commands.js +862 -0
  34. package/dist/commands.js.map +1 -0
  35. package/dist/config.d.ts +130 -0
  36. package/dist/config.d.ts.map +1 -0
  37. package/dist/config.js +648 -0
  38. package/dist/config.js.map +1 -0
  39. package/dist/context-rendering.d.ts +69 -0
  40. package/dist/context-rendering.d.ts.map +1 -0
  41. package/dist/context-rendering.js +250 -0
  42. package/dist/context-rendering.js.map +1 -0
  43. package/dist/document-projection.d.ts +12 -0
  44. package/dist/document-projection.d.ts.map +1 -0
  45. package/dist/document-projection.js +75 -0
  46. package/dist/document-projection.js.map +1 -0
  47. package/dist/ephemeral-log.d.ts +15 -0
  48. package/dist/ephemeral-log.d.ts.map +1 -0
  49. package/dist/ephemeral-log.js +173 -0
  50. package/dist/ephemeral-log.js.map +1 -0
  51. package/dist/file-attach.d.ts +89 -0
  52. package/dist/file-attach.d.ts.map +1 -0
  53. package/dist/file-attach.js +571 -0
  54. package/dist/file-attach.js.map +1 -0
  55. package/dist/index.d.ts +29 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +43 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/init-wizard.d.ts +13 -0
  60. package/dist/init-wizard.d.ts.map +1 -0
  61. package/dist/init-wizard.js +328 -0
  62. package/dist/init-wizard.js.map +1 -0
  63. package/dist/log-entry.d.ts +104 -0
  64. package/dist/log-entry.d.ts.map +1 -0
  65. package/dist/log-entry.js +292 -0
  66. package/dist/log-entry.js.map +1 -0
  67. package/dist/log-projection.d.ts +73 -0
  68. package/dist/log-projection.d.ts.map +1 -0
  69. package/dist/log-projection.js +651 -0
  70. package/dist/log-projection.js.map +1 -0
  71. package/dist/mcp-client.d.ts +55 -0
  72. package/dist/mcp-client.d.ts.map +1 -0
  73. package/dist/mcp-client.js +402 -0
  74. package/dist/mcp-client.js.map +1 -0
  75. package/dist/model-selection.d.ts +16 -0
  76. package/dist/model-selection.d.ts.map +1 -0
  77. package/dist/model-selection.js +181 -0
  78. package/dist/model-selection.js.map +1 -0
  79. package/dist/network-retry.d.ts +38 -0
  80. package/dist/network-retry.d.ts.map +1 -0
  81. package/dist/network-retry.js +140 -0
  82. package/dist/network-retry.js.map +1 -0
  83. package/dist/persistence.d.ts +104 -0
  84. package/dist/persistence.d.ts.map +1 -0
  85. package/dist/persistence.js +644 -0
  86. package/dist/persistence.js.map +1 -0
  87. package/dist/primitives/context.d.ts +29 -0
  88. package/dist/primitives/context.d.ts.map +1 -0
  89. package/dist/primitives/context.js +85 -0
  90. package/dist/primitives/context.js.map +1 -0
  91. package/dist/progress.d.ts +51 -0
  92. package/dist/progress.d.ts.map +1 -0
  93. package/dist/progress.js +229 -0
  94. package/dist/progress.js.map +1 -0
  95. package/dist/provider-presets.d.ts +34 -0
  96. package/dist/provider-presets.d.ts.map +1 -0
  97. package/dist/provider-presets.js +181 -0
  98. package/dist/provider-presets.js.map +1 -0
  99. package/dist/providers/anthropic.d.ts +32 -0
  100. package/dist/providers/anthropic.d.ts.map +1 -0
  101. package/dist/providers/anthropic.js +450 -0
  102. package/dist/providers/anthropic.js.map +1 -0
  103. package/dist/providers/base.d.ts +135 -0
  104. package/dist/providers/base.d.ts.map +1 -0
  105. package/dist/providers/base.js +104 -0
  106. package/dist/providers/base.js.map +1 -0
  107. package/dist/providers/glm.d.ts +18 -0
  108. package/dist/providers/glm.d.ts.map +1 -0
  109. package/dist/providers/glm.js +59 -0
  110. package/dist/providers/glm.js.map +1 -0
  111. package/dist/providers/kimi.d.ts +23 -0
  112. package/dist/providers/kimi.d.ts.map +1 -0
  113. package/dist/providers/kimi.js +89 -0
  114. package/dist/providers/kimi.js.map +1 -0
  115. package/dist/providers/minimax.d.ts +20 -0
  116. package/dist/providers/minimax.d.ts.map +1 -0
  117. package/dist/providers/minimax.js +192 -0
  118. package/dist/providers/minimax.js.map +1 -0
  119. package/dist/providers/openai-chat.d.ts +33 -0
  120. package/dist/providers/openai-chat.d.ts.map +1 -0
  121. package/dist/providers/openai-chat.js +543 -0
  122. package/dist/providers/openai-chat.js.map +1 -0
  123. package/dist/providers/openai-responses.d.ts +26 -0
  124. package/dist/providers/openai-responses.d.ts.map +1 -0
  125. package/dist/providers/openai-responses.js +443 -0
  126. package/dist/providers/openai-responses.js.map +1 -0
  127. package/dist/providers/openrouter.d.ts +24 -0
  128. package/dist/providers/openrouter.d.ts.map +1 -0
  129. package/dist/providers/openrouter.js +177 -0
  130. package/dist/providers/openrouter.js.map +1 -0
  131. package/dist/providers/registry.d.ts +7 -0
  132. package/dist/providers/registry.d.ts.map +1 -0
  133. package/dist/providers/registry.js +38 -0
  134. package/dist/providers/registry.js.map +1 -0
  135. package/dist/security/path.d.ts +51 -0
  136. package/dist/security/path.d.ts.map +1 -0
  137. package/dist/security/path.js +187 -0
  138. package/dist/security/path.js.map +1 -0
  139. package/dist/security/sensitive-files.d.ts +3 -0
  140. package/dist/security/sensitive-files.d.ts.map +1 -0
  141. package/dist/security/sensitive-files.js +41 -0
  142. package/dist/security/sensitive-files.js.map +1 -0
  143. package/dist/session.d.ts +446 -0
  144. package/dist/session.d.ts.map +1 -0
  145. package/dist/session.js +4595 -0
  146. package/dist/session.js.map +1 -0
  147. package/dist/settings.d.ts +46 -0
  148. package/dist/settings.d.ts.map +1 -0
  149. package/dist/settings.js +134 -0
  150. package/dist/settings.js.map +1 -0
  151. package/dist/show-context.d.ts +35 -0
  152. package/dist/show-context.d.ts.map +1 -0
  153. package/dist/show-context.js +320 -0
  154. package/dist/show-context.js.map +1 -0
  155. package/dist/skills/loader.d.ts +49 -0
  156. package/dist/skills/loader.d.ts.map +1 -0
  157. package/dist/skills/loader.js +166 -0
  158. package/dist/skills/loader.js.map +1 -0
  159. package/dist/summarize-context.d.ts +29 -0
  160. package/dist/summarize-context.d.ts.map +1 -0
  161. package/dist/summarize-context.js +247 -0
  162. package/dist/summarize-context.js.map +1 -0
  163. package/dist/templates/loader.d.ts +104 -0
  164. package/dist/templates/loader.d.ts.map +1 -0
  165. package/dist/templates/loader.js +514 -0
  166. package/dist/templates/loader.js.map +1 -0
  167. package/dist/tools/basic.d.ts +29 -0
  168. package/dist/tools/basic.d.ts.map +1 -0
  169. package/dist/tools/basic.js +2079 -0
  170. package/dist/tools/basic.js.map +1 -0
  171. package/dist/tools/comm.d.ts +17 -0
  172. package/dist/tools/comm.d.ts.map +1 -0
  173. package/dist/tools/comm.js +192 -0
  174. package/dist/tools/comm.js.map +1 -0
  175. package/dist/tools/web-fetch.d.ts +11 -0
  176. package/dist/tools/web-fetch.d.ts.map +1 -0
  177. package/dist/tools/web-fetch.js +237 -0
  178. package/dist/tools/web-fetch.js.map +1 -0
  179. package/dist/tools/web-search.d.ts +24 -0
  180. package/dist/tools/web-search.d.ts.map +1 -0
  181. package/dist/tools/web-search.js +51 -0
  182. package/dist/tools/web-search.js.map +1 -0
  183. package/dist/tui/app.d.ts +35 -0
  184. package/dist/tui/app.d.ts.map +1 -0
  185. package/dist/tui/app.js +1042 -0
  186. package/dist/tui/app.js.map +1 -0
  187. package/dist/tui/checkbox-picker.d.ts +35 -0
  188. package/dist/tui/checkbox-picker.d.ts.map +1 -0
  189. package/dist/tui/checkbox-picker.js +85 -0
  190. package/dist/tui/checkbox-picker.js.map +1 -0
  191. package/dist/tui/command-picker.d.ts +31 -0
  192. package/dist/tui/command-picker.d.ts.map +1 -0
  193. package/dist/tui/command-picker.js +113 -0
  194. package/dist/tui/command-picker.js.map +1 -0
  195. package/dist/tui/components/ask-panel.d.ts +21 -0
  196. package/dist/tui/components/ask-panel.d.ts.map +1 -0
  197. package/dist/tui/components/ask-panel.js +81 -0
  198. package/dist/tui/components/ask-panel.js.map +1 -0
  199. package/dist/tui/components/conversation-panel.d.ts +68 -0
  200. package/dist/tui/components/conversation-panel.d.ts.map +1 -0
  201. package/dist/tui/components/conversation-panel.js +611 -0
  202. package/dist/tui/components/conversation-panel.js.map +1 -0
  203. package/dist/tui/components/input-panel.d.ts +27 -0
  204. package/dist/tui/components/input-panel.d.ts.map +1 -0
  205. package/dist/tui/components/input-panel.js +725 -0
  206. package/dist/tui/components/input-panel.js.map +1 -0
  207. package/dist/tui/components/logo-panel.d.ts +14 -0
  208. package/dist/tui/components/logo-panel.d.ts.map +1 -0
  209. package/dist/tui/components/logo-panel.js +37 -0
  210. package/dist/tui/components/logo-panel.js.map +1 -0
  211. package/dist/tui/components/plan-panel.d.ts +10 -0
  212. package/dist/tui/components/plan-panel.d.ts.map +1 -0
  213. package/dist/tui/components/plan-panel.js +8 -0
  214. package/dist/tui/components/plan-panel.js.map +1 -0
  215. package/dist/tui/components/status-bar.d.ts +24 -0
  216. package/dist/tui/components/status-bar.d.ts.map +1 -0
  217. package/dist/tui/components/status-bar.js +80 -0
  218. package/dist/tui/components/status-bar.js.map +1 -0
  219. package/dist/tui/input/editor-state.d.ts +22 -0
  220. package/dist/tui/input/editor-state.d.ts.map +1 -0
  221. package/dist/tui/input/editor-state.js +157 -0
  222. package/dist/tui/input/editor-state.js.map +1 -0
  223. package/dist/tui/input/keymap.d.ts +3 -0
  224. package/dist/tui/input/keymap.d.ts.map +1 -0
  225. package/dist/tui/input/keymap.js +72 -0
  226. package/dist/tui/input/keymap.js.map +1 -0
  227. package/dist/tui/input/paste-slots.d.ts +17 -0
  228. package/dist/tui/input/paste-slots.d.ts.map +1 -0
  229. package/dist/tui/input/paste-slots.js +46 -0
  230. package/dist/tui/input/paste-slots.js.map +1 -0
  231. package/dist/tui/input/paste.d.ts +15 -0
  232. package/dist/tui/input/paste.d.ts.map +1 -0
  233. package/dist/tui/input/paste.js +35 -0
  234. package/dist/tui/input/paste.js.map +1 -0
  235. package/dist/tui/input/protocol.d.ts +9 -0
  236. package/dist/tui/input/protocol.d.ts.map +1 -0
  237. package/dist/tui/input/protocol.js +387 -0
  238. package/dist/tui/input/protocol.js.map +1 -0
  239. package/dist/tui/input/sanitize.d.ts +6 -0
  240. package/dist/tui/input/sanitize.d.ts.map +1 -0
  241. package/dist/tui/input/sanitize.js +20 -0
  242. package/dist/tui/input/sanitize.js.map +1 -0
  243. package/dist/tui/input/types.d.ts +18 -0
  244. package/dist/tui/input/types.d.ts.map +1 -0
  245. package/dist/tui/input/types.js +2 -0
  246. package/dist/tui/input/types.js.map +1 -0
  247. package/dist/tui/launch.d.ts +23 -0
  248. package/dist/tui/launch.d.ts.map +1 -0
  249. package/dist/tui/launch.js +104 -0
  250. package/dist/tui/launch.js.map +1 -0
  251. package/dist/tui/theme.d.ts +20 -0
  252. package/dist/tui/theme.d.ts.map +1 -0
  253. package/dist/tui/theme.js +29 -0
  254. package/dist/tui/theme.js.map +1 -0
  255. package/dist/tui/types.d.ts +136 -0
  256. package/dist/tui/types.d.ts.map +1 -0
  257. package/dist/tui/types.js +9 -0
  258. package/dist/tui/types.js.map +1 -0
  259. package/package.json +76 -0
  260. package/prompts/sections/agents_md.md +23 -0
  261. package/prompts/sections/important_log.md +16 -0
  262. package/prompts/sections/system_mechanisms.md +18 -0
  263. package/prompts/tools/apply_patch.md +31 -0
  264. package/prompts/tools/ask.md +18 -0
  265. package/prompts/tools/bash.md +13 -0
  266. package/prompts/tools/bash_background.md +9 -0
  267. package/prompts/tools/bash_output.md +9 -0
  268. package/prompts/tools/check_status.md +3 -0
  269. package/prompts/tools/diff.md +5 -0
  270. package/prompts/tools/edit_file.md +11 -0
  271. package/prompts/tools/glob.md +7 -0
  272. package/prompts/tools/grep.md +20 -0
  273. package/prompts/tools/kill_agent.md +3 -0
  274. package/prompts/tools/kill_shell.md +5 -0
  275. package/prompts/tools/list_dir.md +5 -0
  276. package/prompts/tools/plan.md +252 -0
  277. package/prompts/tools/read_file.md +9 -0
  278. package/prompts/tools/show_context.md +12 -0
  279. package/prompts/tools/skill.md +7 -0
  280. package/prompts/tools/spawn_agent.md +195 -0
  281. package/prompts/tools/summarize_context.md +122 -0
  282. package/prompts/tools/test.md +5 -0
  283. package/prompts/tools/wait.md +17 -0
  284. package/prompts/tools/web_fetch.md +9 -0
  285. package/prompts/tools/web_search.md +5 -0
  286. package/prompts/tools/write_file.md +11 -0
  287. package/skills/.staging/.gitkeep +0 -0
  288. package/skills/explain-code/SKILL.md +15 -0
  289. package/skills/skill-manager/SKILL.md +83 -0
@@ -0,0 +1,862 @@
1
+ /**
2
+ * Extensible slash-command system.
3
+ *
4
+ * Usage:
5
+ *
6
+ * const registry = buildDefaultRegistry();
7
+ * const cmd = registry.lookup("/help");
8
+ * if (cmd) {
9
+ * await cmd.handler(ctx, "");
10
+ * }
11
+ */
12
+ import { existsSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { loadLog, validateAndRepairLog } from "./persistence.js";
15
+ import { formatDisplayModelName, formatScopedModelName, getThinkingLevels, } from "./config.js";
16
+ import { PROVIDER_PRESETS, } from "./provider-presets.js";
17
+ import { resolveModelSelection as resolveModelSelectionCore } from "./model-selection.js";
18
+ import { resolveSkillContent } from "./skills/loader.js";
19
+ import { ACCENT_PRESETS, setAccent, theme } from "./tui/theme.js";
20
+ import { hasOAuthTokens } from "./auth/openai-oauth.js";
21
+ export class CommandExitSignal extends Error {
22
+ code;
23
+ constructor(code = 0) {
24
+ super(`Command requested exit (${code})`);
25
+ this.name = "CommandExitSignal";
26
+ this.code = code;
27
+ }
28
+ }
29
+ export function isCommandExitSignal(err) {
30
+ return err instanceof CommandExitSignal ||
31
+ (err?.name === "CommandExitSignal" &&
32
+ typeof err?.code === "number");
33
+ }
34
+ // ------------------------------------------------------------------
35
+ // CommandRegistry
36
+ // ------------------------------------------------------------------
37
+ export class CommandRegistry {
38
+ _commands = new Map();
39
+ /** Register a command. Overwrites any existing command with the same name. */
40
+ register(cmd) {
41
+ this._commands.set(cmd.name, cmd);
42
+ }
43
+ /** Remove a command by its exact name. Returns true if it existed. */
44
+ unregister(name) {
45
+ return this._commands.delete(name);
46
+ }
47
+ /** Look up a command by its exact name. */
48
+ lookup(name) {
49
+ return this._commands.get(name);
50
+ }
51
+ /** Return all registered commands sorted alphabetically by name. */
52
+ getAll() {
53
+ return Array.from(this._commands.values()).sort((a, b) => a.name.localeCompare(b.name));
54
+ }
55
+ /** Return command names that start with the given prefix (for completion). */
56
+ getCompletions(prefix) {
57
+ const results = [];
58
+ for (const name of Array.from(this._commands.keys())) {
59
+ if (name.startsWith(prefix)) {
60
+ results.push(name);
61
+ }
62
+ }
63
+ return results.sort();
64
+ }
65
+ }
66
+ // ------------------------------------------------------------------
67
+ // Built-in command handlers
68
+ // ------------------------------------------------------------------
69
+ async function cmdHelp(ctx, _args) {
70
+ const lines = ["Commands:"];
71
+ for (const cmd of ctx.commandRegistry.getAll()) {
72
+ lines.push(` ${cmd.name} ${cmd.description}`);
73
+ }
74
+ lines.push("");
75
+ lines.push("Shortcuts:");
76
+ lines.push(" Enter Send message");
77
+ lines.push(" Option+Enter Insert newline");
78
+ lines.push(" Ctrl+N Insert newline");
79
+ lines.push(" Ctrl+G Toggle markdown raw view");
80
+ lines.push(" Cmd+Delete Delete to line start (Ghostty/kitty protocol)");
81
+ lines.push(" Alt+Backspace/Ctrl+W Delete previous word");
82
+ lines.push(" Ctrl+C Cancel / Exit");
83
+ lines.push(" @filename Attach file");
84
+ ctx.showMessage(lines.join("\n"));
85
+ }
86
+ async function cmdNew(ctx, _args) {
87
+ ctx.resetUiState();
88
+ ctx.autoSave();
89
+ // Clear session dir — a new directory will be created lazily on first save.
90
+ // This avoids creating an empty session file when the user doesn't send any messages.
91
+ if (ctx.store) {
92
+ ctx.store.clearSession();
93
+ }
94
+ // Full session reset — store is updated, then conversation re-initialized
95
+ // with correct paths. Equivalent to constructing a fresh Session.
96
+ ctx.session.resetForNewSession(ctx.store);
97
+ ctx.showMessage("--- New session started ---");
98
+ }
99
+ async function cmdSummarize(ctx, args) {
100
+ if (!ctx.onManualSummarizeRequested) {
101
+ ctx.showMessage("Manual summarize is not available in this UI.");
102
+ return;
103
+ }
104
+ ctx.onManualSummarizeRequested(args.trim());
105
+ }
106
+ async function cmdCompact(ctx, args) {
107
+ if (!ctx.onManualCompactRequested) {
108
+ ctx.showMessage("Manual compact is not available in this UI.");
109
+ return;
110
+ }
111
+ ctx.onManualCompactRequested(args.trim());
112
+ }
113
+ async function cmdResume(ctx, args) {
114
+ const store = ctx.store;
115
+ if (!store) {
116
+ ctx.showMessage("Session persistence not available.");
117
+ return;
118
+ }
119
+ const sessions = store.listSessions();
120
+ if (sessions.length === 0) {
121
+ ctx.showMessage("No saved sessions found.");
122
+ return;
123
+ }
124
+ const trimmed = args.trim();
125
+ if (!trimmed) {
126
+ // List sessions
127
+ const lines = ["Recent Sessions:"];
128
+ const shown = sessions.slice(0, 10);
129
+ for (let i = 0; i < shown.length; i++) {
130
+ const s = shown[i];
131
+ const created = s.created
132
+ ? s.created.slice(0, 19).replace("T", " ")
133
+ : "?";
134
+ const summary = truncateDisplayText(s.summary || "(empty)", 25);
135
+ lines.push(` ${i + 1} ${created} ${s.turns}t ${summary}`);
136
+ }
137
+ lines.push("");
138
+ lines.push("Use /resume <number> to load a session.");
139
+ ctx.showMessage(lines.join("\n"));
140
+ return;
141
+ }
142
+ // Load specific session
143
+ const idx = parseInt(trimmed, 10) - 1;
144
+ if (isNaN(idx)) {
145
+ ctx.showMessage(`Invalid session number: ${trimmed}`);
146
+ return;
147
+ }
148
+ if (idx < 0 || idx >= sessions.length) {
149
+ ctx.showMessage(`Session number out of range (1-${sessions.length}).`);
150
+ return;
151
+ }
152
+ // Auto-save current first
153
+ ctx.autoSave();
154
+ const target = sessions[idx];
155
+ const session = ctx.session;
156
+ const logJsonPath = join(target.path, "log.json");
157
+ const hasLogJson = existsSync(logJsonPath);
158
+ if (!hasLogJson) {
159
+ ctx.showMessage("No log.json found for this session.");
160
+ return;
161
+ }
162
+ let logData;
163
+ try {
164
+ logData = loadLog(target.path);
165
+ }
166
+ catch (e) {
167
+ ctx.showMessage(`Failed to load log: ${e instanceof Error ? e.message : String(e)}`);
168
+ return;
169
+ }
170
+ // Validate and repair
171
+ const { entries: repairedEntries, repaired, warnings } = validateAndRepairLog(logData.entries);
172
+ if (repaired) {
173
+ for (const w of warnings) {
174
+ ctx.showMessage(`[repair] ${w}`);
175
+ }
176
+ }
177
+ ctx.resetUiState();
178
+ try {
179
+ session.restoreFromLog(logData.meta, repairedEntries, logData.idAllocator);
180
+ }
181
+ catch (e) {
182
+ ctx.showMessage(`Failed to restore session: ${e instanceof Error ? e.message : String(e)}`);
183
+ return;
184
+ }
185
+ // Point store at the loaded session
186
+ store.sessionDir = target.path;
187
+ if (typeof session.setStore === "function") {
188
+ session.setStore(store);
189
+ }
190
+ }
191
+ function buildResumeOptionLabel(index, created, turns, summary) {
192
+ const date = (created || "").slice(0, 16);
193
+ return `${index + 1}. ${date} ${turns ?? 0} turns ${truncateDisplayText(summary || "", 25)}`;
194
+ }
195
+ function truncateDisplayText(text, maxChars) {
196
+ return Array.from(text).slice(0, maxChars).join("");
197
+ }
198
+ function resumeOptions(ctx) {
199
+ const store = ctx.store;
200
+ if (!store)
201
+ return [];
202
+ const sessions = store.listSessions();
203
+ return sessions.map((s, i) => ({
204
+ label: buildResumeOptionLabel(i, s.created, s.turns, s.summary),
205
+ value: String(i + 1),
206
+ }));
207
+ }
208
+ async function cmdQuit(ctx, _args) {
209
+ if (ctx.exit) {
210
+ await ctx.exit();
211
+ return;
212
+ }
213
+ ctx.autoSave();
214
+ try {
215
+ if (typeof ctx.session.close === "function") {
216
+ await ctx.session.close();
217
+ }
218
+ }
219
+ catch {
220
+ // ignore
221
+ }
222
+ // Non-TUI callers decide how to handle shutdown.
223
+ throw new CommandExitSignal(0);
224
+ }
225
+ function currentSessionModelDisplayName(session) {
226
+ return formatDisplayModelName(session.primaryAgent?.modelConfig?.provider, session.currentModelName ?? session.primaryAgent?.modelConfig?.model);
227
+ }
228
+ function persistGlobalPreferences(ctx) {
229
+ if (!ctx.store || typeof ctx.store.saveGlobalPreferences !== "function")
230
+ return;
231
+ if (typeof ctx.session.getGlobalPreferences !== "function")
232
+ return;
233
+ try {
234
+ ctx.store.saveGlobalPreferences(ctx.session.getGlobalPreferences());
235
+ }
236
+ catch {
237
+ // Ignore preference persistence failures during command execution.
238
+ }
239
+ }
240
+ function thinkingOptions(ctx) {
241
+ const session = ctx.session;
242
+ const model = session.currentModelName ?? "";
243
+ const levels = getThinkingLevels(model);
244
+ const current = session.thinkingLevel ?? "default";
245
+ const opts = [];
246
+ // "default" is always available as reset option
247
+ opts.push({
248
+ label: current === "default" ? "default (current)" : "default",
249
+ value: "default",
250
+ });
251
+ for (const level of levels) {
252
+ const isCurrent = current === level;
253
+ opts.push({
254
+ label: isCurrent ? `${level} (current)` : level,
255
+ value: level,
256
+ });
257
+ }
258
+ return opts;
259
+ }
260
+ async function cmdThinking(ctx, args) {
261
+ const session = ctx.session;
262
+ const model = session.currentModelName;
263
+ const displayModel = currentSessionModelDisplayName(session);
264
+ const levels = getThinkingLevels(model);
265
+ const trimmed = args.trim().toLowerCase();
266
+ if (!trimmed) {
267
+ // No arg: show info (fallback for non-overlay usage)
268
+ const current = session.thinkingLevel;
269
+ if (!levels.length) {
270
+ ctx.showMessage(`Model '${displayModel}' does not support configurable thinking levels.`);
271
+ }
272
+ else {
273
+ ctx.showMessage(`Thinking level: ${current}\n` +
274
+ `Available levels for ${displayModel}: ${levels.join(", ")}`);
275
+ }
276
+ return;
277
+ }
278
+ if (trimmed === "default") {
279
+ session.thinkingLevel = "default";
280
+ persistGlobalPreferences(ctx);
281
+ ctx.showMessage("Thinking level reset to provider default.");
282
+ return;
283
+ }
284
+ if (levels.length && !levels.includes(trimmed)) {
285
+ ctx.showMessage(`Invalid level '${trimmed}' for ${displayModel}.\n` +
286
+ `Available: ${levels.join(", ")}`);
287
+ return;
288
+ }
289
+ session.thinkingLevel = trimmed;
290
+ persistGlobalPreferences(ctx);
291
+ ctx.showMessage(`Thinking level set to: ${trimmed}`);
292
+ }
293
+ function cacheHitOptions(ctx) {
294
+ const session = ctx.session;
295
+ const enabled = session.cacheHitEnabled ?? true;
296
+ return [
297
+ { label: enabled ? "ON (current)" : "ON", value: "on" },
298
+ { label: enabled ? "OFF" : "OFF (current)", value: "off" },
299
+ ];
300
+ }
301
+ async function cmdCacheHit(ctx, args) {
302
+ const session = ctx.session;
303
+ const trimmed = args.trim().toLowerCase();
304
+ if (trimmed === "on") {
305
+ session.cacheHitEnabled = true;
306
+ }
307
+ else if (trimmed === "off") {
308
+ session.cacheHitEnabled = false;
309
+ }
310
+ else {
311
+ // No argument toggles the current setting.
312
+ session.cacheHitEnabled = !session.cacheHitEnabled;
313
+ }
314
+ persistGlobalPreferences(ctx);
315
+ const state = session.cacheHitEnabled ? "ON" : "OFF";
316
+ const provider = session.primaryAgent?.modelConfig?.provider ?? "";
317
+ let note = "";
318
+ if (provider === "anthropic") {
319
+ note = session.cacheHitEnabled
320
+ ? " (cache_control markers will be sent)"
321
+ : " (cache_control markers disabled)";
322
+ }
323
+ else if (provider === "openrouter") {
324
+ note = " (Cache is automatic via OpenRouter for supported models)";
325
+ }
326
+ else {
327
+ note = " (Cache is automatic for this provider)";
328
+ }
329
+ ctx.showMessage(`Prompt caching: ${state}${note}`);
330
+ }
331
+ const PROVIDER_KEY_GROUP_ALIASES = {
332
+ "openai-chat": "openai",
333
+ "openai-responses": "openai",
334
+ "openai-codex": "openai-codex", // Separate group — uses OAuth, not shared API key
335
+ "kimi-cn": "kimi",
336
+ "kimi-ai": "kimi",
337
+ "kimi-code": "kimi",
338
+ "glm-intl": "glm",
339
+ "glm-code": "glm",
340
+ "glm-intl-code": "glm",
341
+ "minimax-cn": "minimax",
342
+ };
343
+ function providerKeyGroup(provider) {
344
+ return PROVIDER_KEY_GROUP_ALIASES[provider] ?? provider;
345
+ }
346
+ const PROVIDER_ENV_VARS = (() => {
347
+ const map = new Map();
348
+ for (const p of PROVIDER_PRESETS) {
349
+ const group = providerKeyGroup(p.id);
350
+ if (!map.has(group))
351
+ map.set(group, p.envVar);
352
+ }
353
+ return map;
354
+ })();
355
+ function readModelEntries(config) {
356
+ if (typeof config?.listModelEntries === "function") {
357
+ try {
358
+ const entries = config.listModelEntries();
359
+ if (Array.isArray(entries))
360
+ return entries;
361
+ }
362
+ catch {
363
+ // Fall through to compatibility mode.
364
+ }
365
+ }
366
+ // Compatibility for old/partial config stubs (best-effort only).
367
+ const out = [];
368
+ for (const name of config?.modelNames ?? []) {
369
+ try {
370
+ const mc = config.getModel(name);
371
+ out.push({
372
+ name,
373
+ provider: String(mc.provider ?? ""),
374
+ model: String(mc.model ?? ""),
375
+ apiKeyRaw: String(mc.apiKey ?? ""),
376
+ hasResolvedApiKey: Boolean(mc.apiKey),
377
+ });
378
+ }
379
+ catch {
380
+ // Ignore invalid entries.
381
+ }
382
+ }
383
+ return out;
384
+ }
385
+ function parseModelArgs(args) {
386
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
387
+ const target = tokens[0] ?? "";
388
+ const rest = tokens.slice(1);
389
+ let apiKey;
390
+ for (const t of rest) {
391
+ if (t.startsWith("key=")) {
392
+ apiKey = t.slice("key=".length);
393
+ break;
394
+ }
395
+ if (t.startsWith("api_key=")) {
396
+ apiKey = t.slice("api_key=".length);
397
+ break;
398
+ }
399
+ }
400
+ if (!apiKey && rest.length === 1) {
401
+ apiKey = rest[0];
402
+ }
403
+ return { target, apiKey };
404
+ }
405
+ function parseProviderModelTarget(target) {
406
+ const idx = target.indexOf(":");
407
+ if (idx <= 0 || idx >= target.length - 1)
408
+ return null;
409
+ return {
410
+ provider: target.slice(0, idx),
411
+ model: target.slice(idx + 1),
412
+ };
413
+ }
414
+ function hasEnvApiKey(envVar) {
415
+ if (!envVar)
416
+ return false;
417
+ const raw = process.env[envVar];
418
+ return typeof raw === "string" && raw.trim() !== "";
419
+ }
420
+ function getProviderKeySource(entries, provider) {
421
+ const group = providerKeyGroup(provider);
422
+ const fromConfig = entries.find((e) => providerKeyGroup(e.provider) === group && e.hasResolvedApiKey && e.apiKeyRaw.trim() !== "");
423
+ if (fromConfig)
424
+ return fromConfig.apiKeyRaw;
425
+ const envVar = PROVIDER_ENV_VARS.get(group);
426
+ if (hasEnvApiKey(envVar))
427
+ return `\${${envVar}}`;
428
+ // OAuth fallback for openai-codex
429
+ if (provider === "openai-codex") {
430
+ try {
431
+ if (hasOAuthTokens())
432
+ return "oauth:openai-codex";
433
+ }
434
+ catch { /* ignore */ }
435
+ }
436
+ return undefined;
437
+ }
438
+ function runtimeModelName(provider, model) {
439
+ const slug = (s) => s
440
+ .toLowerCase()
441
+ .replace(/[^a-z0-9]+/g, "-")
442
+ .replace(/^-+|-+$/g, "");
443
+ return `runtime-${slug(provider)}-${slug(model)}`;
444
+ }
445
+ function formatPresetPickerLabel(provider, presetModel) {
446
+ let label = formatDisplayModelName(provider, presetModel.id);
447
+ if (presetModel.optionNote) {
448
+ label = `${label} (${presetModel.optionNote})`;
449
+ }
450
+ return label;
451
+ }
452
+ function formatPresetSelectedHint(provider, presetModel) {
453
+ let label = formatScopedModelName(provider, presetModel.id);
454
+ if (presetModel.optionNote) {
455
+ label = `${label} (${presetModel.optionNote})`;
456
+ }
457
+ return label;
458
+ }
459
+ export function resolveModelSelection(session, target, apiKey) {
460
+ return resolveModelSelectionCore(session, target, apiKey);
461
+ }
462
+ /**
463
+ * Build model children (leaf-level options) for a single provider.
464
+ */
465
+ function buildModelChildren(provider, byProvider, providerHasKey, session, currentProvider, currentModel) {
466
+ const models = Array.from((byProvider.get(provider) ?? new Map()).entries());
467
+ models.sort((a, b) => a[1].label.localeCompare(b[1].label));
468
+ const children = [];
469
+ for (const [selectionKey, item] of models) {
470
+ const runtimeSelectionName = runtimeModelName(provider, selectionKey);
471
+ const isCurrent = session.currentModelConfigName === runtimeSelectionName
472
+ || (selectionKey === item.model
473
+ && provider === currentProvider
474
+ && item.model === currentModel);
475
+ const missingApiKey = !providerHasKey.get(providerKeyGroup(provider));
476
+ const missingHint = provider === "openai-codex"
477
+ ? "not logged in: run longeragent oauth"
478
+ : "key missing: run longeragent init";
479
+ let label = item.label;
480
+ if (isCurrent && missingApiKey) {
481
+ label = `${label} (current, ${missingHint})`;
482
+ }
483
+ else if (isCurrent) {
484
+ label = `${label} (current)`;
485
+ }
486
+ else if (missingApiKey) {
487
+ label = `${label} (${missingHint})`;
488
+ }
489
+ children.push({
490
+ label,
491
+ value: `${provider}:${selectionKey}`,
492
+ });
493
+ }
494
+ return children;
495
+ }
496
+ /** Display names for OpenRouter vendor prefixes. */
497
+ const OPENROUTER_VENDOR_NAMES = {
498
+ "anthropic": "Anthropic",
499
+ "openai": "OpenAI",
500
+ "moonshotai": "Kimi",
501
+ "minimax": "MiniMax",
502
+ "z-ai": "GLM / Zhipu",
503
+ };
504
+ /**
505
+ * Build options for /model picker.
506
+ *
507
+ * Supports three structures:
508
+ * - Two-level: provider → model (for ungrouped providers like anthropic, openai)
509
+ * - Three-level via group field: group → sub-provider → model (kimi, glm, minimax)
510
+ * - Three-level via vendor prefix: openrouter → vendor → model
511
+ */
512
+ function modelOptions(ctx) {
513
+ const session = ctx.session;
514
+ const config = session.config;
515
+ if (!config)
516
+ return [];
517
+ const entries = readModelEntries(config);
518
+ const currentProvider = String(session.primaryAgent?.modelConfig?.provider ?? "");
519
+ const currentModel = String(session.primaryAgent?.modelConfig?.model ?? "");
520
+ // Gather all providers/models:
521
+ // 1) preset catalog
522
+ // 2) user-defined config models (for custom IDs/providers)
523
+ const byProvider = new Map();
524
+ const providerOrder = [];
525
+ const addModel = (provider, selectionKey, model, label) => {
526
+ if (!provider || !selectionKey || !model)
527
+ return;
528
+ if (!byProvider.has(provider)) {
529
+ byProvider.set(provider, new Map());
530
+ providerOrder.push(provider);
531
+ }
532
+ if (!byProvider.get(provider).has(selectionKey)) {
533
+ byProvider.get(provider).set(selectionKey, { model, label });
534
+ }
535
+ };
536
+ for (const preset of PROVIDER_PRESETS) {
537
+ for (const m of preset.models) {
538
+ addModel(preset.id, m.key, m.id, formatPresetPickerLabel(preset.id, m));
539
+ }
540
+ }
541
+ for (const e of entries) {
542
+ addModel(e.provider, e.model, e.model, formatDisplayModelName(e.provider, e.model));
543
+ }
544
+ // Provider-level key status from config/env/current model.
545
+ const providerHasKey = new Map();
546
+ for (const e of entries) {
547
+ if (e.hasResolvedApiKey) {
548
+ providerHasKey.set(providerKeyGroup(e.provider), true);
549
+ }
550
+ }
551
+ for (const [group, envVar] of PROVIDER_ENV_VARS) {
552
+ if (hasEnvApiKey(envVar))
553
+ providerHasKey.set(group, true);
554
+ }
555
+ // OAuth: check token store for openai-codex (sync, no HTTP)
556
+ try {
557
+ if (hasOAuthTokens())
558
+ providerHasKey.set("openai-codex", true);
559
+ }
560
+ catch { /* auth module not available */ }
561
+ const currentProviderGroup = providerKeyGroup(currentProvider);
562
+ if (session.primaryAgent?.modelConfig?.apiKey) {
563
+ providerHasKey.set(currentProviderGroup, true);
564
+ }
565
+ // Build a lookup from provider id → preset (for group metadata).
566
+ const presetById = new Map();
567
+ for (const p of PROVIDER_PRESETS) {
568
+ presetById.set(p.id, p);
569
+ }
570
+ const options = [];
571
+ const processed = new Set();
572
+ for (const provider of providerOrder) {
573
+ if (processed.has(provider))
574
+ continue;
575
+ processed.add(provider);
576
+ const preset = presetById.get(provider);
577
+ // ── Three-level: grouped providers (kimi, glm, minimax) ──
578
+ if (preset?.group) {
579
+ // Collect all providers in this group (preserving providerOrder).
580
+ const groupMembers = providerOrder.filter((p) => {
581
+ const pp = presetById.get(p);
582
+ return pp?.group === preset.group;
583
+ });
584
+ for (const gp of groupMembers)
585
+ processed.add(gp);
586
+ const subOptions = [];
587
+ let groupHasCurrent = false;
588
+ for (const gp of groupMembers) {
589
+ const gpPreset = presetById.get(gp);
590
+ const children = buildModelChildren(gp, byProvider, providerHasKey, session, currentProvider, currentModel);
591
+ const subHasCurrent = children.some((c) => c.label.includes("(current)"));
592
+ if (subHasCurrent)
593
+ groupHasCurrent = true;
594
+ const subLabel = gpPreset?.subLabel ?? gp;
595
+ subOptions.push({
596
+ label: subHasCurrent ? `${subLabel} (current)` : subLabel,
597
+ value: gp,
598
+ children,
599
+ });
600
+ }
601
+ const groupLabel = preset.groupLabel ?? preset.group;
602
+ options.push({
603
+ label: groupHasCurrent ? `${groupLabel} (current)` : groupLabel,
604
+ value: preset.group,
605
+ children: subOptions,
606
+ });
607
+ continue;
608
+ }
609
+ // ── Three-level: OpenRouter (sub-group by vendor prefix) ──
610
+ if (provider === "openrouter") {
611
+ const children = buildModelChildren(provider, byProvider, providerHasKey, session, currentProvider, currentModel);
612
+ // Group children by vendor prefix (e.g. "anthropic/..." → "anthropic").
613
+ const vendorGroups = new Map();
614
+ const vendorOrder = [];
615
+ for (const child of children) {
616
+ const modelKey = child.value.split(":")[1] ?? "";
617
+ const slashIdx = modelKey.indexOf("/");
618
+ const vendor = slashIdx > 0 ? modelKey.slice(0, slashIdx) : "other";
619
+ if (!vendorGroups.has(vendor)) {
620
+ vendorGroups.set(vendor, []);
621
+ vendorOrder.push(vendor);
622
+ }
623
+ vendorGroups.get(vendor).push(child);
624
+ }
625
+ const subOptions = [];
626
+ let openrouterHasCurrent = false;
627
+ for (const vendor of vendorOrder) {
628
+ const vendorChildren = vendorGroups.get(vendor);
629
+ const vendorHasCurrent = vendorChildren.some((c) => c.label.includes("(current)"));
630
+ if (vendorHasCurrent)
631
+ openrouterHasCurrent = true;
632
+ const displayName = OPENROUTER_VENDOR_NAMES[vendor] ?? vendor;
633
+ subOptions.push({
634
+ label: vendorHasCurrent ? `${displayName} (current)` : displayName,
635
+ value: `openrouter-${vendor}`,
636
+ children: vendorChildren,
637
+ });
638
+ }
639
+ options.push({
640
+ label: openrouterHasCurrent ? "openrouter (current)" : "openrouter",
641
+ value: "openrouter",
642
+ children: subOptions,
643
+ });
644
+ continue;
645
+ }
646
+ // ── Two-level: ungrouped providers (anthropic, openai, user-defined) ──
647
+ const children = buildModelChildren(provider, byProvider, providerHasKey, session, currentProvider, currentModel);
648
+ const hasCurrent = children.some((c) => c.label.includes("(current)"));
649
+ options.push({
650
+ label: hasCurrent ? `${provider} (current)` : provider,
651
+ value: provider,
652
+ children,
653
+ });
654
+ }
655
+ return options;
656
+ }
657
+ /**
658
+ * /model command: switch model by creating a new session.
659
+ *
660
+ * The selected value is either a config name or a provider:model target.
661
+ */
662
+ async function cmdModel(ctx, args) {
663
+ const session = ctx.session;
664
+ const trimmed = args.trim();
665
+ if (!trimmed) {
666
+ const displayCurrent = currentSessionModelDisplayName(session) || "unknown";
667
+ const current = session.currentModelConfigName
668
+ ? `${session.currentModelConfigName} (${displayCurrent})`
669
+ : displayCurrent;
670
+ ctx.showMessage(`Current model: ${current}\n` +
671
+ "Use /model to select a new model.\n" +
672
+ "For models marked 'key missing', run 'longeragent init' (or use /model provider:model key=YOUR_API_KEY).");
673
+ return;
674
+ }
675
+ if (!session.switchModel) {
676
+ ctx.showMessage("Model switching is not supported in this session.");
677
+ return;
678
+ }
679
+ try {
680
+ const { target, apiKey } = parseModelArgs(trimmed);
681
+ const resolvedSelection = resolveModelSelection(session, target, apiKey);
682
+ const { selectedConfigName, selectedHint } = resolvedSelection;
683
+ // Save current session before switching
684
+ ctx.resetUiState();
685
+ ctx.autoSave();
686
+ if (ctx.store) {
687
+ ctx.store.clearSession();
688
+ }
689
+ // Switch model, then create fresh session
690
+ session.switchModel(selectedConfigName);
691
+ session.setPersistedModelSelection?.({
692
+ modelConfigName: selectedConfigName,
693
+ modelProvider: resolvedSelection.modelProvider,
694
+ modelSelectionKey: resolvedSelection.modelSelectionKey,
695
+ modelId: resolvedSelection.modelId,
696
+ });
697
+ session.resetForNewSession(ctx.store);
698
+ persistGlobalPreferences(ctx);
699
+ const mc = session.primaryAgent?.modelConfig;
700
+ if (mc) {
701
+ ctx.showMessage(`--- New session with ${selectedHint} (${formatScopedModelName(mc.provider, mc.model)}) ---\n` +
702
+ ` Context: ${(mc.contextLength ?? 0).toLocaleString()} tokens`);
703
+ }
704
+ else {
705
+ ctx.showMessage(`--- New session with ${selectedHint} ---`);
706
+ }
707
+ }
708
+ catch (e) {
709
+ ctx.showMessage(`Failed to switch model: ${e instanceof Error ? e.message : String(e)}`);
710
+ }
711
+ }
712
+ // ------------------------------------------------------------------
713
+ // /theme command
714
+ // ------------------------------------------------------------------
715
+ function themeOptions(_ctx) {
716
+ const current = theme.accent;
717
+ return ACCENT_PRESETS.map((preset) => {
718
+ const isCurrent = preset.value === current;
719
+ return {
720
+ label: isCurrent ? `${preset.label} (current)` : preset.label,
721
+ value: preset.value,
722
+ };
723
+ });
724
+ }
725
+ async function cmdTheme(ctx, args) {
726
+ const trimmed = args.trim();
727
+ if (!trimmed) {
728
+ ctx.showMessage(`Current accent: ${theme.accent}\n` +
729
+ "Use /theme to select a new accent color.");
730
+ return;
731
+ }
732
+ // Accept preset label (case-insensitive) or raw hex value
733
+ const preset = ACCENT_PRESETS.find((p) => p.value === trimmed || p.label.toLowerCase() === trimmed.toLowerCase());
734
+ const color = preset ? preset.value : trimmed;
735
+ // Basic hex validation
736
+ if (!/^#[0-9a-fA-F]{6}$/.test(color)) {
737
+ ctx.showMessage(`Invalid color: "${trimmed}". Use a preset name or a hex color like #3b82f6.`);
738
+ return;
739
+ }
740
+ setAccent(color);
741
+ ctx.session.accentColor = color;
742
+ persistGlobalPreferences(ctx);
743
+ const label = preset ? `${preset.label} (${color})` : color;
744
+ ctx.showMessage(`Accent color set to: ${label}`);
745
+ }
746
+ // ------------------------------------------------------------------
747
+ // Registry builder
748
+ // ------------------------------------------------------------------
749
+ /**
750
+ * Build the default command registry with all built-in commands.
751
+ */
752
+ export function buildDefaultRegistry() {
753
+ const registry = new CommandRegistry();
754
+ registry.register({ name: "/help", description: "Show commands and shortcuts", handler: cmdHelp });
755
+ registry.register({ name: "/compact", description: "Manually compact the active context", handler: cmdCompact });
756
+ registry.register({ name: "/new", description: "Start a new session", handler: cmdNew });
757
+ registry.register({ name: "/resume", description: "Resume a previous session", handler: cmdResume, options: resumeOptions });
758
+ registry.register({ name: "/summarize", description: "Manually summarize older context", handler: cmdSummarize });
759
+ registry.register({ name: "/model", description: "Switch model", handler: cmdModel, options: modelOptions });
760
+ registry.register({ name: "/quit", description: "Exit the application", handler: cmdQuit });
761
+ registry.register({ name: "/exit", description: "Exit the application", handler: cmdQuit });
762
+ registry.register({ name: "/thinking", description: "Set thinking level", handler: cmdThinking, options: thinkingOptions });
763
+ registry.register({ name: "/cachehit", description: "Prompt caching", handler: cmdCacheHit, options: cacheHitOptions });
764
+ registry.register({ name: "/theme", description: "Change accent color", handler: cmdTheme, options: themeOptions });
765
+ registry.register({ name: "/skills", description: "Manage installed skills", handler: cmdSkills, options: skillsOptions, checkboxMode: true });
766
+ return registry;
767
+ }
768
+ // ------------------------------------------------------------------
769
+ // /skills command
770
+ // ------------------------------------------------------------------
771
+ function skillsOptions(ctx) {
772
+ const session = ctx.session;
773
+ if (!session?.getAllSkillNames)
774
+ return [];
775
+ const allSkills = session.getAllSkillNames();
776
+ if (allSkills.length === 0)
777
+ return [];
778
+ return allSkills.map((s) => ({
779
+ label: `${s.name} ${s.description.length > 50 ? s.description.slice(0, 47) + "..." : s.description}`,
780
+ value: s.name,
781
+ checked: s.enabled,
782
+ }));
783
+ }
784
+ async function cmdSkills(ctx, args) {
785
+ const session = ctx.session;
786
+ if (!session?.getAllSkillNames) {
787
+ ctx.showMessage("Skills system not available.");
788
+ return;
789
+ }
790
+ const trimmed = args.trim();
791
+ if (!trimmed) {
792
+ // No args — show list
793
+ const allSkills = session.getAllSkillNames();
794
+ if (allSkills.length === 0) {
795
+ ctx.showMessage("No skills installed.");
796
+ return;
797
+ }
798
+ const lines = ["Installed skills:"];
799
+ for (const s of allSkills) {
800
+ lines.push(` ${s.enabled ? "[x]" : "[ ]"} ${s.name} — ${s.description}`);
801
+ }
802
+ ctx.showMessage(lines.join("\n"));
803
+ return;
804
+ }
805
+ // Checkbox picker submits comma-separated enabled skill names
806
+ // Parse: all items were submitted, enabled ones are in the args
807
+ const enabledNames = new Set(trimmed.split(",").map((s) => s.trim()).filter(Boolean));
808
+ const allSkills = session.getAllSkillNames();
809
+ const oldSkills = session.skills;
810
+ for (const s of allSkills) {
811
+ session.setSkillEnabled(s.name, enabledNames.has(s.name));
812
+ }
813
+ session.reloadSkills();
814
+ // Re-register slash commands
815
+ reRegisterSkillCommands(ctx.commandRegistry, oldSkills, session.skills);
816
+ const enabledCount = enabledNames.size;
817
+ const totalCount = allSkills.length;
818
+ ctx.showMessage(`Skills updated: ${enabledCount}/${totalCount} enabled.`);
819
+ persistGlobalPreferences(ctx);
820
+ }
821
+ // ------------------------------------------------------------------
822
+ // Skill command registration
823
+ // ------------------------------------------------------------------
824
+ /**
825
+ * Register slash commands for user-invocable skills.
826
+ *
827
+ * Each skill with `userInvocable === true` gets a `/skill-name` command.
828
+ * When invoked, the skill content is injected and a turn is triggered.
829
+ */
830
+ export function registerSkillCommands(registry, skills) {
831
+ for (const skill of skills.values()) {
832
+ if (!skill.userInvocable)
833
+ continue;
834
+ const captured = skill; // capture for closure
835
+ const desc = captured.description.length > 60
836
+ ? captured.description.slice(0, 57) + "..."
837
+ : captured.description;
838
+ registry.register({
839
+ name: "/" + captured.name,
840
+ description: desc,
841
+ handler: async (ctx, args) => {
842
+ const content = resolveSkillContent(captured, args);
843
+ const tagged = `[SKILL: ${captured.name}]\n\n${content}`;
844
+ ctx.showMessage(`Loaded skill: ${captured.name}`);
845
+ if (ctx.onTurnRequested) {
846
+ ctx.onTurnRequested(tagged);
847
+ }
848
+ },
849
+ });
850
+ }
851
+ }
852
+ /**
853
+ * Unregister old skill commands, then register new ones.
854
+ * Used after reloadSkills() to keep slash commands in sync.
855
+ */
856
+ export function reRegisterSkillCommands(registry, oldSkills, newSkills) {
857
+ for (const skill of oldSkills.values()) {
858
+ registry.unregister("/" + skill.name);
859
+ }
860
+ registerSkillCommands(registry, newSkills);
861
+ }
862
+ //# sourceMappingURL=commands.js.map