pikiloop 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/README.v2.md +287 -0
  4. package/README.zh-CN.md +352 -0
  5. package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
  6. package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
  7. package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
  8. package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
  9. package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
  10. package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
  11. package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
  12. package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
  13. package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
  14. package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
  15. package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
  16. package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
  17. package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
  18. package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
  19. package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
  20. package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
  21. package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
  22. package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
  23. package/dashboard/dist/assets/index-reSbuley.css +1 -0
  24. package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
  25. package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
  26. package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
  27. package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
  28. package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
  29. package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
  30. package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
  31. package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
  32. package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
  33. package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
  34. package/dashboard/dist/favicon.svg +28 -0
  35. package/dashboard/dist/index.html +17 -0
  36. package/dist/agent/acp-client.js +261 -0
  37. package/dist/agent/auto-update.js +432 -0
  38. package/dist/agent/await-resume.js +50 -0
  39. package/dist/agent/cli/auth.js +325 -0
  40. package/dist/agent/cli/catalog.js +40 -0
  41. package/dist/agent/cli/detector.js +136 -0
  42. package/dist/agent/cli/index.js +7 -0
  43. package/dist/agent/cli/registry.js +33 -0
  44. package/dist/agent/driver.js +39 -0
  45. package/dist/agent/drivers/claude-tui.js +2297 -0
  46. package/dist/agent/drivers/claude.js +2689 -0
  47. package/dist/agent/drivers/codex.js +2210 -0
  48. package/dist/agent/drivers/gemini.js +1059 -0
  49. package/dist/agent/drivers/hermes.js +795 -0
  50. package/dist/agent/goal.js +274 -0
  51. package/dist/agent/handover.js +130 -0
  52. package/dist/agent/images.js +355 -0
  53. package/dist/agent/index.js +50 -0
  54. package/dist/agent/mcp/bridge.js +791 -0
  55. package/dist/agent/mcp/extensions.js +637 -0
  56. package/dist/agent/mcp/oauth.js +353 -0
  57. package/dist/agent/mcp/registry.js +119 -0
  58. package/dist/agent/mcp/session-server.js +229 -0
  59. package/dist/agent/mcp/tools/ask-user.js +113 -0
  60. package/dist/agent/mcp/tools/await-resume.js +77 -0
  61. package/dist/agent/mcp/tools/goal.js +144 -0
  62. package/dist/agent/mcp/tools/types.js +12 -0
  63. package/dist/agent/mcp/tools/workspace.js +212 -0
  64. package/dist/agent/npm.js +31 -0
  65. package/dist/agent/session.js +1206 -0
  66. package/dist/agent/skill-installer.js +160 -0
  67. package/dist/agent/skills.js +257 -0
  68. package/dist/agent/stream.js +743 -0
  69. package/dist/agent/types.js +13 -0
  70. package/dist/agent/utils.js +687 -0
  71. package/dist/bot/bot.js +2499 -0
  72. package/dist/bot/command-ui.js +633 -0
  73. package/dist/bot/commands.js +513 -0
  74. package/dist/bot/headless-bot.js +36 -0
  75. package/dist/bot/host.js +192 -0
  76. package/dist/bot/human-loop.js +168 -0
  77. package/dist/bot/menu.js +48 -0
  78. package/dist/bot/orchestration.js +79 -0
  79. package/dist/bot/render-shared.js +309 -0
  80. package/dist/bot/session-hub.js +361 -0
  81. package/dist/bot/session-status.js +55 -0
  82. package/dist/bot/streaming.js +309 -0
  83. package/dist/browser-profile.js +579 -0
  84. package/dist/browser-supervisor.js +249 -0
  85. package/dist/catalog/cli-tools.js +421 -0
  86. package/dist/catalog/index.js +21 -0
  87. package/dist/catalog/local-models.js +94 -0
  88. package/dist/catalog/mcp-servers.js +315 -0
  89. package/dist/catalog/skill-repos.js +173 -0
  90. package/dist/channels/base.js +55 -0
  91. package/dist/channels/dingtalk/bot.js +549 -0
  92. package/dist/channels/dingtalk/channel.js +268 -0
  93. package/dist/channels/discord/bot.js +552 -0
  94. package/dist/channels/discord/channel.js +245 -0
  95. package/dist/channels/feishu/bot.js +1275 -0
  96. package/dist/channels/feishu/channel.js +911 -0
  97. package/dist/channels/feishu/markdown.js +91 -0
  98. package/dist/channels/feishu/render.js +619 -0
  99. package/dist/channels/health.js +109 -0
  100. package/dist/channels/slack/bot.js +554 -0
  101. package/dist/channels/slack/channel.js +283 -0
  102. package/dist/channels/states.js +6 -0
  103. package/dist/channels/telegram/bot.js +1310 -0
  104. package/dist/channels/telegram/channel.js +820 -0
  105. package/dist/channels/telegram/directory.js +111 -0
  106. package/dist/channels/telegram/live-preview.js +220 -0
  107. package/dist/channels/telegram/render.js +384 -0
  108. package/dist/channels/wecom/bot.js +558 -0
  109. package/dist/channels/wecom/channel.js +479 -0
  110. package/dist/channels/weixin/api.js +520 -0
  111. package/dist/channels/weixin/bot.js +1000 -0
  112. package/dist/channels/weixin/channel.js +222 -0
  113. package/dist/cli/autostart.js +262 -0
  114. package/dist/cli/channel-supervisor.js +313 -0
  115. package/dist/cli/channels.js +54 -0
  116. package/dist/cli/main.js +726 -0
  117. package/dist/cli/onboarding.js +227 -0
  118. package/dist/cli/run.js +308 -0
  119. package/dist/cli/setup-wizard.js +235 -0
  120. package/dist/core/config/runtime-config.js +201 -0
  121. package/dist/core/config/user-config.js +510 -0
  122. package/dist/core/config/validation.js +521 -0
  123. package/dist/core/constants.js +400 -0
  124. package/dist/core/git.js +145 -0
  125. package/dist/core/legacy-compat.js +60 -0
  126. package/dist/core/logging.js +101 -0
  127. package/dist/core/platform.js +59 -0
  128. package/dist/core/process-control.js +315 -0
  129. package/dist/core/secrets/index.js +42 -0
  130. package/dist/core/secrets/inline-seal.js +60 -0
  131. package/dist/core/secrets/ref.js +33 -0
  132. package/dist/core/secrets/resolver.js +65 -0
  133. package/dist/core/secrets/store.js +63 -0
  134. package/dist/core/utils.js +233 -0
  135. package/dist/core/version.js +15 -0
  136. package/dist/dashboard/platform.js +219 -0
  137. package/dist/dashboard/routes/agents.js +450 -0
  138. package/dist/dashboard/routes/cli.js +174 -0
  139. package/dist/dashboard/routes/config.js +523 -0
  140. package/dist/dashboard/routes/extensions.js +745 -0
  141. package/dist/dashboard/routes/local-models.js +290 -0
  142. package/dist/dashboard/routes/models.js +324 -0
  143. package/dist/dashboard/routes/sessions.js +838 -0
  144. package/dist/dashboard/runtime.js +410 -0
  145. package/dist/dashboard/server.js +237 -0
  146. package/dist/dashboard/session-control.js +347 -0
  147. package/dist/model/catalog.js +104 -0
  148. package/dist/model/index.js +20 -0
  149. package/dist/model/injector.js +272 -0
  150. package/dist/model/provider-models.js +112 -0
  151. package/dist/model/store.js +212 -0
  152. package/dist/model/types.js +13 -0
  153. package/dist/model/validation.js +203 -0
  154. package/package.json +82 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Skill installer — wrapper around `npx skills` CLI.
3
+ *
4
+ * Skills are installed via the community-standard `npx skills add` command.
5
+ * Global skills go to ~/.pikiloop/skills/, project skills to <workdir>/.pikiloop/skills/.
6
+ *
7
+ * The upstream CLI doesn't recognize `pikiloop` as an agent, so we install with
8
+ * `--agent claude-code` (the driver pikiloop runs by default) and rely on
9
+ * ~/.claude/skills → ~/.pikiloop/skills being symlinked to the same directory.
10
+ */
11
+ import { execFile } from 'node:child_process';
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { STATE_DIR_NAME } from '../core/constants.js';
16
+ // ---------------------------------------------------------------------------
17
+ // Constants
18
+ // ---------------------------------------------------------------------------
19
+ const GLOBAL_SKILLS_DIR = path.join(os.homedir(), STATE_DIR_NAME, 'skills');
20
+ const INSTALL_TIMEOUT_MS = 60_000;
21
+ const REMOVE_TIMEOUT_MS = 10_000;
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Make sure the global skills directory exists, and that the agent-specific
27
+ * dirs that the upstream skills CLI writes to (`~/.claude/skills`,
28
+ * `~/.agents/skills`) resolve back to it. This is what lets us install with
29
+ * `--agent claude-code` and still read the results from `~/.pikiloop/skills`.
30
+ */
31
+ function ensureGlobalSkillsDir() {
32
+ fs.mkdirSync(GLOBAL_SKILLS_DIR, { recursive: true });
33
+ for (const linkDir of [
34
+ path.join(os.homedir(), '.claude', 'skills'),
35
+ path.join(os.homedir(), '.agents', 'skills'),
36
+ ]) {
37
+ try {
38
+ const stat = fs.lstatSync(linkDir);
39
+ if (stat.isSymbolicLink()) {
40
+ const real = fs.realpathSync(linkDir);
41
+ if (real === fs.realpathSync(GLOBAL_SKILLS_DIR))
42
+ continue;
43
+ }
44
+ // Existing dir/link doesn't match — leave it alone rather than destroy user data.
45
+ continue;
46
+ }
47
+ catch {
48
+ try {
49
+ fs.mkdirSync(path.dirname(linkDir), { recursive: true });
50
+ fs.symlinkSync(GLOBAL_SKILLS_DIR, linkDir, 'dir');
51
+ }
52
+ catch { /* best effort */ }
53
+ }
54
+ }
55
+ }
56
+ function runNpx(args, cwd, timeoutMs) {
57
+ return new Promise((resolve) => {
58
+ const child = execFile('npx', args, {
59
+ cwd,
60
+ timeout: timeoutMs,
61
+ env: { ...process.env, NODE_NO_WARNINGS: '1' },
62
+ shell: process.platform === 'win32',
63
+ }, (error, stdout, stderr) => {
64
+ resolve({
65
+ ok: !error,
66
+ stdout: stdout?.toString() || '',
67
+ stderr: stderr?.toString() || '',
68
+ });
69
+ });
70
+ // Prevent child from keeping parent alive
71
+ child.unref?.();
72
+ });
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // Install
76
+ // ---------------------------------------------------------------------------
77
+ /**
78
+ * Install a skill from a source (GitHub owner/repo, URL, or local path).
79
+ *
80
+ * Uses `npx skills add <source>` with appropriate flags.
81
+ */
82
+ export async function installSkill(source, opts = {}) {
83
+ const { global: isGlobal, skill, workdir } = opts;
84
+ if (!isGlobal && !workdir) {
85
+ return { ok: false, error: 'workdir is required for project-scoped skill installation' };
86
+ }
87
+ const cwd = isGlobal ? os.homedir() : workdir;
88
+ const args = ['-y', 'skills', 'add', source, '--yes', '--agent', 'claude-code'];
89
+ if (isGlobal) {
90
+ args.push('-g');
91
+ ensureGlobalSkillsDir();
92
+ }
93
+ if (skill) {
94
+ args.push('-s', skill);
95
+ }
96
+ const result = await runNpx(args, cwd, INSTALL_TIMEOUT_MS);
97
+ if (!result.ok) {
98
+ const errorMsg = result.stderr.trim().split('\n').pop()?.trim() || 'installation failed';
99
+ return { ok: false, error: errorMsg, output: result.stdout + result.stderr };
100
+ }
101
+ return { ok: true, output: result.stdout };
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // Remove
105
+ // ---------------------------------------------------------------------------
106
+ /**
107
+ * Remove an installed skill by name.
108
+ * Deletes the skill directory from the appropriate location.
109
+ */
110
+ export function removeSkill(skillName, opts = {}) {
111
+ const { global: isGlobal, workdir } = opts;
112
+ if (!isGlobal && !workdir) {
113
+ return { ok: false, error: 'workdir is required for project-scoped skill removal' };
114
+ }
115
+ // Security: prevent path traversal — skill name must be a plain directory name
116
+ const sanitized = path.basename(skillName);
117
+ if (!sanitized || sanitized === '.' || sanitized === '..' || sanitized !== skillName) {
118
+ return { ok: false, error: 'invalid skill name' };
119
+ }
120
+ const parentDir = isGlobal
121
+ ? GLOBAL_SKILLS_DIR
122
+ : path.join(workdir, STATE_DIR_NAME, 'skills');
123
+ const skillDir = path.join(parentDir, sanitized);
124
+ // Double-check the resolved path is inside the expected parent
125
+ const realParent = path.resolve(parentDir);
126
+ const realSkill = path.resolve(skillDir);
127
+ if (!realSkill.startsWith(realParent + path.sep)) {
128
+ return { ok: false, error: 'invalid skill path' };
129
+ }
130
+ try {
131
+ if (!fs.existsSync(skillDir)) {
132
+ return { ok: false, error: `skill "${sanitized}" not found` };
133
+ }
134
+ fs.rmSync(skillDir, { recursive: true, force: true });
135
+ return { ok: true };
136
+ }
137
+ catch (e) {
138
+ return { ok: false, error: e?.message || 'removal failed' };
139
+ }
140
+ }
141
+ // ---------------------------------------------------------------------------
142
+ // List installed (enhanced)
143
+ // ---------------------------------------------------------------------------
144
+ export function getGlobalSkillsDir() {
145
+ return GLOBAL_SKILLS_DIR;
146
+ }
147
+ // ---------------------------------------------------------------------------
148
+ // Check for updates
149
+ // ---------------------------------------------------------------------------
150
+ export async function checkSkillUpdates(opts = {}) {
151
+ const cwd = opts.global ? os.homedir() : (opts.workdir || process.cwd());
152
+ const args = ['-y', 'skills', 'check'];
153
+ if (opts.global)
154
+ args.push('-g');
155
+ return runNpx(args, cwd, INSTALL_TIMEOUT_MS).then(r => ({
156
+ ok: r.ok,
157
+ output: r.stdout,
158
+ error: r.ok ? undefined : r.stderr,
159
+ }));
160
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Project skill discovery from .pikiloop/skills and .claude/commands.
3
+ */
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { STATE_DIR_NAME, LEGACY_STATE_DIR_NAME } from '../core/constants.js';
8
+ function resolveProjectSkillRoots(workdir) {
9
+ return {
10
+ canonicalRoot: path.join(workdir, STATE_DIR_NAME, 'skills'),
11
+ legacyRoot: path.join(workdir, LEGACY_STATE_DIR_NAME, 'skills'),
12
+ claudeRoot: path.join(workdir, '.claude', 'skills'),
13
+ agentsRoot: path.join(workdir, '.agents', 'skills'),
14
+ };
15
+ }
16
+ function resolveSkillFile(root, skillName) {
17
+ return path.join(root, skillName, 'SKILL.md');
18
+ }
19
+ function parseSkillMeta(content) {
20
+ let label = null;
21
+ let description = null;
22
+ let mcpRequires;
23
+ const fm = content.match(/^---\s*\n([\s\S]*?)\n---/);
24
+ if (fm) {
25
+ const lm = fm[1].match(/^label:\s*(.+)/m);
26
+ if (lm)
27
+ label = lm[1].trim();
28
+ const dm = fm[1].match(/^description:\s*(.+)/m);
29
+ if (dm)
30
+ description = dm[1].trim();
31
+ // Parse mcp_requires as YAML list
32
+ const mr = fm[1].match(/^mcp_requires:\s*\n((?:\s+-\s+.+\n?)+)/m);
33
+ if (mr) {
34
+ mcpRequires = mr[1]
35
+ .split('\n')
36
+ .map(l => l.replace(/^\s*-\s*/, '').replace(/["']/g, '').trim())
37
+ .filter(Boolean);
38
+ }
39
+ }
40
+ if (!label) {
41
+ const hm = content.match(/^#\s+(.+)$/m);
42
+ if (hm)
43
+ label = hm[1].trim();
44
+ }
45
+ return { label, description, mcpRequires };
46
+ }
47
+ function hasFile(filePath) {
48
+ try {
49
+ return fs.statSync(filePath).isFile();
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ function hasDir(dirPath) {
56
+ try {
57
+ return fs.statSync(dirPath).isDirectory();
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ function readSortedDir(dirPath) {
64
+ try {
65
+ return fs.readdirSync(dirPath).sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }));
66
+ }
67
+ catch {
68
+ return [];
69
+ }
70
+ }
71
+ function listRelativeFiles(dirPath, prefix = '') {
72
+ const files = [];
73
+ for (const entry of readSortedDir(dirPath)) {
74
+ const abs = path.join(dirPath, entry);
75
+ const rel = prefix ? path.join(prefix, entry) : entry;
76
+ let stat;
77
+ try {
78
+ stat = fs.statSync(abs);
79
+ }
80
+ catch {
81
+ continue;
82
+ }
83
+ if (stat.isDirectory())
84
+ files.push(...listRelativeFiles(abs, rel));
85
+ else if (stat.isFile())
86
+ files.push(rel);
87
+ }
88
+ return files;
89
+ }
90
+ function realPathOrNull(filePath) {
91
+ try {
92
+ return fs.realpathSync(filePath);
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
98
+ function ensureDirSymlink(linkPath, targetDir) {
99
+ const desiredTarget = path.relative(path.dirname(linkPath), targetDir) || '.';
100
+ try {
101
+ const stat = fs.lstatSync(linkPath);
102
+ if (stat.isSymbolicLink()) {
103
+ const currentTarget = fs.readlinkSync(linkPath);
104
+ const currentReal = realPathOrNull(path.resolve(path.dirname(linkPath), currentTarget));
105
+ const desiredReal = realPathOrNull(targetDir);
106
+ if (currentTarget === desiredTarget || (currentReal && desiredReal && currentReal === desiredReal))
107
+ return;
108
+ }
109
+ fs.rmSync(linkPath, { recursive: true, force: true });
110
+ }
111
+ catch { }
112
+ fs.mkdirSync(path.dirname(linkPath), { recursive: true });
113
+ fs.symlinkSync(desiredTarget, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
114
+ }
115
+ function copyMergedTree(sourceRoot, targetRoot, opts = {}) {
116
+ for (const relPath of listRelativeFiles(sourceRoot)) {
117
+ const sourcePath = path.join(sourceRoot, relPath);
118
+ const targetPath = path.join(targetRoot, relPath);
119
+ if (hasFile(targetPath)) {
120
+ opts.log?.(`skills merge skipped existing file: ${relPath}`);
121
+ continue;
122
+ }
123
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
124
+ fs.copyFileSync(sourcePath, targetPath);
125
+ }
126
+ }
127
+ export function initializeProjectSkills(workdir, opts = {}) {
128
+ const { canonicalRoot, claudeRoot, agentsRoot } = resolveProjectSkillRoots(workdir);
129
+ fs.mkdirSync(canonicalRoot, { recursive: true });
130
+ const canonicalReal = realPathOrNull(canonicalRoot);
131
+ for (const legacyRoot of [claudeRoot, agentsRoot]) {
132
+ if (!hasDir(legacyRoot))
133
+ continue;
134
+ const legacyReal = realPathOrNull(legacyRoot);
135
+ if (legacyReal && canonicalReal && legacyReal === canonicalReal)
136
+ continue;
137
+ copyMergedTree(legacyRoot, canonicalRoot, opts);
138
+ }
139
+ for (const linkRoot of [claudeRoot, agentsRoot]) {
140
+ ensureDirSymlink(linkRoot, canonicalRoot);
141
+ }
142
+ opts.log?.(`skills merged into .pikiloop/skills and linked to .claude/.agents workdir=${workdir}`);
143
+ }
144
+ export function getProjectSkillPaths(workdir, skillName) {
145
+ const { canonicalRoot, claudeRoot, agentsRoot } = resolveProjectSkillRoots(workdir);
146
+ const sharedSkillFile = resolveSkillFile(canonicalRoot, skillName);
147
+ const agentsSkillFile = resolveSkillFile(agentsRoot, skillName);
148
+ const claudeSkillFile = resolveSkillFile(claudeRoot, skillName);
149
+ return {
150
+ sharedSkillFile: hasFile(sharedSkillFile) ? sharedSkillFile : null,
151
+ agentsSkillFile: hasFile(agentsSkillFile) ? agentsSkillFile : null,
152
+ claudeSkillFile: hasFile(claudeSkillFile) ? claudeSkillFile : null,
153
+ };
154
+ }
155
+ // Matches the canonical prompt produced by `resolveSkillPrompt` (bot/commands)
156
+ // and `resolveSkillFromPrompt` (dashboard/session-control). Both build the same
157
+ // shape: `[Project directory: <wd>]\n\nRead the skill definition at \`<path>\`
158
+ // and execute the instructions defined there.[ Additional context: <args>]`.
159
+ //
160
+ // Whitespace between the segments is tolerant (`\s+`) so the regex still
161
+ // matches after the claude driver collapses interior `\s+` to single spaces
162
+ // when surfacing user text in `getClaudeSessionMessages`.
163
+ const SKILL_PROMPT_RE = /^\[Project directory: [^\]\n]+?\]\s+Read the skill definition at `([^`\n]+)` and execute the instructions defined there\.(?:\s+Additional context:\s+([\s\S]+?))?\s*$/;
164
+ /**
165
+ * Inverse of `resolveSkillPrompt`. When a stored user message matches the
166
+ * canonical skill-execution expansion, return the original `/skillname [args]`
167
+ * shorthand for display. Returns null when the text isn't a recognized skill
168
+ * prompt — callers should fall back to the raw text.
169
+ *
170
+ * The expanded form is what the agent CLI actually consumed and what gets
171
+ * persisted to its session log; this collapse exists purely so the dashboard
172
+ * (and other display surfaces) can render the slash command the user typed.
173
+ */
174
+ export function collapseSkillPrompt(text) {
175
+ if (!text)
176
+ return null;
177
+ const m = SKILL_PROMPT_RE.exec(text);
178
+ if (!m)
179
+ return null;
180
+ // Skill files are always at `<root>/<skillName>/SKILL.md`. Split on both
181
+ // `/` and `\` so Windows-generated paths (path.join) resolve correctly.
182
+ const segments = m[1].split(/[/\\]/).filter(Boolean);
183
+ if (segments.length < 2 || segments[segments.length - 1] !== 'SKILL.md')
184
+ return null;
185
+ const name = segments[segments.length - 2];
186
+ if (!name)
187
+ return null;
188
+ const args = (m[2] || '').trim();
189
+ return args ? `/${name} ${args}` : `/${name}`;
190
+ }
191
+ const GLOBAL_SKILLS_ROOT = path.join(os.homedir(), STATE_DIR_NAME, 'skills');
192
+ // Per-file cache of parsed SKILL.md metadata, keyed by file mtime. listSkills runs
193
+ // on every skills-menu render (IM + dashboard); without this it re-read and
194
+ // re-regex-parsed every SKILL.md each time. A changed skill re-parses just itself.
195
+ const skillMetaCache = new Map();
196
+ function discoverSkillsFromDir(dir, scope, seen) {
197
+ const skills = [];
198
+ for (const entry of readSortedDir(dir)) {
199
+ if (!entry || seen.has(entry))
200
+ continue;
201
+ const skillDir = path.join(dir, entry);
202
+ const skillFile = resolveSkillFile(dir, entry);
203
+ try {
204
+ if (!fs.statSync(skillDir).isDirectory())
205
+ continue;
206
+ }
207
+ catch {
208
+ continue;
209
+ }
210
+ if (!hasFile(skillFile))
211
+ continue;
212
+ let meta = { label: null, description: null };
213
+ try {
214
+ const mtimeMs = fs.statSync(skillFile).mtimeMs;
215
+ const cached = skillMetaCache.get(skillFile);
216
+ if (cached && cached.mtimeMs === mtimeMs) {
217
+ meta = cached.meta;
218
+ }
219
+ else {
220
+ meta = parseSkillMeta(fs.readFileSync(skillFile, 'utf-8'));
221
+ skillMetaCache.set(skillFile, { mtimeMs, meta });
222
+ }
223
+ }
224
+ catch { }
225
+ skills.push({
226
+ name: entry,
227
+ label: meta.label,
228
+ description: meta.description,
229
+ source: 'skills',
230
+ scope,
231
+ mcpRequires: meta.mcpRequires,
232
+ });
233
+ seen.add(entry);
234
+ }
235
+ return skills;
236
+ }
237
+ /**
238
+ * List all skills — project-scoped (workdir) first, then global (~/.pikiloop/skills/).
239
+ * Project skills with the same name shadow global ones.
240
+ */
241
+ export function listSkills(workdir) {
242
+ const seen = new Set();
243
+ const { canonicalRoot, legacyRoot } = resolveProjectSkillRoots(workdir);
244
+ // Project skills take precedence. Also scan the pre-rename `.pikiclaw/skills`
245
+ // dir so repos that committed project skills before the rename keep working.
246
+ const projectSkills = [
247
+ ...discoverSkillsFromDir(canonicalRoot, 'project', seen),
248
+ ...discoverSkillsFromDir(legacyRoot, 'project', seen),
249
+ ];
250
+ // Global skills fill in the rest
251
+ const globalSkills = discoverSkillsFromDir(GLOBAL_SKILLS_ROOT, 'global', seen);
252
+ return { skills: [...projectSkills, ...globalSkills], workdir };
253
+ }
254
+ /** Return the global skills root directory path. */
255
+ export function getGlobalSkillsRoot() {
256
+ return GLOBAL_SKILLS_ROOT;
257
+ }