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.
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/README.v2.md +287 -0
- package/README.zh-CN.md +352 -0
- package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
- package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
- package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
- package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
- package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
- package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
- package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
- package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
- package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
- package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
- package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
- package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
- package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
- package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
- package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
- package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
- package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
- package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
- package/dashboard/dist/assets/index-reSbuley.css +1 -0
- package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
- package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
- package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
- package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
- package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
- package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
- package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
- package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
- package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
- package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
- package/dashboard/dist/favicon.svg +28 -0
- package/dashboard/dist/index.html +17 -0
- package/dist/agent/acp-client.js +261 -0
- package/dist/agent/auto-update.js +432 -0
- package/dist/agent/await-resume.js +50 -0
- package/dist/agent/cli/auth.js +325 -0
- package/dist/agent/cli/catalog.js +40 -0
- package/dist/agent/cli/detector.js +136 -0
- package/dist/agent/cli/index.js +7 -0
- package/dist/agent/cli/registry.js +33 -0
- package/dist/agent/driver.js +39 -0
- package/dist/agent/drivers/claude-tui.js +2297 -0
- package/dist/agent/drivers/claude.js +2689 -0
- package/dist/agent/drivers/codex.js +2210 -0
- package/dist/agent/drivers/gemini.js +1059 -0
- package/dist/agent/drivers/hermes.js +795 -0
- package/dist/agent/goal.js +274 -0
- package/dist/agent/handover.js +130 -0
- package/dist/agent/images.js +355 -0
- package/dist/agent/index.js +50 -0
- package/dist/agent/mcp/bridge.js +791 -0
- package/dist/agent/mcp/extensions.js +637 -0
- package/dist/agent/mcp/oauth.js +353 -0
- package/dist/agent/mcp/registry.js +119 -0
- package/dist/agent/mcp/session-server.js +229 -0
- package/dist/agent/mcp/tools/ask-user.js +113 -0
- package/dist/agent/mcp/tools/await-resume.js +77 -0
- package/dist/agent/mcp/tools/goal.js +144 -0
- package/dist/agent/mcp/tools/types.js +12 -0
- package/dist/agent/mcp/tools/workspace.js +212 -0
- package/dist/agent/npm.js +31 -0
- package/dist/agent/session.js +1206 -0
- package/dist/agent/skill-installer.js +160 -0
- package/dist/agent/skills.js +257 -0
- package/dist/agent/stream.js +743 -0
- package/dist/agent/types.js +13 -0
- package/dist/agent/utils.js +687 -0
- package/dist/bot/bot.js +2499 -0
- package/dist/bot/command-ui.js +633 -0
- package/dist/bot/commands.js +513 -0
- package/dist/bot/headless-bot.js +36 -0
- package/dist/bot/host.js +192 -0
- package/dist/bot/human-loop.js +168 -0
- package/dist/bot/menu.js +48 -0
- package/dist/bot/orchestration.js +79 -0
- package/dist/bot/render-shared.js +309 -0
- package/dist/bot/session-hub.js +361 -0
- package/dist/bot/session-status.js +55 -0
- package/dist/bot/streaming.js +309 -0
- package/dist/browser-profile.js +579 -0
- package/dist/browser-supervisor.js +249 -0
- package/dist/catalog/cli-tools.js +421 -0
- package/dist/catalog/index.js +21 -0
- package/dist/catalog/local-models.js +94 -0
- package/dist/catalog/mcp-servers.js +315 -0
- package/dist/catalog/skill-repos.js +173 -0
- package/dist/channels/base.js +55 -0
- package/dist/channels/dingtalk/bot.js +549 -0
- package/dist/channels/dingtalk/channel.js +268 -0
- package/dist/channels/discord/bot.js +552 -0
- package/dist/channels/discord/channel.js +245 -0
- package/dist/channels/feishu/bot.js +1275 -0
- package/dist/channels/feishu/channel.js +911 -0
- package/dist/channels/feishu/markdown.js +91 -0
- package/dist/channels/feishu/render.js +619 -0
- package/dist/channels/health.js +109 -0
- package/dist/channels/slack/bot.js +554 -0
- package/dist/channels/slack/channel.js +283 -0
- package/dist/channels/states.js +6 -0
- package/dist/channels/telegram/bot.js +1310 -0
- package/dist/channels/telegram/channel.js +820 -0
- package/dist/channels/telegram/directory.js +111 -0
- package/dist/channels/telegram/live-preview.js +220 -0
- package/dist/channels/telegram/render.js +384 -0
- package/dist/channels/wecom/bot.js +558 -0
- package/dist/channels/wecom/channel.js +479 -0
- package/dist/channels/weixin/api.js +520 -0
- package/dist/channels/weixin/bot.js +1000 -0
- package/dist/channels/weixin/channel.js +222 -0
- package/dist/cli/autostart.js +262 -0
- package/dist/cli/channel-supervisor.js +313 -0
- package/dist/cli/channels.js +54 -0
- package/dist/cli/main.js +726 -0
- package/dist/cli/onboarding.js +227 -0
- package/dist/cli/run.js +308 -0
- package/dist/cli/setup-wizard.js +235 -0
- package/dist/core/config/runtime-config.js +201 -0
- package/dist/core/config/user-config.js +510 -0
- package/dist/core/config/validation.js +521 -0
- package/dist/core/constants.js +400 -0
- package/dist/core/git.js +145 -0
- package/dist/core/legacy-compat.js +60 -0
- package/dist/core/logging.js +101 -0
- package/dist/core/platform.js +59 -0
- package/dist/core/process-control.js +315 -0
- package/dist/core/secrets/index.js +42 -0
- package/dist/core/secrets/inline-seal.js +60 -0
- package/dist/core/secrets/ref.js +33 -0
- package/dist/core/secrets/resolver.js +65 -0
- package/dist/core/secrets/store.js +63 -0
- package/dist/core/utils.js +233 -0
- package/dist/core/version.js +15 -0
- package/dist/dashboard/platform.js +219 -0
- package/dist/dashboard/routes/agents.js +450 -0
- package/dist/dashboard/routes/cli.js +174 -0
- package/dist/dashboard/routes/config.js +523 -0
- package/dist/dashboard/routes/extensions.js +745 -0
- package/dist/dashboard/routes/local-models.js +290 -0
- package/dist/dashboard/routes/models.js +324 -0
- package/dist/dashboard/routes/sessions.js +838 -0
- package/dist/dashboard/runtime.js +410 -0
- package/dist/dashboard/server.js +237 -0
- package/dist/dashboard/session-control.js +347 -0
- package/dist/model/catalog.js +104 -0
- package/dist/model/index.js +20 -0
- package/dist/model/injector.js +272 -0
- package/dist/model/provider-models.js +112 -0
- package/dist/model/store.js +212 -0
- package/dist/model/types.js +13 -0
- package/dist/model/validation.js +203 -0
- 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
|
+
}
|