@mariozechner/pi-coding-agent 0.23.4 → 0.24.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 (104) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +20 -13
  3. package/dist/core/custom-tools/loader.d.ts.map +1 -1
  4. package/dist/core/custom-tools/loader.js +56 -6
  5. package/dist/core/custom-tools/loader.js.map +1 -1
  6. package/dist/core/custom-tools/types.d.ts +9 -1
  7. package/dist/core/custom-tools/types.d.ts.map +1 -1
  8. package/dist/core/custom-tools/types.js.map +1 -1
  9. package/dist/core/hooks/index.d.ts +2 -1
  10. package/dist/core/hooks/index.d.ts.map +1 -1
  11. package/dist/core/hooks/index.js +1 -0
  12. package/dist/core/hooks/index.js.map +1 -1
  13. package/dist/core/hooks/runner.d.ts.map +1 -1
  14. package/dist/core/hooks/runner.js +44 -4
  15. package/dist/core/hooks/runner.js.map +1 -1
  16. package/dist/core/hooks/tool-wrapper.d.ts.map +1 -1
  17. package/dist/core/hooks/tool-wrapper.js +5 -9
  18. package/dist/core/hooks/tool-wrapper.js.map +1 -1
  19. package/dist/core/hooks/types.d.ts +73 -11
  20. package/dist/core/hooks/types.d.ts.map +1 -1
  21. package/dist/core/hooks/types.js +22 -1
  22. package/dist/core/hooks/types.js.map +1 -1
  23. package/dist/core/skills.d.ts +18 -5
  24. package/dist/core/skills.d.ts.map +1 -1
  25. package/dist/core/skills.js +183 -72
  26. package/dist/core/skills.js.map +1 -1
  27. package/dist/core/slash-commands.d.ts.map +1 -1
  28. package/dist/core/slash-commands.js +2 -2
  29. package/dist/core/slash-commands.js.map +1 -1
  30. package/dist/core/system-prompt.d.ts.map +1 -1
  31. package/dist/core/system-prompt.js +2 -2
  32. package/dist/core/system-prompt.js.map +1 -1
  33. package/dist/core/tools/bash.d.ts +5 -0
  34. package/dist/core/tools/bash.d.ts.map +1 -1
  35. package/dist/core/tools/bash.js.map +1 -1
  36. package/dist/core/tools/find.d.ts +5 -0
  37. package/dist/core/tools/find.d.ts.map +1 -1
  38. package/dist/core/tools/find.js.map +1 -1
  39. package/dist/core/tools/grep.d.ts +6 -0
  40. package/dist/core/tools/grep.d.ts.map +1 -1
  41. package/dist/core/tools/grep.js.map +1 -1
  42. package/dist/core/tools/index.d.ts +6 -5
  43. package/dist/core/tools/index.d.ts.map +1 -1
  44. package/dist/core/tools/index.js.map +1 -1
  45. package/dist/core/tools/ls.d.ts +5 -0
  46. package/dist/core/tools/ls.d.ts.map +1 -1
  47. package/dist/core/tools/ls.js.map +1 -1
  48. package/dist/core/tools/read.d.ts +4 -0
  49. package/dist/core/tools/read.d.ts.map +1 -1
  50. package/dist/core/tools/read.js.map +1 -1
  51. package/dist/index.d.ts +5 -3
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +5 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/main.d.ts.map +1 -1
  56. package/dist/main.js +4 -0
  57. package/dist/main.js.map +1 -1
  58. package/dist/modes/interactive/components/custom-editor.d.ts +1 -0
  59. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  60. package/dist/modes/interactive/components/custom-editor.js +16 -7
  61. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  62. package/dist/modes/interactive/components/hook-input.d.ts.map +1 -1
  63. package/dist/modes/interactive/components/hook-input.js +2 -2
  64. package/dist/modes/interactive/components/hook-input.js.map +1 -1
  65. package/dist/modes/interactive/components/hook-selector.d.ts.map +1 -1
  66. package/dist/modes/interactive/components/hook-selector.js +2 -2
  67. package/dist/modes/interactive/components/hook-selector.js.map +1 -1
  68. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/model-selector.js +2 -2
  70. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  71. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  72. package/dist/modes/interactive/components/oauth-selector.js +2 -2
  73. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  74. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  75. package/dist/modes/interactive/components/session-selector.js +3 -3
  76. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  77. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  78. package/dist/modes/interactive/components/user-message-selector.js +3 -3
  79. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  80. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  81. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  82. package/dist/modes/interactive/interactive-mode.js +63 -1
  83. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  84. package/dist/modes/print-mode.d.ts.map +1 -1
  85. package/dist/modes/print-mode.js +10 -0
  86. package/dist/modes/print-mode.js.map +1 -1
  87. package/docs/custom-tools.md +43 -4
  88. package/docs/hooks.md +104 -5
  89. package/docs/skills.md +65 -24
  90. package/examples/custom-tools/README.md +18 -7
  91. package/examples/custom-tools/subagent/README.md +172 -0
  92. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  93. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  94. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  95. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  96. package/examples/custom-tools/subagent/agents.ts +157 -0
  97. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  98. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  99. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  100. package/examples/custom-tools/subagent/index.ts +772 -0
  101. package/package.json +4 -4
  102. /package/examples/custom-tools/{hello.ts → hello/index.ts} +0 -0
  103. /package/examples/custom-tools/{question.ts → question/index.ts} +0 -0
  104. /package/examples/custom-tools/{todo.ts → todo/index.ts} +0 -0
@@ -2,6 +2,22 @@ import { existsSync, readdirSync, readFileSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { basename, dirname, join, resolve } from "path";
4
4
  import { CONFIG_DIR_NAME } from "../config.js";
5
+ /**
6
+ * Standard frontmatter fields per Agent Skills spec.
7
+ * See: https://agentskills.io/specification#frontmatter-required
8
+ */
9
+ const ALLOWED_FRONTMATTER_FIELDS = new Set([
10
+ "name",
11
+ "description",
12
+ "license",
13
+ "compatibility",
14
+ "metadata",
15
+ "allowed-tools",
16
+ ]);
17
+ /** Max name length per spec */
18
+ const MAX_NAME_LENGTH = 64;
19
+ /** Max description length per spec */
20
+ const MAX_DESCRIPTION_LENGTH = 1024;
5
21
  function stripQuotes(value) {
6
22
  if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
7
23
  return value.slice(1, -1);
@@ -9,22 +25,24 @@ function stripQuotes(value) {
9
25
  return value;
10
26
  }
11
27
  function parseFrontmatter(content) {
12
- const frontmatter = { description: "" };
28
+ const frontmatter = {};
29
+ const allKeys = [];
13
30
  const normalizedContent = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
14
31
  if (!normalizedContent.startsWith("---")) {
15
- return { frontmatter, body: normalizedContent };
32
+ return { frontmatter, body: normalizedContent, allKeys };
16
33
  }
17
34
  const endIndex = normalizedContent.indexOf("\n---", 3);
18
35
  if (endIndex === -1) {
19
- return { frontmatter, body: normalizedContent };
36
+ return { frontmatter, body: normalizedContent, allKeys };
20
37
  }
21
38
  const frontmatterBlock = normalizedContent.slice(4, endIndex);
22
39
  const body = normalizedContent.slice(endIndex + 4).trim();
23
40
  for (const line of frontmatterBlock.split("\n")) {
24
- const match = line.match(/^(\w+):\s*(.*)$/);
41
+ const match = line.match(/^(\w[\w-]*):\s*(.*)$/);
25
42
  if (match) {
26
43
  const key = match[1];
27
44
  const value = stripQuotes(match[2].trim());
45
+ allKeys.push(key);
28
46
  if (key === "name") {
29
47
  frontmatter.name = value;
30
48
  }
@@ -33,20 +51,69 @@ function parseFrontmatter(content) {
33
51
  }
34
52
  }
35
53
  }
36
- return { frontmatter, body };
54
+ return { frontmatter, body, allKeys };
55
+ }
56
+ /**
57
+ * Validate skill name per Agent Skills spec.
58
+ * Returns array of validation error messages (empty if valid).
59
+ */
60
+ function validateName(name, parentDirName) {
61
+ const errors = [];
62
+ if (name !== parentDirName) {
63
+ errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
64
+ }
65
+ if (name.length > MAX_NAME_LENGTH) {
66
+ errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
67
+ }
68
+ if (!/^[a-z0-9-]+$/.test(name)) {
69
+ errors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);
70
+ }
71
+ if (name.startsWith("-") || name.endsWith("-")) {
72
+ errors.push(`name must not start or end with a hyphen`);
73
+ }
74
+ if (name.includes("--")) {
75
+ errors.push(`name must not contain consecutive hyphens`);
76
+ }
77
+ return errors;
78
+ }
79
+ /**
80
+ * Validate description per Agent Skills spec.
81
+ */
82
+ function validateDescription(description) {
83
+ const errors = [];
84
+ if (!description || description.trim() === "") {
85
+ errors.push(`description is required`);
86
+ }
87
+ else if (description.length > MAX_DESCRIPTION_LENGTH) {
88
+ errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
89
+ }
90
+ return errors;
91
+ }
92
+ /**
93
+ * Check for unknown frontmatter fields.
94
+ */
95
+ function validateFrontmatterFields(keys) {
96
+ const errors = [];
97
+ for (const key of keys) {
98
+ if (!ALLOWED_FRONTMATTER_FIELDS.has(key)) {
99
+ errors.push(`unknown frontmatter field "${key}"`);
100
+ }
101
+ }
102
+ return errors;
37
103
  }
38
104
  /**
39
105
  * Load skills from a directory recursively.
40
106
  * Skills are directories containing a SKILL.md file with frontmatter including a description.
41
107
  */
42
- export function loadSkillsFromDir(options, subdir = "") {
43
- const { dir, source, useColonPath = false } = options;
44
- return loadSkillsFromDirInternal(dir, source, "recursive", useColonPath, subdir);
108
+ export function loadSkillsFromDir(options) {
109
+ const { dir, source } = options;
110
+ return loadSkillsFromDirInternal(dir, source, "recursive");
45
111
  }
46
- function loadSkillsFromDirInternal(dir, source, format, useColonPath = false, subdir = "") {
112
+ function loadSkillsFromDirInternal(dir, source, format) {
47
113
  const skills = [];
114
+ const warnings = [];
48
115
  if (!existsSync(dir)) {
49
- return skills;
116
+ return { skills, warnings };
50
117
  }
51
118
  try {
52
119
  const entries = readdirSync(dir, { withFileTypes: true });
@@ -61,29 +128,16 @@ function loadSkillsFromDirInternal(dir, source, format, useColonPath = false, su
61
128
  if (format === "recursive") {
62
129
  // Recursive format: scan directories, look for SKILL.md files
63
130
  if (entry.isDirectory()) {
64
- const newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;
65
- skills.push(...loadSkillsFromDirInternal(fullPath, source, format, useColonPath, newSubdir));
131
+ const subResult = loadSkillsFromDirInternal(fullPath, source, format);
132
+ skills.push(...subResult.skills);
133
+ warnings.push(...subResult.warnings);
66
134
  }
67
135
  else if (entry.isFile() && entry.name === "SKILL.md") {
68
- try {
69
- const rawContent = readFileSync(fullPath, "utf-8");
70
- const { frontmatter } = parseFrontmatter(rawContent);
71
- if (!frontmatter.description) {
72
- continue;
73
- }
74
- const skillDir = dirname(fullPath);
75
- // useColonPath: db:migrate (pi), otherwise just: migrate (codex)
76
- const nameFromPath = useColonPath ? subdir || basename(skillDir) : basename(skillDir);
77
- const name = frontmatter.name || nameFromPath;
78
- skills.push({
79
- name,
80
- description: frontmatter.description,
81
- filePath: fullPath,
82
- baseDir: skillDir,
83
- source,
84
- });
136
+ const result = loadSkillFromFile(fullPath, source);
137
+ if (result.skill) {
138
+ skills.push(result.skill);
85
139
  }
86
- catch { }
140
+ warnings.push(...result.warnings);
87
141
  }
88
142
  }
89
143
  else if (format === "claude") {
@@ -91,79 +145,136 @@ function loadSkillsFromDirInternal(dir, source, format, useColonPath = false, su
91
145
  if (!entry.isDirectory()) {
92
146
  continue;
93
147
  }
94
- const skillDir = fullPath;
95
- const skillFile = join(skillDir, "SKILL.md");
148
+ const skillFile = join(fullPath, "SKILL.md");
96
149
  if (!existsSync(skillFile)) {
97
150
  continue;
98
151
  }
99
- try {
100
- const rawContent = readFileSync(skillFile, "utf-8");
101
- const { frontmatter } = parseFrontmatter(rawContent);
102
- if (!frontmatter.description) {
103
- continue;
104
- }
105
- const name = frontmatter.name || entry.name;
106
- skills.push({
107
- name,
108
- description: frontmatter.description,
109
- filePath: skillFile,
110
- baseDir: skillDir,
111
- source,
112
- });
152
+ const result = loadSkillFromFile(skillFile, source);
153
+ if (result.skill) {
154
+ skills.push(result.skill);
113
155
  }
114
- catch { }
156
+ warnings.push(...result.warnings);
115
157
  }
116
158
  }
117
159
  }
118
160
  catch { }
119
- return skills;
161
+ return { skills, warnings };
162
+ }
163
+ function loadSkillFromFile(filePath, source) {
164
+ const warnings = [];
165
+ try {
166
+ const rawContent = readFileSync(filePath, "utf-8");
167
+ const { frontmatter, allKeys } = parseFrontmatter(rawContent);
168
+ const skillDir = dirname(filePath);
169
+ const parentDirName = basename(skillDir);
170
+ // Validate frontmatter fields
171
+ const fieldErrors = validateFrontmatterFields(allKeys);
172
+ for (const error of fieldErrors) {
173
+ warnings.push({ skillPath: filePath, message: error });
174
+ }
175
+ // Validate description
176
+ const descErrors = validateDescription(frontmatter.description);
177
+ for (const error of descErrors) {
178
+ warnings.push({ skillPath: filePath, message: error });
179
+ }
180
+ // Use name from frontmatter, or fall back to parent directory name
181
+ const name = frontmatter.name || parentDirName;
182
+ // Validate name
183
+ const nameErrors = validateName(name, parentDirName);
184
+ for (const error of nameErrors) {
185
+ warnings.push({ skillPath: filePath, message: error });
186
+ }
187
+ // Still load the skill even with warnings (unless description is completely missing)
188
+ if (!frontmatter.description || frontmatter.description.trim() === "") {
189
+ return { skill: null, warnings };
190
+ }
191
+ return {
192
+ skill: {
193
+ name,
194
+ description: frontmatter.description,
195
+ filePath,
196
+ baseDir: skillDir,
197
+ source,
198
+ },
199
+ warnings,
200
+ };
201
+ }
202
+ catch {
203
+ return { skill: null, warnings };
204
+ }
120
205
  }
121
206
  /**
122
207
  * Format skills for inclusion in a system prompt.
208
+ * Uses XML format per Agent Skills standard.
209
+ * See: https://agentskills.io/integrate-skills
123
210
  */
124
211
  export function formatSkillsForPrompt(skills) {
125
212
  if (skills.length === 0) {
126
213
  return "";
127
214
  }
128
215
  const lines = [
129
- "\n\n<available_skills>",
130
- "The following skills provide specialized instructions for specific tasks.",
216
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
131
217
  "Use the read tool to load a skill's file when the task matches its description.",
132
- "Skills may contain {baseDir} placeholders - replace them with the skill's base directory path.\n",
218
+ "",
219
+ "<available_skills>",
133
220
  ];
134
221
  for (const skill of skills) {
135
- lines.push(`- ${skill.name}: ${skill.description}`);
136
- lines.push(` File: ${skill.filePath}`);
137
- lines.push(` Base directory: ${skill.baseDir}`);
222
+ lines.push(" <skill>");
223
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
224
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
225
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
226
+ lines.push(" </skill>");
138
227
  }
139
228
  lines.push("</available_skills>");
140
229
  return lines.join("\n");
141
230
  }
231
+ function escapeXml(str) {
232
+ return str
233
+ .replace(/&/g, "&amp;")
234
+ .replace(/</g, "&lt;")
235
+ .replace(/>/g, "&gt;")
236
+ .replace(/"/g, "&quot;")
237
+ .replace(/'/g, "&apos;");
238
+ }
239
+ /**
240
+ * Load skills from all configured locations.
241
+ * Returns skills and any validation warnings.
242
+ */
142
243
  export function loadSkills() {
143
244
  const skillMap = new Map();
144
- // Codex: recursive, simple directory name
145
- const codexUserDir = join(homedir(), ".codex", "skills");
146
- for (const skill of loadSkillsFromDirInternal(codexUserDir, "codex-user", "recursive", false)) {
147
- skillMap.set(skill.name, skill);
245
+ const allWarnings = [];
246
+ const collisionWarnings = [];
247
+ function addSkills(result) {
248
+ allWarnings.push(...result.warnings);
249
+ for (const skill of result.skills) {
250
+ const existing = skillMap.get(skill.name);
251
+ if (existing) {
252
+ collisionWarnings.push({
253
+ skillPath: skill.filePath,
254
+ message: `name collision: "${skill.name}" already loaded from ${existing.filePath}, skipping this one`,
255
+ });
256
+ }
257
+ else {
258
+ skillMap.set(skill.name, skill);
259
+ }
260
+ }
148
261
  }
262
+ // Codex: recursive
263
+ const codexUserDir = join(homedir(), ".codex", "skills");
264
+ addSkills(loadSkillsFromDirInternal(codexUserDir, "codex-user", "recursive"));
149
265
  // Claude: single level only
150
266
  const claudeUserDir = join(homedir(), ".claude", "skills");
151
- for (const skill of loadSkillsFromDirInternal(claudeUserDir, "claude-user", "claude", false)) {
152
- skillMap.set(skill.name, skill);
153
- }
267
+ addSkills(loadSkillsFromDirInternal(claudeUserDir, "claude-user", "claude"));
154
268
  const claudeProjectDir = resolve(process.cwd(), ".claude", "skills");
155
- for (const skill of loadSkillsFromDirInternal(claudeProjectDir, "claude-project", "claude", false)) {
156
- skillMap.set(skill.name, skill);
157
- }
158
- // Pi: recursive, colon-separated path names
269
+ addSkills(loadSkillsFromDirInternal(claudeProjectDir, "claude-project", "claude"));
270
+ // Pi: recursive
159
271
  const globalSkillsDir = join(homedir(), CONFIG_DIR_NAME, "agent", "skills");
160
- for (const skill of loadSkillsFromDirInternal(globalSkillsDir, "user", "recursive", true)) {
161
- skillMap.set(skill.name, skill);
162
- }
272
+ addSkills(loadSkillsFromDirInternal(globalSkillsDir, "user", "recursive"));
163
273
  const projectSkillsDir = resolve(process.cwd(), CONFIG_DIR_NAME, "skills");
164
- for (const skill of loadSkillsFromDirInternal(projectSkillsDir, "project", "recursive", true)) {
165
- skillMap.set(skill.name, skill);
166
- }
167
- return Array.from(skillMap.values());
274
+ addSkills(loadSkillsFromDirInternal(projectSkillsDir, "project", "recursive"));
275
+ return {
276
+ skills: Array.from(skillMap.values()),
277
+ warnings: [...allWarnings, ...collisionWarnings],
278
+ };
168
279
  }
169
280
  //# sourceMappingURL=skills.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/core/skills.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME } from \"../config.js\";\n\nexport interface SkillFrontmatter {\n\tname?: string;\n\tdescription: string;\n}\n\nexport interface Skill {\n\tname: string;\n\tdescription: string;\n\tfilePath: string;\n\tbaseDir: string;\n\tsource: string;\n}\n\ntype SkillFormat = \"recursive\" | \"claude\";\n\nfunction stripQuotes(value: string): string {\n\tif ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n\t\treturn value.slice(1, -1);\n\t}\n\treturn value;\n}\n\nfunction parseFrontmatter(content: string): { frontmatter: SkillFrontmatter; body: string } {\n\tconst frontmatter: SkillFrontmatter = { description: \"\" };\n\n\tconst normalizedContent = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n\n\tif (!normalizedContent.startsWith(\"---\")) {\n\t\treturn { frontmatter, body: normalizedContent };\n\t}\n\n\tconst endIndex = normalizedContent.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, body: normalizedContent };\n\t}\n\n\tconst frontmatterBlock = normalizedContent.slice(4, endIndex);\n\tconst body = normalizedContent.slice(endIndex + 4).trim();\n\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w+):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tconst key = match[1];\n\t\t\tconst value = stripQuotes(match[2].trim());\n\t\t\tif (key === \"name\") {\n\t\t\t\tfrontmatter.name = value;\n\t\t\t} else if (key === \"description\") {\n\t\t\t\tfrontmatter.description = value;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { frontmatter, body };\n}\n\nexport interface LoadSkillsFromDirOptions {\n\t/** Directory to scan for skills */\n\tdir: string;\n\t/** Source identifier for these skills */\n\tsource: string;\n\t/** Use colon-separated path names (e.g., db:migrate) instead of simple directory name */\n\tuseColonPath?: boolean;\n}\n\n/**\n * Load skills from a directory recursively.\n * Skills are directories containing a SKILL.md file with frontmatter including a description.\n */\nexport function loadSkillsFromDir(options: LoadSkillsFromDirOptions, subdir: string = \"\"): Skill[] {\n\tconst { dir, source, useColonPath = false } = options;\n\treturn loadSkillsFromDirInternal(dir, source, \"recursive\", useColonPath, subdir);\n}\n\nfunction loadSkillsFromDirInternal(\n\tdir: string,\n\tsource: string,\n\tformat: SkillFormat,\n\tuseColonPath: boolean = false,\n\tsubdir: string = \"\",\n): Skill[] {\n\tconst skills: Skill[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn skills;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\")) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (format === \"recursive\") {\n\t\t\t\t// Recursive format: scan directories, look for SKILL.md files\n\t\t\t\tif (entry.isDirectory()) {\n\t\t\t\t\tconst newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;\n\t\t\t\t\tskills.push(...loadSkillsFromDirInternal(fullPath, source, format, useColonPath, newSubdir));\n\t\t\t\t} else if (entry.isFile() && entry.name === \"SKILL.md\") {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst rawContent = readFileSync(fullPath, \"utf-8\");\n\t\t\t\t\t\tconst { frontmatter } = parseFrontmatter(rawContent);\n\n\t\t\t\t\t\tif (!frontmatter.description) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst skillDir = dirname(fullPath);\n\t\t\t\t\t\t// useColonPath: db:migrate (pi), otherwise just: migrate (codex)\n\t\t\t\t\t\tconst nameFromPath = useColonPath ? subdir || basename(skillDir) : basename(skillDir);\n\t\t\t\t\t\tconst name = frontmatter.name || nameFromPath;\n\n\t\t\t\t\t\tskills.push({\n\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\tdescription: frontmatter.description,\n\t\t\t\t\t\t\tfilePath: fullPath,\n\t\t\t\t\t\t\tbaseDir: skillDir,\n\t\t\t\t\t\t\tsource,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch {}\n\t\t\t\t}\n\t\t\t} else if (format === \"claude\") {\n\t\t\t\t// Claude format: only one level deep, each directory must contain SKILL.md\n\t\t\t\tif (!entry.isDirectory()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst skillDir = fullPath;\n\t\t\t\tconst skillFile = join(skillDir, \"SKILL.md\");\n\n\t\t\t\tif (!existsSync(skillFile)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst rawContent = readFileSync(skillFile, \"utf-8\");\n\t\t\t\t\tconst { frontmatter } = parseFrontmatter(rawContent);\n\n\t\t\t\t\tif (!frontmatter.description) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst name = frontmatter.name || entry.name;\n\n\t\t\t\t\tskills.push({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tdescription: frontmatter.description,\n\t\t\t\t\t\tfilePath: skillFile,\n\t\t\t\t\t\tbaseDir: skillDir,\n\t\t\t\t\t\tsource,\n\t\t\t\t\t});\n\t\t\t\t} catch {}\n\t\t\t}\n\t\t}\n\t} catch {}\n\n\treturn skills;\n}\n\n/**\n * Format skills for inclusion in a system prompt.\n */\nexport function formatSkillsForPrompt(skills: Skill[]): string {\n\tif (skills.length === 0) {\n\t\treturn \"\";\n\t}\n\n\tconst lines = [\n\t\t\"\\n\\n<available_skills>\",\n\t\t\"The following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"Skills may contain {baseDir} placeholders - replace them with the skill's base directory path.\\n\",\n\t];\n\n\tfor (const skill of skills) {\n\t\tlines.push(`- ${skill.name}: ${skill.description}`);\n\t\tlines.push(` File: ${skill.filePath}`);\n\t\tlines.push(` Base directory: ${skill.baseDir}`);\n\t}\n\n\tlines.push(\"</available_skills>\");\n\n\treturn lines.join(\"\\n\");\n}\n\nexport function loadSkills(): Skill[] {\n\tconst skillMap = new Map<string, Skill>();\n\n\t// Codex: recursive, simple directory name\n\tconst codexUserDir = join(homedir(), \".codex\", \"skills\");\n\tfor (const skill of loadSkillsFromDirInternal(codexUserDir, \"codex-user\", \"recursive\", false)) {\n\t\tskillMap.set(skill.name, skill);\n\t}\n\n\t// Claude: single level only\n\tconst claudeUserDir = join(homedir(), \".claude\", \"skills\");\n\tfor (const skill of loadSkillsFromDirInternal(claudeUserDir, \"claude-user\", \"claude\", false)) {\n\t\tskillMap.set(skill.name, skill);\n\t}\n\n\tconst claudeProjectDir = resolve(process.cwd(), \".claude\", \"skills\");\n\tfor (const skill of loadSkillsFromDirInternal(claudeProjectDir, \"claude-project\", \"claude\", false)) {\n\t\tskillMap.set(skill.name, skill);\n\t}\n\n\t// Pi: recursive, colon-separated path names\n\tconst globalSkillsDir = join(homedir(), CONFIG_DIR_NAME, \"agent\", \"skills\");\n\tfor (const skill of loadSkillsFromDirInternal(globalSkillsDir, \"user\", \"recursive\", true)) {\n\t\tskillMap.set(skill.name, skill);\n\t}\n\n\tconst projectSkillsDir = resolve(process.cwd(), CONFIG_DIR_NAME, \"skills\");\n\tfor (const skill of loadSkillsFromDirInternal(projectSkillsDir, \"project\", \"recursive\", true)) {\n\t\tskillMap.set(skill.name, skill);\n\t}\n\n\treturn Array.from(skillMap.values());\n}\n"]}
1
+ {"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/core/skills.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME } from \"../config.js\";\n\n/**\n * Standard frontmatter fields per Agent Skills spec.\n * See: https://agentskills.io/specification#frontmatter-required\n */\nconst ALLOWED_FRONTMATTER_FIELDS = new Set([\n\t\"name\",\n\t\"description\",\n\t\"license\",\n\t\"compatibility\",\n\t\"metadata\",\n\t\"allowed-tools\",\n]);\n\n/** Max name length per spec */\nconst MAX_NAME_LENGTH = 64;\n\n/** Max description length per spec */\nconst MAX_DESCRIPTION_LENGTH = 1024;\n\nexport interface SkillFrontmatter {\n\tname?: string;\n\tdescription?: string;\n\t[key: string]: unknown;\n}\n\nexport interface Skill {\n\tname: string;\n\tdescription: string;\n\tfilePath: string;\n\tbaseDir: string;\n\tsource: string;\n}\n\nexport interface SkillWarning {\n\tskillPath: string;\n\tmessage: string;\n}\n\nexport interface LoadSkillsResult {\n\tskills: Skill[];\n\twarnings: SkillWarning[];\n}\n\ntype SkillFormat = \"recursive\" | \"claude\";\n\nfunction stripQuotes(value: string): string {\n\tif ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n\t\treturn value.slice(1, -1);\n\t}\n\treturn value;\n}\n\nfunction parseFrontmatter(content: string): { frontmatter: SkillFrontmatter; body: string; allKeys: string[] } {\n\tconst frontmatter: SkillFrontmatter = {};\n\tconst allKeys: string[] = [];\n\n\tconst normalizedContent = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n\n\tif (!normalizedContent.startsWith(\"---\")) {\n\t\treturn { frontmatter, body: normalizedContent, allKeys };\n\t}\n\n\tconst endIndex = normalizedContent.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, body: normalizedContent, allKeys };\n\t}\n\n\tconst frontmatterBlock = normalizedContent.slice(4, endIndex);\n\tconst body = normalizedContent.slice(endIndex + 4).trim();\n\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w[\\w-]*):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tconst key = match[1];\n\t\t\tconst value = stripQuotes(match[2].trim());\n\t\t\tallKeys.push(key);\n\t\t\tif (key === \"name\") {\n\t\t\t\tfrontmatter.name = value;\n\t\t\t} else if (key === \"description\") {\n\t\t\t\tfrontmatter.description = value;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { frontmatter, body, allKeys };\n}\n\n/**\n * Validate skill name per Agent Skills spec.\n * Returns array of validation error messages (empty if valid).\n */\nfunction validateName(name: string, parentDirName: string): string[] {\n\tconst errors: string[] = [];\n\n\tif (name !== parentDirName) {\n\t\terrors.push(`name \"${name}\" does not match parent directory \"${parentDirName}\"`);\n\t}\n\n\tif (name.length > MAX_NAME_LENGTH) {\n\t\terrors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);\n\t}\n\n\tif (!/^[a-z0-9-]+$/.test(name)) {\n\t\terrors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);\n\t}\n\n\tif (name.startsWith(\"-\") || name.endsWith(\"-\")) {\n\t\terrors.push(`name must not start or end with a hyphen`);\n\t}\n\n\tif (name.includes(\"--\")) {\n\t\terrors.push(`name must not contain consecutive hyphens`);\n\t}\n\n\treturn errors;\n}\n\n/**\n * Validate description per Agent Skills spec.\n */\nfunction validateDescription(description: string | undefined): string[] {\n\tconst errors: string[] = [];\n\n\tif (!description || description.trim() === \"\") {\n\t\terrors.push(`description is required`);\n\t} else if (description.length > MAX_DESCRIPTION_LENGTH) {\n\t\terrors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);\n\t}\n\n\treturn errors;\n}\n\n/**\n * Check for unknown frontmatter fields.\n */\nfunction validateFrontmatterFields(keys: string[]): string[] {\n\tconst errors: string[] = [];\n\tfor (const key of keys) {\n\t\tif (!ALLOWED_FRONTMATTER_FIELDS.has(key)) {\n\t\t\terrors.push(`unknown frontmatter field \"${key}\"`);\n\t\t}\n\t}\n\treturn errors;\n}\n\nexport interface LoadSkillsFromDirOptions {\n\t/** Directory to scan for skills */\n\tdir: string;\n\t/** Source identifier for these skills */\n\tsource: string;\n}\n\n/**\n * Load skills from a directory recursively.\n * Skills are directories containing a SKILL.md file with frontmatter including a description.\n */\nexport function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult {\n\tconst { dir, source } = options;\n\treturn loadSkillsFromDirInternal(dir, source, \"recursive\");\n}\n\nfunction loadSkillsFromDirInternal(dir: string, source: string, format: SkillFormat): LoadSkillsResult {\n\tconst skills: Skill[] = [];\n\tconst warnings: SkillWarning[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn { skills, warnings };\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\")) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (format === \"recursive\") {\n\t\t\t\t// Recursive format: scan directories, look for SKILL.md files\n\t\t\t\tif (entry.isDirectory()) {\n\t\t\t\t\tconst subResult = loadSkillsFromDirInternal(fullPath, source, format);\n\t\t\t\t\tskills.push(...subResult.skills);\n\t\t\t\t\twarnings.push(...subResult.warnings);\n\t\t\t\t} else if (entry.isFile() && entry.name === \"SKILL.md\") {\n\t\t\t\t\tconst result = loadSkillFromFile(fullPath, source);\n\t\t\t\t\tif (result.skill) {\n\t\t\t\t\t\tskills.push(result.skill);\n\t\t\t\t\t}\n\t\t\t\t\twarnings.push(...result.warnings);\n\t\t\t\t}\n\t\t\t} else if (format === \"claude\") {\n\t\t\t\t// Claude format: only one level deep, each directory must contain SKILL.md\n\t\t\t\tif (!entry.isDirectory()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst skillFile = join(fullPath, \"SKILL.md\");\n\t\t\t\tif (!existsSync(skillFile)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst result = loadSkillFromFile(skillFile, source);\n\t\t\t\tif (result.skill) {\n\t\t\t\t\tskills.push(result.skill);\n\t\t\t\t}\n\t\t\t\twarnings.push(...result.warnings);\n\t\t\t}\n\t\t}\n\t} catch {}\n\n\treturn { skills, warnings };\n}\n\nfunction loadSkillFromFile(filePath: string, source: string): { skill: Skill | null; warnings: SkillWarning[] } {\n\tconst warnings: SkillWarning[] = [];\n\n\ttry {\n\t\tconst rawContent = readFileSync(filePath, \"utf-8\");\n\t\tconst { frontmatter, allKeys } = parseFrontmatter(rawContent);\n\t\tconst skillDir = dirname(filePath);\n\t\tconst parentDirName = basename(skillDir);\n\n\t\t// Validate frontmatter fields\n\t\tconst fieldErrors = validateFrontmatterFields(allKeys);\n\t\tfor (const error of fieldErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Validate description\n\t\tconst descErrors = validateDescription(frontmatter.description);\n\t\tfor (const error of descErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Use name from frontmatter, or fall back to parent directory name\n\t\tconst name = frontmatter.name || parentDirName;\n\n\t\t// Validate name\n\t\tconst nameErrors = validateName(name, parentDirName);\n\t\tfor (const error of nameErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Still load the skill even with warnings (unless description is completely missing)\n\t\tif (!frontmatter.description || frontmatter.description.trim() === \"\") {\n\t\t\treturn { skill: null, warnings };\n\t\t}\n\n\t\treturn {\n\t\t\tskill: {\n\t\t\t\tname,\n\t\t\t\tdescription: frontmatter.description,\n\t\t\t\tfilePath,\n\t\t\t\tbaseDir: skillDir,\n\t\t\t\tsource,\n\t\t\t},\n\t\t\twarnings,\n\t\t};\n\t} catch {\n\t\treturn { skill: null, warnings };\n\t}\n}\n\n/**\n * Format skills for inclusion in a system prompt.\n * Uses XML format per Agent Skills standard.\n * See: https://agentskills.io/integrate-skills\n */\nexport function formatSkillsForPrompt(skills: Skill[]): string {\n\tif (skills.length === 0) {\n\t\treturn \"\";\n\t}\n\n\tconst lines = [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t];\n\n\tfor (const skill of skills) {\n\t\tlines.push(\" <skill>\");\n\t\tlines.push(` <name>${escapeXml(skill.name)}</name>`);\n\t\tlines.push(` <description>${escapeXml(skill.description)}</description>`);\n\t\tlines.push(` <location>${escapeXml(skill.filePath)}</location>`);\n\t\tlines.push(\" </skill>\");\n\t}\n\n\tlines.push(\"</available_skills>\");\n\n\treturn lines.join(\"\\n\");\n}\n\nfunction escapeXml(str: string): string {\n\treturn str\n\t\t.replace(/&/g, \"&amp;\")\n\t\t.replace(/</g, \"&lt;\")\n\t\t.replace(/>/g, \"&gt;\")\n\t\t.replace(/\"/g, \"&quot;\")\n\t\t.replace(/'/g, \"&apos;\");\n}\n\n/**\n * Load skills from all configured locations.\n * Returns skills and any validation warnings.\n */\nexport function loadSkills(): LoadSkillsResult {\n\tconst skillMap = new Map<string, Skill>();\n\tconst allWarnings: SkillWarning[] = [];\n\tconst collisionWarnings: SkillWarning[] = [];\n\n\tfunction addSkills(result: LoadSkillsResult) {\n\t\tallWarnings.push(...result.warnings);\n\t\tfor (const skill of result.skills) {\n\t\t\tconst existing = skillMap.get(skill.name);\n\t\t\tif (existing) {\n\t\t\t\tcollisionWarnings.push({\n\t\t\t\t\tskillPath: skill.filePath,\n\t\t\t\t\tmessage: `name collision: \"${skill.name}\" already loaded from ${existing.filePath}, skipping this one`,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tskillMap.set(skill.name, skill);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Codex: recursive\n\tconst codexUserDir = join(homedir(), \".codex\", \"skills\");\n\taddSkills(loadSkillsFromDirInternal(codexUserDir, \"codex-user\", \"recursive\"));\n\n\t// Claude: single level only\n\tconst claudeUserDir = join(homedir(), \".claude\", \"skills\");\n\taddSkills(loadSkillsFromDirInternal(claudeUserDir, \"claude-user\", \"claude\"));\n\n\tconst claudeProjectDir = resolve(process.cwd(), \".claude\", \"skills\");\n\taddSkills(loadSkillsFromDirInternal(claudeProjectDir, \"claude-project\", \"claude\"));\n\n\t// Pi: recursive\n\tconst globalSkillsDir = join(homedir(), CONFIG_DIR_NAME, \"agent\", \"skills\");\n\taddSkills(loadSkillsFromDirInternal(globalSkillsDir, \"user\", \"recursive\"));\n\n\tconst projectSkillsDir = resolve(process.cwd(), CONFIG_DIR_NAME, \"skills\");\n\taddSkills(loadSkillsFromDirInternal(projectSkillsDir, \"project\", \"recursive\"));\n\n\treturn {\n\t\tskills: Array.from(skillMap.values()),\n\t\twarnings: [...allWarnings, ...collisionWarnings],\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CACf;AAgCD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CA+B7D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAatE;AAqED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,gBAAgB,EAAE,CAYtD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAczF","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getCommandsDir } from \"../config.js\";\n\n/**\n * Represents a custom slash command loaded from a file\n */\nexport interface FileSlashCommand {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // e.g., \"(user)\", \"(project)\", \"(project:frontend)\"\n}\n\n/**\n * Parse YAML frontmatter from markdown content\n * Returns { frontmatter, content } where content has frontmatter stripped\n */\nfunction parseFrontmatter(content: string): { frontmatter: Record<string, string>; content: string } {\n\tconst frontmatter: Record<string, string> = {};\n\n\tif (!content.startsWith(\"---\")) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst endIndex = content.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst frontmatterBlock = content.slice(4, endIndex);\n\tconst remainingContent = content.slice(endIndex + 4).trim();\n\n\t// Simple YAML parsing - just key: value pairs\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w+):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tfrontmatter[match[1]] = match[2].trim();\n\t\t}\n\t}\n\n\treturn { frontmatter, content: remainingContent };\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in command content\n * Supports $1, $2, ... for positional args and $@ for all args\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $@ with all args joined\n\tresult = result.replace(/\\$@/g, args.join(\" \"));\n\n\t// Replace $1, $2, etc. with positional args\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\treturn result;\n}\n\n/**\n * Recursively scan a directory for .md files and load them as slash commands\n */\nfunction loadCommandsFromDir(dir: string, source: \"user\" | \"project\", subdir: string = \"\"): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn commands;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// Recurse into subdirectory\n\t\t\t\tconst newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;\n\t\t\t\tcommands.push(...loadCommandsFromDir(fullPath, source, newSubdir));\n\t\t\t} else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst rawContent = readFileSync(fullPath, \"utf-8\");\n\t\t\t\t\tconst { frontmatter, content } = parseFrontmatter(rawContent);\n\n\t\t\t\t\tconst name = entry.name.slice(0, -3); // Remove .md extension\n\n\t\t\t\t\t// Build source string\n\t\t\t\t\tlet sourceStr: string;\n\t\t\t\t\tif (source === \"user\") {\n\t\t\t\t\t\tsourceStr = subdir ? `(user:${subdir})` : \"(user)\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsourceStr = subdir ? `(project:${subdir})` : \"(project)\";\n\t\t\t\t\t}\n\n\t\t\t\t\t// Get description from frontmatter or first non-empty line\n\t\t\t\t\tlet description = frontmatter.description || \"\";\n\t\t\t\t\tif (!description) {\n\t\t\t\t\t\tconst firstLine = content.split(\"\\n\").find((line) => line.trim());\n\t\t\t\t\t\tif (firstLine) {\n\t\t\t\t\t\t\t// Truncate if too long\n\t\t\t\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Append source to description\n\t\t\t\t\tdescription = description ? `${description} ${sourceStr}` : sourceStr;\n\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\tsource: sourceStr,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Silently skip files that can't be read\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\t// Silently skip directories that can't be read\n\t}\n\n\treturn commands;\n}\n\n/**\n * Load all custom slash commands from:\n * 1. Global: ~/{CONFIG_DIR_NAME}/agent/commands/\n * 2. Project: ./{CONFIG_DIR_NAME}/commands/\n */\nexport function loadSlashCommands(): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\t// 1. Load global commands from ~/{CONFIG_DIR_NAME}/agent/commands/\n\tconst globalCommandsDir = getCommandsDir();\n\tcommands.push(...loadCommandsFromDir(globalCommandsDir, \"user\"));\n\n\t// 2. Load project commands from ./{CONFIG_DIR_NAME}/commands/\n\tconst projectCommandsDir = resolve(process.cwd(), CONFIG_DIR_NAME, \"commands\");\n\tcommands.push(...loadCommandsFromDir(projectCommandsDir, \"project\"));\n\n\treturn commands;\n}\n\n/**\n * Expand a slash command if it matches a file-based command.\n * Returns the expanded content or the original text if not a slash command.\n */\nexport function expandSlashCommand(text: string, fileCommands: FileSlashCommand[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst argsString = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\tconst fileCommand = fileCommands.find((cmd) => cmd.name === commandName);\n\tif (fileCommand) {\n\t\tconst args = parseCommandArgs(argsString);\n\t\treturn substituteArgs(fileCommand.content, args);\n\t}\n\n\treturn text;\n}\n"]}
1
+ {"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CACf;AAgCD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CA+B7D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAatE;AAqED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,gBAAgB,EAAE,CAYtD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAczF","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getCommandsDir } from \"../config.js\";\n\n/**\n * Represents a custom slash command loaded from a file\n */\nexport interface FileSlashCommand {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // e.g., \"(user)\", \"(project)\", \"(project:frontend)\"\n}\n\n/**\n * Parse YAML frontmatter from markdown content\n * Returns { frontmatter, content } where content has frontmatter stripped\n */\nfunction parseFrontmatter(content: string): { frontmatter: Record<string, string>; content: string } {\n\tconst frontmatter: Record<string, string> = {};\n\n\tif (!content.startsWith(\"---\")) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst endIndex = content.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst frontmatterBlock = content.slice(4, endIndex);\n\tconst remainingContent = content.slice(endIndex + 4).trim();\n\n\t// Simple YAML parsing - just key: value pairs\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w+):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tfrontmatter[match[1]] = match[2].trim();\n\t\t}\n\t}\n\n\treturn { frontmatter, content: remainingContent };\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in command content\n * Supports $1, $2, ... for positional args and $@ for all args\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $@ with all args joined\n\tresult = result.replace(/\\$@/g, args.join(\" \"));\n\n\t// Replace $1, $2, etc. with positional args\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\treturn result;\n}\n\n/**\n * Recursively scan a directory for .md files (and symlinks to .md files) and load them as slash commands\n */\nfunction loadCommandsFromDir(dir: string, source: \"user\" | \"project\", subdir: string = \"\"): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn commands;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// Recurse into subdirectory\n\t\t\t\tconst newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;\n\t\t\t\tcommands.push(...loadCommandsFromDir(fullPath, source, newSubdir));\n\t\t\t} else if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(\".md\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst rawContent = readFileSync(fullPath, \"utf-8\");\n\t\t\t\t\tconst { frontmatter, content } = parseFrontmatter(rawContent);\n\n\t\t\t\t\tconst name = entry.name.slice(0, -3); // Remove .md extension\n\n\t\t\t\t\t// Build source string\n\t\t\t\t\tlet sourceStr: string;\n\t\t\t\t\tif (source === \"user\") {\n\t\t\t\t\t\tsourceStr = subdir ? `(user:${subdir})` : \"(user)\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsourceStr = subdir ? `(project:${subdir})` : \"(project)\";\n\t\t\t\t\t}\n\n\t\t\t\t\t// Get description from frontmatter or first non-empty line\n\t\t\t\t\tlet description = frontmatter.description || \"\";\n\t\t\t\t\tif (!description) {\n\t\t\t\t\t\tconst firstLine = content.split(\"\\n\").find((line) => line.trim());\n\t\t\t\t\t\tif (firstLine) {\n\t\t\t\t\t\t\t// Truncate if too long\n\t\t\t\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Append source to description\n\t\t\t\t\tdescription = description ? `${description} ${sourceStr}` : sourceStr;\n\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\tsource: sourceStr,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Silently skip files that can't be read\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\t// Silently skip directories that can't be read\n\t}\n\n\treturn commands;\n}\n\n/**\n * Load all custom slash commands from:\n * 1. Global: ~/{CONFIG_DIR_NAME}/agent/commands/\n * 2. Project: ./{CONFIG_DIR_NAME}/commands/\n */\nexport function loadSlashCommands(): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\t// 1. Load global commands from ~/{CONFIG_DIR_NAME}/agent/commands/\n\tconst globalCommandsDir = getCommandsDir();\n\tcommands.push(...loadCommandsFromDir(globalCommandsDir, \"user\"));\n\n\t// 2. Load project commands from ./{CONFIG_DIR_NAME}/commands/\n\tconst projectCommandsDir = resolve(process.cwd(), CONFIG_DIR_NAME, \"commands\");\n\tcommands.push(...loadCommandsFromDir(projectCommandsDir, \"project\"));\n\n\treturn commands;\n}\n\n/**\n * Expand a slash command if it matches a file-based command.\n * Returns the expanded content or the original text if not a slash command.\n */\nexport function expandSlashCommand(text: string, fileCommands: FileSlashCommand[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst argsString = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\tconst fileCommand = fileCommands.find((cmd) => cmd.name === commandName);\n\tif (fileCommand) {\n\t\tconst args = parseCommandArgs(argsString);\n\t\treturn substituteArgs(fileCommand.content, args);\n\t}\n\n\treturn text;\n}\n"]}
@@ -77,7 +77,7 @@ export function substituteArgs(content, args) {
77
77
  return result;
78
78
  }
79
79
  /**
80
- * Recursively scan a directory for .md files and load them as slash commands
80
+ * Recursively scan a directory for .md files (and symlinks to .md files) and load them as slash commands
81
81
  */
82
82
  function loadCommandsFromDir(dir, source, subdir = "") {
83
83
  const commands = [];
@@ -93,7 +93,7 @@ function loadCommandsFromDir(dir, source, subdir = "") {
93
93
  const newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;
94
94
  commands.push(...loadCommandsFromDir(fullPath, source, newSubdir));
95
95
  }
96
- else if (entry.isFile() && entry.name.endsWith(".md")) {
96
+ else if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md")) {
97
97
  try {
98
98
  const rawContent = readFileSync(fullPath, "utf-8");
99
99
  const { frontmatter, content } = parseFrontmatter(rawContent);
@@ -1 +1 @@
1
- {"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getCommandsDir } from \"../config.js\";\n\n/**\n * Represents a custom slash command loaded from a file\n */\nexport interface FileSlashCommand {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // e.g., \"(user)\", \"(project)\", \"(project:frontend)\"\n}\n\n/**\n * Parse YAML frontmatter from markdown content\n * Returns { frontmatter, content } where content has frontmatter stripped\n */\nfunction parseFrontmatter(content: string): { frontmatter: Record<string, string>; content: string } {\n\tconst frontmatter: Record<string, string> = {};\n\n\tif (!content.startsWith(\"---\")) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst endIndex = content.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst frontmatterBlock = content.slice(4, endIndex);\n\tconst remainingContent = content.slice(endIndex + 4).trim();\n\n\t// Simple YAML parsing - just key: value pairs\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w+):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tfrontmatter[match[1]] = match[2].trim();\n\t\t}\n\t}\n\n\treturn { frontmatter, content: remainingContent };\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in command content\n * Supports $1, $2, ... for positional args and $@ for all args\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $@ with all args joined\n\tresult = result.replace(/\\$@/g, args.join(\" \"));\n\n\t// Replace $1, $2, etc. with positional args\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\treturn result;\n}\n\n/**\n * Recursively scan a directory for .md files and load them as slash commands\n */\nfunction loadCommandsFromDir(dir: string, source: \"user\" | \"project\", subdir: string = \"\"): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn commands;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// Recurse into subdirectory\n\t\t\t\tconst newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;\n\t\t\t\tcommands.push(...loadCommandsFromDir(fullPath, source, newSubdir));\n\t\t\t} else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst rawContent = readFileSync(fullPath, \"utf-8\");\n\t\t\t\t\tconst { frontmatter, content } = parseFrontmatter(rawContent);\n\n\t\t\t\t\tconst name = entry.name.slice(0, -3); // Remove .md extension\n\n\t\t\t\t\t// Build source string\n\t\t\t\t\tlet sourceStr: string;\n\t\t\t\t\tif (source === \"user\") {\n\t\t\t\t\t\tsourceStr = subdir ? `(user:${subdir})` : \"(user)\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsourceStr = subdir ? `(project:${subdir})` : \"(project)\";\n\t\t\t\t\t}\n\n\t\t\t\t\t// Get description from frontmatter or first non-empty line\n\t\t\t\t\tlet description = frontmatter.description || \"\";\n\t\t\t\t\tif (!description) {\n\t\t\t\t\t\tconst firstLine = content.split(\"\\n\").find((line) => line.trim());\n\t\t\t\t\t\tif (firstLine) {\n\t\t\t\t\t\t\t// Truncate if too long\n\t\t\t\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Append source to description\n\t\t\t\t\tdescription = description ? `${description} ${sourceStr}` : sourceStr;\n\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\tsource: sourceStr,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Silently skip files that can't be read\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\t// Silently skip directories that can't be read\n\t}\n\n\treturn commands;\n}\n\n/**\n * Load all custom slash commands from:\n * 1. Global: ~/{CONFIG_DIR_NAME}/agent/commands/\n * 2. Project: ./{CONFIG_DIR_NAME}/commands/\n */\nexport function loadSlashCommands(): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\t// 1. Load global commands from ~/{CONFIG_DIR_NAME}/agent/commands/\n\tconst globalCommandsDir = getCommandsDir();\n\tcommands.push(...loadCommandsFromDir(globalCommandsDir, \"user\"));\n\n\t// 2. Load project commands from ./{CONFIG_DIR_NAME}/commands/\n\tconst projectCommandsDir = resolve(process.cwd(), CONFIG_DIR_NAME, \"commands\");\n\tcommands.push(...loadCommandsFromDir(projectCommandsDir, \"project\"));\n\n\treturn commands;\n}\n\n/**\n * Expand a slash command if it matches a file-based command.\n * Returns the expanded content or the original text if not a slash command.\n */\nexport function expandSlashCommand(text: string, fileCommands: FileSlashCommand[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst argsString = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\tconst fileCommand = fileCommands.find((cmd) => cmd.name === commandName);\n\tif (fileCommand) {\n\t\tconst args = parseCommandArgs(argsString);\n\t\treturn substituteArgs(fileCommand.content, args);\n\t}\n\n\treturn text;\n}\n"]}
1
+ {"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getCommandsDir } from \"../config.js\";\n\n/**\n * Represents a custom slash command loaded from a file\n */\nexport interface FileSlashCommand {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // e.g., \"(user)\", \"(project)\", \"(project:frontend)\"\n}\n\n/**\n * Parse YAML frontmatter from markdown content\n * Returns { frontmatter, content } where content has frontmatter stripped\n */\nfunction parseFrontmatter(content: string): { frontmatter: Record<string, string>; content: string } {\n\tconst frontmatter: Record<string, string> = {};\n\n\tif (!content.startsWith(\"---\")) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst endIndex = content.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst frontmatterBlock = content.slice(4, endIndex);\n\tconst remainingContent = content.slice(endIndex + 4).trim();\n\n\t// Simple YAML parsing - just key: value pairs\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w+):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tfrontmatter[match[1]] = match[2].trim();\n\t\t}\n\t}\n\n\treturn { frontmatter, content: remainingContent };\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in command content\n * Supports $1, $2, ... for positional args and $@ for all args\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $@ with all args joined\n\tresult = result.replace(/\\$@/g, args.join(\" \"));\n\n\t// Replace $1, $2, etc. with positional args\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\treturn result;\n}\n\n/**\n * Recursively scan a directory for .md files (and symlinks to .md files) and load them as slash commands\n */\nfunction loadCommandsFromDir(dir: string, source: \"user\" | \"project\", subdir: string = \"\"): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn commands;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// Recurse into subdirectory\n\t\t\t\tconst newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;\n\t\t\t\tcommands.push(...loadCommandsFromDir(fullPath, source, newSubdir));\n\t\t\t} else if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(\".md\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst rawContent = readFileSync(fullPath, \"utf-8\");\n\t\t\t\t\tconst { frontmatter, content } = parseFrontmatter(rawContent);\n\n\t\t\t\t\tconst name = entry.name.slice(0, -3); // Remove .md extension\n\n\t\t\t\t\t// Build source string\n\t\t\t\t\tlet sourceStr: string;\n\t\t\t\t\tif (source === \"user\") {\n\t\t\t\t\t\tsourceStr = subdir ? `(user:${subdir})` : \"(user)\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsourceStr = subdir ? `(project:${subdir})` : \"(project)\";\n\t\t\t\t\t}\n\n\t\t\t\t\t// Get description from frontmatter or first non-empty line\n\t\t\t\t\tlet description = frontmatter.description || \"\";\n\t\t\t\t\tif (!description) {\n\t\t\t\t\t\tconst firstLine = content.split(\"\\n\").find((line) => line.trim());\n\t\t\t\t\t\tif (firstLine) {\n\t\t\t\t\t\t\t// Truncate if too long\n\t\t\t\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Append source to description\n\t\t\t\t\tdescription = description ? `${description} ${sourceStr}` : sourceStr;\n\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\tsource: sourceStr,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Silently skip files that can't be read\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\t// Silently skip directories that can't be read\n\t}\n\n\treturn commands;\n}\n\n/**\n * Load all custom slash commands from:\n * 1. Global: ~/{CONFIG_DIR_NAME}/agent/commands/\n * 2. Project: ./{CONFIG_DIR_NAME}/commands/\n */\nexport function loadSlashCommands(): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\t// 1. Load global commands from ~/{CONFIG_DIR_NAME}/agent/commands/\n\tconst globalCommandsDir = getCommandsDir();\n\tcommands.push(...loadCommandsFromDir(globalCommandsDir, \"user\"));\n\n\t// 2. Load project commands from ./{CONFIG_DIR_NAME}/commands/\n\tconst projectCommandsDir = resolve(process.cwd(), CONFIG_DIR_NAME, \"commands\");\n\tcommands.push(...loadCommandsFromDir(projectCommandsDir, \"project\"));\n\n\treturn commands;\n}\n\n/**\n * Expand a slash command if it matches a file-based command.\n * Returns the expanded content or the original text if not a slash command.\n */\nexport function expandSlashCommand(text: string, fileCommands: FileSlashCommand[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst argsString = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\tconst fileCommand = fileCommands.find((cmd) => cmd.name === commandName);\n\tif (fileCommand) {\n\t\tconst args = parseCommandArgs(argsString);\n\t\treturn substituteArgs(fileCommand.content, args);\n\t}\n\n\treturn text;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAkDjD;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAqClF;AAED,MAAM,WAAW,wBAAwB;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,QAAQ,EAAE,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,GAAG,MAAM,CA0JhF","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport chalk from \"chalk\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { getAgentDir, getDocsPath, getReadmePath } from \"../config.js\";\nimport { formatSkillsForPrompt, loadSkills } from \"./skills.js\";\nimport type { ToolName } from \"./tools/index.js\";\n\n/** Tool descriptions for system prompt */\nconst toolDescriptions: Record<ToolName, string> = {\n\tread: \"Read file contents\",\n\tbash: \"Execute bash commands (ls, grep, find, etc.)\",\n\tedit: \"Make surgical edits to files (find exact text and replace)\",\n\twrite: \"Create or overwrite files\",\n\tgrep: \"Search file contents for patterns (respects .gitignore)\",\n\tfind: \"Find files by glob pattern (respects .gitignore)\",\n\tls: \"List directory contents\",\n};\n\n/** Resolve input as file path or literal string */\nfunction resolvePromptInput(input: string | undefined, description: string): string | undefined {\n\tif (!input) {\n\t\treturn undefined;\n\t}\n\n\tif (existsSync(input)) {\n\t\ttry {\n\t\t\treturn readFileSync(input, \"utf-8\");\n\t\t} catch (error) {\n\t\t\tconsole.error(chalk.yellow(`Warning: Could not read ${description} file ${input}: ${error}`));\n\t\t\treturn input;\n\t\t}\n\t}\n\n\treturn input;\n}\n\n/** Look for AGENTS.md or CLAUDE.md in a directory (prefers AGENTS.md) */\nfunction loadContextFileFromDir(dir: string): { path: string; content: string } | null {\n\tconst candidates = [\"AGENTS.md\", \"CLAUDE.md\"];\n\tfor (const filename of candidates) {\n\t\tconst filePath = join(dir, filename);\n\t\tif (existsSync(filePath)) {\n\t\t\ttry {\n\t\t\t\treturn {\n\t\t\t\t\tpath: filePath,\n\t\t\t\t\tcontent: readFileSync(filePath, \"utf-8\"),\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(chalk.yellow(`Warning: Could not read ${filePath}: ${error}`));\n\t\t\t}\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Load all project context files in order:\n * 1. Global: ~/{CONFIG_DIR_NAME}/agent/AGENTS.md or CLAUDE.md\n * 2. Parent directories (top-most first) down to cwd\n * Each returns {path, content} for separate messages\n */\nexport function loadProjectContextFiles(): Array<{ path: string; content: string }> {\n\tconst contextFiles: Array<{ path: string; content: string }> = [];\n\n\t// 1. Load global context from ~/{CONFIG_DIR_NAME}/agent/\n\tconst globalContextDir = getAgentDir();\n\tconst globalContext = loadContextFileFromDir(globalContextDir);\n\tif (globalContext) {\n\t\tcontextFiles.push(globalContext);\n\t}\n\n\t// 2. Walk up from cwd to root, collecting all context files\n\tconst cwd = process.cwd();\n\tconst ancestorContextFiles: Array<{ path: string; content: string }> = [];\n\n\tlet currentDir = cwd;\n\tconst root = resolve(\"/\");\n\n\twhile (true) {\n\t\tconst contextFile = loadContextFileFromDir(currentDir);\n\t\tif (contextFile) {\n\t\t\t// Add to beginning so we get top-most parent first\n\t\t\tancestorContextFiles.unshift(contextFile);\n\t\t}\n\n\t\t// Stop if we've reached root\n\t\tif (currentDir === root) break;\n\n\t\t// Move up one directory\n\t\tconst parentDir = resolve(currentDir, \"..\");\n\t\tif (parentDir === currentDir) break; // Safety check\n\t\tcurrentDir = parentDir;\n\t}\n\n\t// Add ancestor files in order (top-most → cwd)\n\tcontextFiles.push(...ancestorContextFiles);\n\n\treturn contextFiles;\n}\n\nexport interface BuildSystemPromptOptions {\n\tcustomPrompt?: string;\n\tselectedTools?: ToolName[];\n\tappendSystemPrompt?: string;\n\tskillsEnabled?: boolean;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {\n\tconst { customPrompt, selectedTools, appendSystemPrompt, skillsEnabled = true } = options;\n\tconst resolvedCustomPrompt = resolvePromptInput(customPrompt, \"system prompt\");\n\tconst resolvedAppendPrompt = resolvePromptInput(appendSystemPrompt, \"append system prompt\");\n\n\tconst now = new Date();\n\tconst dateTime = now.toLocaleString(\"en-US\", {\n\t\tweekday: \"long\",\n\t\tyear: \"numeric\",\n\t\tmonth: \"long\",\n\t\tday: \"numeric\",\n\t\thour: \"2-digit\",\n\t\tminute: \"2-digit\",\n\t\tsecond: \"2-digit\",\n\t\ttimeZoneName: \"short\",\n\t});\n\n\tconst appendSection = resolvedAppendPrompt ? `\\n\\n${resolvedAppendPrompt}` : \"\";\n\n\tif (resolvedCustomPrompt) {\n\t\tlet prompt = resolvedCustomPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tconst contextFiles = loadProjectContextFiles();\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"The following project context files have been loaded:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst customPromptHasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (skillsEnabled && customPromptHasRead) {\n\t\t\tconst skills = loadSkills();\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Add date/time and working directory last\n\t\tprompt += `\\nCurrent date and time: ${dateTime}`;\n\t\tprompt += `\\nCurrent working directory: ${process.cwd()}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Get absolute paths to documentation\n\tconst readmePath = getReadmePath();\n\tconst docsPath = getDocsPath();\n\n\t// Build tools list based on selected tools\n\tconst tools = selectedTools || ([\"read\", \"bash\", \"edit\", \"write\"] as ToolName[]);\n\tconst toolsList = tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join(\"\\n\");\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasEdit = tools.includes(\"edit\");\n\tconst hasWrite = tools.includes(\"write\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\n\t// Read-only mode notice (no bash, edit, or write)\n\tif (!hasBash && !hasEdit && !hasWrite) {\n\t\tguidelinesList.push(\"You are in READ-ONLY mode - you cannot modify files or execute arbitrary commands\");\n\t}\n\n\t// Bash without edit/write = read-only bash mode\n\tif (hasBash && !hasEdit && !hasWrite) {\n\t\tguidelinesList.push(\n\t\t\t\"Use bash ONLY for read-only operations (git log, gh issue view, curl, etc.) - do NOT modify any files\",\n\t\t);\n\t}\n\n\t// File exploration guidelines\n\tif (hasBash && !hasGrep && !hasFind && !hasLs) {\n\t\tguidelinesList.push(\"Use bash for file operations like ls, grep, find\");\n\t} else if (hasBash && (hasGrep || hasFind || hasLs)) {\n\t\tguidelinesList.push(\"Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)\");\n\t}\n\n\t// Read before edit guideline\n\tif (hasRead && hasEdit) {\n\t\tguidelinesList.push(\"Use read to examine files before editing\");\n\t}\n\n\t// Edit guideline\n\tif (hasEdit) {\n\t\tguidelinesList.push(\"Use edit for precise changes (old text must match exactly)\");\n\t}\n\n\t// Write guideline\n\tif (hasWrite) {\n\t\tguidelinesList.push(\"Use write only for new files or complete rewrites\");\n\t}\n\n\t// Output guideline (only when actually writing/executing)\n\tif (hasEdit || hasWrite) {\n\t\tguidelinesList.push(\n\t\t\t\"When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did\",\n\t\t);\n\t}\n\n\t// Always include these\n\tguidelinesList.push(\"Be concise in your responses\");\n\tguidelinesList.push(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nGuidelines:\n${guidelines}\n\nDocumentation:\n- Main documentation: ${readmePath}\n- Additional docs: ${docsPath}\n- When asked about: custom models/providers (README sufficient), themes (docs/theme.md), skills (docs/skills.md), hooks (docs/hooks.md), custom tools (docs/custom-tools.md), RPC (docs/rpc.md)`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tconst contextFiles = loadProjectContextFiles();\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"The following project context files have been loaded:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (skillsEnabled && hasRead) {\n\t\tconst skills = loadSkills();\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Add date/time and working directory last\n\tprompt += `\\nCurrent date and time: ${dateTime}`;\n\tprompt += `\\nCurrent working directory: ${process.cwd()}`;\n\n\treturn prompt;\n}\n"]}
1
+ {"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAkDjD;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAqClF;AAED,MAAM,WAAW,wBAAwB;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,QAAQ,EAAE,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,GAAG,MAAM,CA0JhF","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport chalk from \"chalk\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { getAgentDir, getDocsPath, getReadmePath } from \"../config.js\";\nimport { formatSkillsForPrompt, loadSkills } from \"./skills.js\";\nimport type { ToolName } from \"./tools/index.js\";\n\n/** Tool descriptions for system prompt */\nconst toolDescriptions: Record<ToolName, string> = {\n\tread: \"Read file contents\",\n\tbash: \"Execute bash commands (ls, grep, find, etc.)\",\n\tedit: \"Make surgical edits to files (find exact text and replace)\",\n\twrite: \"Create or overwrite files\",\n\tgrep: \"Search file contents for patterns (respects .gitignore)\",\n\tfind: \"Find files by glob pattern (respects .gitignore)\",\n\tls: \"List directory contents\",\n};\n\n/** Resolve input as file path or literal string */\nfunction resolvePromptInput(input: string | undefined, description: string): string | undefined {\n\tif (!input) {\n\t\treturn undefined;\n\t}\n\n\tif (existsSync(input)) {\n\t\ttry {\n\t\t\treturn readFileSync(input, \"utf-8\");\n\t\t} catch (error) {\n\t\t\tconsole.error(chalk.yellow(`Warning: Could not read ${description} file ${input}: ${error}`));\n\t\t\treturn input;\n\t\t}\n\t}\n\n\treturn input;\n}\n\n/** Look for AGENTS.md or CLAUDE.md in a directory (prefers AGENTS.md) */\nfunction loadContextFileFromDir(dir: string): { path: string; content: string } | null {\n\tconst candidates = [\"AGENTS.md\", \"CLAUDE.md\"];\n\tfor (const filename of candidates) {\n\t\tconst filePath = join(dir, filename);\n\t\tif (existsSync(filePath)) {\n\t\t\ttry {\n\t\t\t\treturn {\n\t\t\t\t\tpath: filePath,\n\t\t\t\t\tcontent: readFileSync(filePath, \"utf-8\"),\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(chalk.yellow(`Warning: Could not read ${filePath}: ${error}`));\n\t\t\t}\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Load all project context files in order:\n * 1. Global: ~/{CONFIG_DIR_NAME}/agent/AGENTS.md or CLAUDE.md\n * 2. Parent directories (top-most first) down to cwd\n * Each returns {path, content} for separate messages\n */\nexport function loadProjectContextFiles(): Array<{ path: string; content: string }> {\n\tconst contextFiles: Array<{ path: string; content: string }> = [];\n\n\t// 1. Load global context from ~/{CONFIG_DIR_NAME}/agent/\n\tconst globalContextDir = getAgentDir();\n\tconst globalContext = loadContextFileFromDir(globalContextDir);\n\tif (globalContext) {\n\t\tcontextFiles.push(globalContext);\n\t}\n\n\t// 2. Walk up from cwd to root, collecting all context files\n\tconst cwd = process.cwd();\n\tconst ancestorContextFiles: Array<{ path: string; content: string }> = [];\n\n\tlet currentDir = cwd;\n\tconst root = resolve(\"/\");\n\n\twhile (true) {\n\t\tconst contextFile = loadContextFileFromDir(currentDir);\n\t\tif (contextFile) {\n\t\t\t// Add to beginning so we get top-most parent first\n\t\t\tancestorContextFiles.unshift(contextFile);\n\t\t}\n\n\t\t// Stop if we've reached root\n\t\tif (currentDir === root) break;\n\n\t\t// Move up one directory\n\t\tconst parentDir = resolve(currentDir, \"..\");\n\t\tif (parentDir === currentDir) break; // Safety check\n\t\tcurrentDir = parentDir;\n\t}\n\n\t// Add ancestor files in order (top-most → cwd)\n\tcontextFiles.push(...ancestorContextFiles);\n\n\treturn contextFiles;\n}\n\nexport interface BuildSystemPromptOptions {\n\tcustomPrompt?: string;\n\tselectedTools?: ToolName[];\n\tappendSystemPrompt?: string;\n\tskillsEnabled?: boolean;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {\n\tconst { customPrompt, selectedTools, appendSystemPrompt, skillsEnabled = true } = options;\n\tconst resolvedCustomPrompt = resolvePromptInput(customPrompt, \"system prompt\");\n\tconst resolvedAppendPrompt = resolvePromptInput(appendSystemPrompt, \"append system prompt\");\n\n\tconst now = new Date();\n\tconst dateTime = now.toLocaleString(\"en-US\", {\n\t\tweekday: \"long\",\n\t\tyear: \"numeric\",\n\t\tmonth: \"long\",\n\t\tday: \"numeric\",\n\t\thour: \"2-digit\",\n\t\tminute: \"2-digit\",\n\t\tsecond: \"2-digit\",\n\t\ttimeZoneName: \"short\",\n\t});\n\n\tconst appendSection = resolvedAppendPrompt ? `\\n\\n${resolvedAppendPrompt}` : \"\";\n\n\tif (resolvedCustomPrompt) {\n\t\tlet prompt = resolvedCustomPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tconst contextFiles = loadProjectContextFiles();\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"The following project context files have been loaded:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst customPromptHasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (skillsEnabled && customPromptHasRead) {\n\t\t\tconst { skills } = loadSkills();\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Add date/time and working directory last\n\t\tprompt += `\\nCurrent date and time: ${dateTime}`;\n\t\tprompt += `\\nCurrent working directory: ${process.cwd()}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Get absolute paths to documentation\n\tconst readmePath = getReadmePath();\n\tconst docsPath = getDocsPath();\n\n\t// Build tools list based on selected tools\n\tconst tools = selectedTools || ([\"read\", \"bash\", \"edit\", \"write\"] as ToolName[]);\n\tconst toolsList = tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join(\"\\n\");\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasEdit = tools.includes(\"edit\");\n\tconst hasWrite = tools.includes(\"write\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\n\t// Read-only mode notice (no bash, edit, or write)\n\tif (!hasBash && !hasEdit && !hasWrite) {\n\t\tguidelinesList.push(\"You are in READ-ONLY mode - you cannot modify files or execute arbitrary commands\");\n\t}\n\n\t// Bash without edit/write = read-only bash mode\n\tif (hasBash && !hasEdit && !hasWrite) {\n\t\tguidelinesList.push(\n\t\t\t\"Use bash ONLY for read-only operations (git log, gh issue view, curl, etc.) - do NOT modify any files\",\n\t\t);\n\t}\n\n\t// File exploration guidelines\n\tif (hasBash && !hasGrep && !hasFind && !hasLs) {\n\t\tguidelinesList.push(\"Use bash for file operations like ls, grep, find\");\n\t} else if (hasBash && (hasGrep || hasFind || hasLs)) {\n\t\tguidelinesList.push(\"Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)\");\n\t}\n\n\t// Read before edit guideline\n\tif (hasRead && hasEdit) {\n\t\tguidelinesList.push(\"Use read to examine files before editing\");\n\t}\n\n\t// Edit guideline\n\tif (hasEdit) {\n\t\tguidelinesList.push(\"Use edit for precise changes (old text must match exactly)\");\n\t}\n\n\t// Write guideline\n\tif (hasWrite) {\n\t\tguidelinesList.push(\"Use write only for new files or complete rewrites\");\n\t}\n\n\t// Output guideline (only when actually writing/executing)\n\tif (hasEdit || hasWrite) {\n\t\tguidelinesList.push(\n\t\t\t\"When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did\",\n\t\t);\n\t}\n\n\t// Always include these\n\tguidelinesList.push(\"Be concise in your responses\");\n\tguidelinesList.push(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nGuidelines:\n${guidelines}\n\nDocumentation:\n- Main documentation: ${readmePath}\n- Additional docs: ${docsPath}\n- When asked about: custom models/providers (README sufficient), themes (docs/theme.md), skills (docs/skills.md), hooks (docs/hooks.md), custom tools (docs/custom-tools.md), RPC (docs/rpc.md)`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tconst contextFiles = loadProjectContextFiles();\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"The following project context files have been loaded:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (skillsEnabled && hasRead) {\n\t\tconst { skills } = loadSkills();\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Add date/time and working directory last\n\tprompt += `\\nCurrent date and time: ${dateTime}`;\n\tprompt += `\\nCurrent working directory: ${process.cwd()}`;\n\n\treturn prompt;\n}\n"]}
@@ -123,7 +123,7 @@ export function buildSystemPrompt(options = {}) {
123
123
  // Append skills section (only if read tool is available)
124
124
  const customPromptHasRead = !selectedTools || selectedTools.includes("read");
125
125
  if (skillsEnabled && customPromptHasRead) {
126
- const skills = loadSkills();
126
+ const { skills } = loadSkills();
127
127
  prompt += formatSkillsForPrompt(skills);
128
128
  }
129
129
  // Add date/time and working directory last
@@ -207,7 +207,7 @@ Documentation:
207
207
  }
208
208
  // Append skills section (only if read tool is available)
209
209
  if (skillsEnabled && hasRead) {
210
- const skills = loadSkills();
210
+ const { skills } = loadSkills();
211
211
  prompt += formatSkillsForPrompt(skills);
212
212
  }
213
213
  // Add date/time and working directory last