playbooks 0.1.6 → 0.1.7
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/dist/index.js +1096 -1043
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { program } from "commander";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "playbooks",
|
|
11
|
-
version: "0.1.
|
|
11
|
+
version: "0.1.7",
|
|
12
12
|
description: "Install agent skills, MCPs and docs into your coding agents from any git repository.",
|
|
13
13
|
type: "module",
|
|
14
14
|
bin: {
|
|
@@ -104,83 +104,296 @@ var package_default = {
|
|
|
104
104
|
packageManager: "npm@10.8.2"
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
-
// src/
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
// src/agents.ts
|
|
108
|
+
import { existsSync } from "fs";
|
|
109
|
+
import { homedir } from "os";
|
|
110
|
+
import { join } from "path";
|
|
111
|
+
var home = homedir();
|
|
112
|
+
var codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
113
|
+
var agents = {
|
|
114
|
+
amp: {
|
|
115
|
+
name: "amp",
|
|
116
|
+
displayName: "Amp",
|
|
117
|
+
skillsDir: ".agents/skills",
|
|
118
|
+
globalSkillsDir: join(home, ".config/agents/skills"),
|
|
119
|
+
detectInstalled: async () => {
|
|
120
|
+
return existsSync(join(home, ".config/amp"));
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
antigravity: {
|
|
124
|
+
name: "antigravity",
|
|
125
|
+
displayName: "Antigravity",
|
|
126
|
+
skillsDir: ".agent/skills",
|
|
127
|
+
globalSkillsDir: join(home, ".gemini/antigravity/global_skills"),
|
|
128
|
+
detectInstalled: async () => {
|
|
129
|
+
return existsSync(join(process.cwd(), ".agent")) || existsSync(join(home, ".gemini/antigravity"));
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"claude-code": {
|
|
133
|
+
name: "claude-code",
|
|
134
|
+
displayName: "Claude Code",
|
|
135
|
+
skillsDir: ".claude/skills",
|
|
136
|
+
globalSkillsDir: join(home, ".claude/skills"),
|
|
137
|
+
detectInstalled: async () => {
|
|
138
|
+
return existsSync(join(home, ".claude"));
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
clawdbot: {
|
|
142
|
+
name: "clawdbot",
|
|
143
|
+
displayName: "Clawdbot",
|
|
144
|
+
skillsDir: "skills",
|
|
145
|
+
globalSkillsDir: join(home, ".clawdbot/skills"),
|
|
146
|
+
detectInstalled: async () => {
|
|
147
|
+
return existsSync(join(home, ".clawdbot"));
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
cline: {
|
|
151
|
+
name: "cline",
|
|
152
|
+
displayName: "Cline",
|
|
153
|
+
skillsDir: ".cline/skills",
|
|
154
|
+
globalSkillsDir: join(home, ".cline/skills"),
|
|
155
|
+
detectInstalled: async () => {
|
|
156
|
+
return existsSync(join(home, ".cline"));
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
codex: {
|
|
160
|
+
name: "codex",
|
|
161
|
+
displayName: "Codex",
|
|
162
|
+
skillsDir: ".codex/skills",
|
|
163
|
+
globalSkillsDir: join(codexHome, "skills"),
|
|
164
|
+
detectInstalled: async () => {
|
|
165
|
+
return existsSync(codexHome) || existsSync("/etc/codex");
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"command-code": {
|
|
169
|
+
name: "command-code",
|
|
170
|
+
displayName: "Command Code",
|
|
171
|
+
skillsDir: ".commandcode/skills",
|
|
172
|
+
globalSkillsDir: join(home, ".commandcode/skills"),
|
|
173
|
+
detectInstalled: async () => {
|
|
174
|
+
return existsSync(join(home, ".commandcode"));
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
continue: {
|
|
178
|
+
name: "continue",
|
|
179
|
+
displayName: "Continue",
|
|
180
|
+
skillsDir: ".continue/skills",
|
|
181
|
+
globalSkillsDir: join(home, ".continue/skills"),
|
|
182
|
+
detectInstalled: async () => {
|
|
183
|
+
return existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue"));
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
crush: {
|
|
187
|
+
name: "crush",
|
|
188
|
+
displayName: "Crush",
|
|
189
|
+
skillsDir: ".crush/skills",
|
|
190
|
+
globalSkillsDir: join(home, ".config/crush/skills"),
|
|
191
|
+
detectInstalled: async () => {
|
|
192
|
+
return existsSync(join(home, ".config/crush"));
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
cursor: {
|
|
196
|
+
name: "cursor",
|
|
197
|
+
displayName: "Cursor",
|
|
198
|
+
skillsDir: ".cursor/skills",
|
|
199
|
+
globalSkillsDir: join(home, ".cursor/skills"),
|
|
200
|
+
detectInstalled: async () => {
|
|
201
|
+
return existsSync(join(home, ".cursor"));
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
droid: {
|
|
205
|
+
name: "droid",
|
|
206
|
+
displayName: "Droid",
|
|
207
|
+
skillsDir: ".factory/skills",
|
|
208
|
+
globalSkillsDir: join(home, ".factory/skills"),
|
|
209
|
+
detectInstalled: async () => {
|
|
210
|
+
return existsSync(join(home, ".factory"));
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"gemini-cli": {
|
|
214
|
+
name: "gemini-cli",
|
|
215
|
+
displayName: "Gemini CLI",
|
|
216
|
+
skillsDir: ".gemini/skills",
|
|
217
|
+
globalSkillsDir: join(home, ".gemini/skills"),
|
|
218
|
+
detectInstalled: async () => {
|
|
219
|
+
return existsSync(join(home, ".gemini"));
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
"github-copilot": {
|
|
223
|
+
name: "github-copilot",
|
|
224
|
+
displayName: "GitHub Copilot",
|
|
225
|
+
skillsDir: ".github/skills",
|
|
226
|
+
globalSkillsDir: join(home, ".copilot/skills"),
|
|
227
|
+
detectInstalled: async () => {
|
|
228
|
+
return existsSync(join(process.cwd(), ".github")) || existsSync(join(home, ".copilot"));
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
goose: {
|
|
232
|
+
name: "goose",
|
|
233
|
+
displayName: "Goose",
|
|
234
|
+
skillsDir: ".goose/skills",
|
|
235
|
+
globalSkillsDir: join(home, ".config/goose/skills"),
|
|
236
|
+
detectInstalled: async () => {
|
|
237
|
+
return existsSync(join(home, ".config/goose"));
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
kilo: {
|
|
241
|
+
name: "kilo",
|
|
242
|
+
displayName: "Kilo Code",
|
|
243
|
+
skillsDir: ".kilocode/skills",
|
|
244
|
+
globalSkillsDir: join(home, ".kilocode/skills"),
|
|
245
|
+
detectInstalled: async () => {
|
|
246
|
+
return existsSync(join(home, ".kilocode"));
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
"kiro-cli": {
|
|
250
|
+
name: "kiro-cli",
|
|
251
|
+
displayName: "Kiro CLI",
|
|
252
|
+
skillsDir: ".kiro/skills",
|
|
253
|
+
globalSkillsDir: join(home, ".kiro/skills"),
|
|
254
|
+
detectInstalled: async () => {
|
|
255
|
+
return existsSync(join(home, ".kiro"));
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
mcpjam: {
|
|
259
|
+
name: "mcpjam",
|
|
260
|
+
displayName: "MCPJam",
|
|
261
|
+
skillsDir: ".mcpjam/skills",
|
|
262
|
+
globalSkillsDir: join(home, ".mcpjam/skills"),
|
|
263
|
+
detectInstalled: async () => {
|
|
264
|
+
return existsSync(join(home, ".mcpjam"));
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
opencode: {
|
|
268
|
+
name: "opencode",
|
|
269
|
+
displayName: "OpenCode",
|
|
270
|
+
skillsDir: ".opencode/skills",
|
|
271
|
+
globalSkillsDir: join(home, ".config/opencode/skills"),
|
|
272
|
+
detectInstalled: async () => {
|
|
273
|
+
return existsSync(join(home, ".config/opencode")) || existsSync(join(home, ".claude/skills"));
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
openhands: {
|
|
277
|
+
name: "openhands",
|
|
278
|
+
displayName: "OpenHands",
|
|
279
|
+
skillsDir: ".openhands/skills",
|
|
280
|
+
globalSkillsDir: join(home, ".openhands/skills"),
|
|
281
|
+
detectInstalled: async () => {
|
|
282
|
+
return existsSync(join(home, ".openhands"));
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
pi: {
|
|
286
|
+
name: "pi",
|
|
287
|
+
displayName: "Pi",
|
|
288
|
+
skillsDir: ".pi/skills",
|
|
289
|
+
globalSkillsDir: join(home, ".pi/agent/skills"),
|
|
290
|
+
detectInstalled: async () => {
|
|
291
|
+
return existsSync(join(home, ".pi/agent"));
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
qoder: {
|
|
295
|
+
name: "qoder",
|
|
296
|
+
displayName: "Qoder",
|
|
297
|
+
skillsDir: ".qoder/skills",
|
|
298
|
+
globalSkillsDir: join(home, ".qoder/skills"),
|
|
299
|
+
detectInstalled: async () => {
|
|
300
|
+
return existsSync(join(home, ".qoder"));
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
"qwen-code": {
|
|
304
|
+
name: "qwen-code",
|
|
305
|
+
displayName: "Qwen Code",
|
|
306
|
+
skillsDir: ".qwen/skills",
|
|
307
|
+
globalSkillsDir: join(home, ".qwen/skills"),
|
|
308
|
+
detectInstalled: async () => {
|
|
309
|
+
return existsSync(join(home, ".qwen"));
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
roo: {
|
|
313
|
+
name: "roo",
|
|
314
|
+
displayName: "Roo Code",
|
|
315
|
+
skillsDir: ".roo/skills",
|
|
316
|
+
globalSkillsDir: join(home, ".roo/skills"),
|
|
317
|
+
detectInstalled: async () => {
|
|
318
|
+
return existsSync(join(home, ".roo"));
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
trae: {
|
|
322
|
+
name: "trae",
|
|
323
|
+
displayName: "Trae",
|
|
324
|
+
skillsDir: ".trae/skills",
|
|
325
|
+
globalSkillsDir: join(home, ".trae/skills"),
|
|
326
|
+
detectInstalled: async () => {
|
|
327
|
+
return existsSync(join(home, ".trae"));
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
windsurf: {
|
|
331
|
+
name: "windsurf",
|
|
332
|
+
displayName: "Windsurf",
|
|
333
|
+
skillsDir: ".windsurf/skills",
|
|
334
|
+
globalSkillsDir: join(home, ".codeium/windsurf/skills"),
|
|
335
|
+
detectInstalled: async () => {
|
|
336
|
+
return existsSync(join(home, ".codeium/windsurf"));
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
zencoder: {
|
|
340
|
+
name: "zencoder",
|
|
341
|
+
displayName: "Zencoder",
|
|
342
|
+
skillsDir: ".zencoder/skills",
|
|
343
|
+
globalSkillsDir: join(home, ".zencoder/skills"),
|
|
344
|
+
detectInstalled: async () => {
|
|
345
|
+
return existsSync(join(home, ".zencoder"));
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
neovate: {
|
|
349
|
+
name: "neovate",
|
|
350
|
+
displayName: "Neovate",
|
|
351
|
+
skillsDir: ".neovate/skills",
|
|
352
|
+
globalSkillsDir: join(home, ".neovate/skills"),
|
|
353
|
+
detectInstalled: async () => {
|
|
354
|
+
return existsSync(join(home, ".neovate"));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
async function detectInstalledAgents() {
|
|
359
|
+
const installed = [];
|
|
360
|
+
for (const [type, config] of Object.entries(agents)) {
|
|
361
|
+
if (await config.detectInstalled()) {
|
|
362
|
+
installed.push(type);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return installed;
|
|
116
366
|
}
|
|
117
367
|
|
|
118
|
-
// src/
|
|
119
|
-
import {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
368
|
+
// src/flows/find-skill.ts
|
|
369
|
+
import { existsSync as existsSync3 } from "fs";
|
|
370
|
+
import { mkdtemp as mkdtemp2 } from "fs/promises";
|
|
371
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
372
|
+
import { join as join4 } from "path";
|
|
373
|
+
|
|
374
|
+
// src/git.ts
|
|
375
|
+
import { mkdtemp, rm as rm2 } from "fs/promises";
|
|
376
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
377
|
+
import { join as join2 } from "path";
|
|
378
|
+
import simpleGit from "simple-git";
|
|
379
|
+
|
|
380
|
+
// src/temp-registry.ts
|
|
381
|
+
import { rmSync } from "fs";
|
|
382
|
+
import { rm } from "fs/promises";
|
|
383
|
+
import { tmpdir } from "os";
|
|
384
|
+
import { normalize, resolve, sep } from "path";
|
|
385
|
+
var tempDirs = /* @__PURE__ */ new Set();
|
|
386
|
+
var handlersInstalled = false;
|
|
387
|
+
function isTempPathSafe(dir) {
|
|
388
|
+
const normalizedDir = normalize(resolve(dir));
|
|
389
|
+
const normalizedTmpDir = normalize(resolve(tmpdir()));
|
|
390
|
+
return normalizedDir.startsWith(normalizedTmpDir + sep) || normalizedDir === normalizedTmpDir;
|
|
125
391
|
}
|
|
126
|
-
function
|
|
127
|
-
|
|
392
|
+
function registerTempDir(dir) {
|
|
393
|
+
tempDirs.add(dir);
|
|
128
394
|
}
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
function buildHeaders(body) {
|
|
133
|
-
const headers = {
|
|
134
|
-
"Content-Type": "application/json",
|
|
135
|
-
"User-Agent": "playbooks-cli"
|
|
136
|
-
};
|
|
137
|
-
if (cliVersion) {
|
|
138
|
-
headers["X-Playbooks-Version"] = cliVersion;
|
|
139
|
-
}
|
|
140
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
141
|
-
headers["X-Playbooks-Timestamp"] = timestamp;
|
|
142
|
-
const secret = process.env.PLAYBOOKS_TELEMETRY_SECRET;
|
|
143
|
-
if (secret) {
|
|
144
|
-
const signature = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
|
|
145
|
-
headers["X-Playbooks-Signature"] = signature;
|
|
146
|
-
}
|
|
147
|
-
return headers;
|
|
148
|
-
}
|
|
149
|
-
function trackInstall(payload) {
|
|
150
|
-
if (!isEnabled()) return;
|
|
151
|
-
try {
|
|
152
|
-
const body = JSON.stringify({
|
|
153
|
-
...payload,
|
|
154
|
-
version: cliVersion ?? void 0,
|
|
155
|
-
ci: isCI() || void 0
|
|
156
|
-
});
|
|
157
|
-
fetch(TELEMETRY_URL, {
|
|
158
|
-
method: "POST",
|
|
159
|
-
headers: buildHeaders(body),
|
|
160
|
-
body
|
|
161
|
-
}).catch(() => {
|
|
162
|
-
});
|
|
163
|
-
} catch {
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// src/temp-registry.ts
|
|
168
|
-
import { rmSync } from "fs";
|
|
169
|
-
import { rm } from "fs/promises";
|
|
170
|
-
import { tmpdir } from "os";
|
|
171
|
-
import { normalize, resolve, sep } from "path";
|
|
172
|
-
var tempDirs = /* @__PURE__ */ new Set();
|
|
173
|
-
var handlersInstalled = false;
|
|
174
|
-
function isTempPathSafe(dir) {
|
|
175
|
-
const normalizedDir = normalize(resolve(dir));
|
|
176
|
-
const normalizedTmpDir = normalize(resolve(tmpdir()));
|
|
177
|
-
return normalizedDir.startsWith(normalizedTmpDir + sep) || normalizedDir === normalizedTmpDir;
|
|
178
|
-
}
|
|
179
|
-
function registerTempDir(dir) {
|
|
180
|
-
tempDirs.add(dir);
|
|
181
|
-
}
|
|
182
|
-
function unregisterTempDir(dir) {
|
|
183
|
-
tempDirs.delete(dir);
|
|
395
|
+
function unregisterTempDir(dir) {
|
|
396
|
+
tempDirs.delete(dir);
|
|
184
397
|
}
|
|
185
398
|
function cleanupAllTempDirsSync() {
|
|
186
399
|
const dirs = Array.from(tempDirs);
|
|
@@ -211,19 +424,9 @@ function setupTempDirCleanup() {
|
|
|
211
424
|
});
|
|
212
425
|
}
|
|
213
426
|
|
|
214
|
-
// src/tui/App.tsx
|
|
215
|
-
import { render } from "ink";
|
|
216
|
-
|
|
217
|
-
// src/tui/context/navigation.tsx
|
|
218
|
-
import React, { createContext, useCallback, useContext, useMemo, useState } from "react";
|
|
219
|
-
|
|
220
427
|
// src/git.ts
|
|
221
|
-
import { mkdtemp, rm as rm2 } from "fs/promises";
|
|
222
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
223
|
-
import { join } from "path";
|
|
224
|
-
import simpleGit from "simple-git";
|
|
225
428
|
async function cloneRepo(url, ref) {
|
|
226
|
-
const tempDir = await mkdtemp(
|
|
429
|
+
const tempDir = await mkdtemp(join2(tmpdir2(), "add-skill-"));
|
|
227
430
|
registerTempDir(tempDir);
|
|
228
431
|
const git = simpleGit();
|
|
229
432
|
const cloneOptions = ref ? ["--depth", "1", "--branch", ref] : ["--depth", "1"];
|
|
@@ -246,517 +449,713 @@ async function cleanupTempDir(dir) {
|
|
|
246
449
|
}
|
|
247
450
|
}
|
|
248
451
|
|
|
249
|
-
// src/
|
|
250
|
-
|
|
251
|
-
var
|
|
252
|
-
function
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
initialScreen
|
|
261
|
-
}) {
|
|
262
|
-
const [screen, setScreen] = useState(initialScreen);
|
|
263
|
-
const [stack, setStack] = useState([initialScreen]);
|
|
264
|
-
const [flashes, setFlashes] = useState([]);
|
|
265
|
-
const [invocation, setInvocation] = useState(initialInvocation);
|
|
266
|
-
const [addSkill, setAddSkill] = useState({});
|
|
267
|
-
const [findSkill, setFindSkill] = useState({ status: "idle" });
|
|
268
|
-
const [isTextInputActive, setTextInputActive] = useState(false);
|
|
269
|
-
const [textInputEscMode, setTextInputEscMode] = useState("back");
|
|
270
|
-
const [navAction, setNavAction] = useState("reset");
|
|
271
|
-
const [lastSource, setLastSource] = useState(initialInvocation.source ?? null);
|
|
272
|
-
const backHandlerRef = React.useRef(null);
|
|
273
|
-
const flashTimersRef = React.useRef(/* @__PURE__ */ new Map());
|
|
274
|
-
const navigateTo = useCallback((s) => {
|
|
275
|
-
setNavAction("push");
|
|
276
|
-
setStack((prev) => [...prev, s]);
|
|
277
|
-
setScreen(s);
|
|
278
|
-
}, []);
|
|
279
|
-
const resetTo = useCallback((s) => {
|
|
280
|
-
setNavAction("reset");
|
|
281
|
-
setStack([s]);
|
|
282
|
-
setScreen(s);
|
|
283
|
-
}, []);
|
|
284
|
-
const goBack = useCallback(() => {
|
|
285
|
-
setNavAction("pop");
|
|
286
|
-
let target;
|
|
287
|
-
if (stack.length <= 1) {
|
|
288
|
-
target = stack[0] ?? "main";
|
|
289
|
-
setStack([target]);
|
|
290
|
-
} else {
|
|
291
|
-
const next = stack.slice(0, -1);
|
|
292
|
-
target = next[next.length - 1] ?? "main";
|
|
293
|
-
setStack(next);
|
|
294
|
-
}
|
|
295
|
-
setScreen(target);
|
|
296
|
-
}, [stack]);
|
|
297
|
-
const setFlash = useCallback((msg) => {
|
|
298
|
-
if (msg === null) {
|
|
299
|
-
for (const timer2 of flashTimersRef.current.values()) {
|
|
300
|
-
clearTimeout(timer2);
|
|
301
|
-
}
|
|
302
|
-
flashTimersRef.current.clear();
|
|
303
|
-
setFlashes([]);
|
|
304
|
-
return;
|
|
452
|
+
// src/playbooks-api.ts
|
|
453
|
+
var API_BASE = process.env.PLAYBOOKS_API_URL?.trim() || "https://playbooks.com/api";
|
|
454
|
+
var USER_AGENT = "playbooks-cli";
|
|
455
|
+
async function searchSkills(query, mode, limit = 10) {
|
|
456
|
+
const url = new URL(`${API_BASE}/skills`);
|
|
457
|
+
url.searchParams.set("search", query);
|
|
458
|
+
url.searchParams.set("limit", String(limit));
|
|
459
|
+
url.searchParams.set("mode", mode);
|
|
460
|
+
const response = await fetch(url.toString(), {
|
|
461
|
+
headers: {
|
|
462
|
+
"User-Agent": USER_AGENT
|
|
305
463
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
flashTimersRef.current.clear();
|
|
320
|
-
};
|
|
321
|
-
}, []);
|
|
322
|
-
React.useEffect(() => {
|
|
323
|
-
if (!addSkill.tempDir) return;
|
|
324
|
-
if (screen.startsWith("add-") || screen.startsWith("find-")) return;
|
|
325
|
-
cleanupTempDir(addSkill.tempDir).catch(() => {
|
|
326
|
-
});
|
|
327
|
-
}, [addSkill.tempDir, screen]);
|
|
328
|
-
const updateAddSkill = useCallback((patch) => {
|
|
329
|
-
setAddSkill((prev) => ({ ...prev, ...patch }));
|
|
330
|
-
}, []);
|
|
331
|
-
const resetAddSkill = useCallback(() => {
|
|
332
|
-
setAddSkill({});
|
|
333
|
-
}, []);
|
|
334
|
-
const updateFindSkill = useCallback((patch) => {
|
|
335
|
-
setFindSkill((prev) => ({ ...prev, ...patch }));
|
|
336
|
-
}, []);
|
|
337
|
-
const resetFindSkill = useCallback(() => {
|
|
338
|
-
setFindSkill({ status: "idle" });
|
|
339
|
-
}, []);
|
|
340
|
-
const value = useMemo(
|
|
341
|
-
() => ({
|
|
342
|
-
screen,
|
|
343
|
-
setScreen,
|
|
344
|
-
navigateTo,
|
|
345
|
-
resetTo,
|
|
346
|
-
goBack,
|
|
347
|
-
stack,
|
|
348
|
-
flashes,
|
|
349
|
-
setFlash,
|
|
350
|
-
navAction,
|
|
351
|
-
lastSource,
|
|
352
|
-
setLastSource,
|
|
353
|
-
getBackHandler: () => backHandlerRef.current,
|
|
354
|
-
setBackHandler: (fn) => {
|
|
355
|
-
backHandlerRef.current = fn;
|
|
356
|
-
},
|
|
357
|
-
invocation,
|
|
358
|
-
setInvocation,
|
|
359
|
-
addSkill,
|
|
360
|
-
setAddSkill,
|
|
361
|
-
updateAddSkill,
|
|
362
|
-
resetAddSkill,
|
|
363
|
-
findSkill,
|
|
364
|
-
setFindSkill,
|
|
365
|
-
updateFindSkill,
|
|
366
|
-
resetFindSkill,
|
|
367
|
-
isTextInputActive,
|
|
368
|
-
setTextInputActive,
|
|
369
|
-
textInputEscMode,
|
|
370
|
-
setTextInputEscMode
|
|
371
|
-
}),
|
|
372
|
-
[
|
|
373
|
-
screen,
|
|
374
|
-
stack,
|
|
375
|
-
flashes,
|
|
376
|
-
navAction,
|
|
377
|
-
navigateTo,
|
|
378
|
-
resetTo,
|
|
379
|
-
goBack,
|
|
380
|
-
setFlash,
|
|
381
|
-
lastSource,
|
|
382
|
-
invocation,
|
|
383
|
-
addSkill,
|
|
384
|
-
updateAddSkill,
|
|
385
|
-
resetAddSkill,
|
|
386
|
-
findSkill,
|
|
387
|
-
updateFindSkill,
|
|
388
|
-
resetFindSkill,
|
|
389
|
-
isTextInputActive,
|
|
390
|
-
textInputEscMode
|
|
391
|
-
]
|
|
392
|
-
);
|
|
393
|
-
return /* @__PURE__ */ jsx(NavigationContext.Provider, { value, children });
|
|
464
|
+
});
|
|
465
|
+
let payload = null;
|
|
466
|
+
try {
|
|
467
|
+
payload = await response.json();
|
|
468
|
+
} catch {
|
|
469
|
+
payload = null;
|
|
470
|
+
}
|
|
471
|
+
if (!response.ok || !payload?.success) {
|
|
472
|
+
const message = payload?.error || `Search failed (${response.status})`;
|
|
473
|
+
throw new Error(message);
|
|
474
|
+
}
|
|
475
|
+
return Array.isArray(payload.data) ? payload.data : [];
|
|
394
476
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
477
|
+
async function requestUrlMarkdown(url) {
|
|
478
|
+
const endpoint = new URL(`${API_BASE}/url`);
|
|
479
|
+
const response = await fetch(endpoint.toString(), {
|
|
480
|
+
method: "POST",
|
|
481
|
+
headers: {
|
|
482
|
+
"User-Agent": USER_AGENT,
|
|
483
|
+
"Content-Type": "application/json"
|
|
484
|
+
},
|
|
485
|
+
body: JSON.stringify({ url })
|
|
486
|
+
});
|
|
487
|
+
let payload = null;
|
|
488
|
+
try {
|
|
489
|
+
payload = await response.json();
|
|
490
|
+
} catch {
|
|
491
|
+
payload = null;
|
|
492
|
+
}
|
|
493
|
+
if (!response.ok && response.status !== 202) {
|
|
494
|
+
const message = payload?.error || `Request failed (${response.status})`;
|
|
495
|
+
throw new Error(message);
|
|
496
|
+
}
|
|
497
|
+
return payload ?? { success: false, error: `Request failed (${response.status})` };
|
|
406
498
|
}
|
|
407
|
-
function
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
function BrandHeader() {
|
|
417
|
-
const [title, setTitle] = React2.useState(
|
|
418
|
-
() => TARGET_TEXT.split("").map((char) => char === " " ? " " : randomChar(TEXT_CHARS)).join("")
|
|
419
|
-
);
|
|
420
|
-
const hasAnimated = React2.useRef(false);
|
|
421
|
-
const textSettles = React2.useRef([]);
|
|
422
|
-
React2.useEffect(() => {
|
|
423
|
-
if (hasAnimated.current) return;
|
|
424
|
-
hasAnimated.current = true;
|
|
425
|
-
textSettles.current = TARGET_TEXT.split("").map(
|
|
426
|
-
(char) => char === " " ? 0 : 0.2 + Math.random() * 0.6
|
|
427
|
-
);
|
|
428
|
-
const steps = 18;
|
|
429
|
-
const interval = 50;
|
|
430
|
-
let step = 0;
|
|
431
|
-
const tick = () => {
|
|
432
|
-
const progress = Math.min(1, step / steps);
|
|
433
|
-
setTitle(buildScrambleText(TARGET_TEXT, textSettles.current, progress));
|
|
434
|
-
step += 1;
|
|
435
|
-
if (step > steps) {
|
|
436
|
-
setTitle(TARGET_TEXT);
|
|
437
|
-
return false;
|
|
438
|
-
}
|
|
439
|
-
return true;
|
|
440
|
-
};
|
|
441
|
-
tick();
|
|
442
|
-
const timer = setInterval(() => {
|
|
443
|
-
if (!tick()) {
|
|
444
|
-
clearInterval(timer);
|
|
499
|
+
async function pollUrlMarkdown(jobId, timeoutMs = 6e4, pollIntervalMs = 1e3) {
|
|
500
|
+
const endpoint = new URL(`${API_BASE}/url`);
|
|
501
|
+
endpoint.searchParams.set("jobId", jobId);
|
|
502
|
+
const deadline = Date.now() + timeoutMs;
|
|
503
|
+
while (Date.now() < deadline) {
|
|
504
|
+
const response = await fetch(endpoint.toString(), {
|
|
505
|
+
headers: {
|
|
506
|
+
"User-Agent": USER_AGENT
|
|
445
507
|
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
/* @__PURE__ */ jsx2(Text, { bold: true, children: title }),
|
|
453
|
-
/* @__PURE__ */ jsx2(Text, { dimColor: true, children: TAGLINE })
|
|
454
|
-
] });
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// src/tui/ui/FlashBar.tsx
|
|
458
|
-
import { Box as Box2, Text as Text2 } from "ink";
|
|
459
|
-
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
460
|
-
function classify(msg) {
|
|
461
|
-
const m = msg.toLowerCase();
|
|
462
|
-
if (/(error|failed|unable|denied|invalid|cannot|not found)/i.test(msg)) return "error";
|
|
463
|
-
if (/(deleted|linked|unlinked|updated|created|saved|enabled|disabled|added|removed|✓)/i.test(msg))
|
|
464
|
-
return "success";
|
|
465
|
-
if (/(warn|deprecated|missing|retry|timeout)/i.test(msg)) return "warning";
|
|
466
|
-
return "info";
|
|
467
|
-
}
|
|
468
|
-
function FlashBar({ align = "left" }) {
|
|
469
|
-
const { flashes } = useNavigation();
|
|
470
|
-
if (flashes.length === 0) return /* @__PURE__ */ jsx3(Box2, { height: 1 });
|
|
471
|
-
return /* @__PURE__ */ jsx3(
|
|
472
|
-
Box2,
|
|
473
|
-
{
|
|
474
|
-
flexDirection: "column",
|
|
475
|
-
paddingX: 1,
|
|
476
|
-
paddingY: 0,
|
|
477
|
-
gap: 0,
|
|
478
|
-
justifyContent: align === "center" ? "center" : "flex-start",
|
|
479
|
-
children: flashes.map((flash) => {
|
|
480
|
-
const kind = classify(flash.text);
|
|
481
|
-
const color = kind === "success" ? "green" : kind === "error" ? "red" : kind === "warning" ? "yellow" : "cyan";
|
|
482
|
-
const label = kind === "success" ? "\u2713" : kind === "error" ? "x" : kind === "warning" ? "!" : "i";
|
|
483
|
-
return /* @__PURE__ */ jsxs2(Text2, { color, children: [
|
|
484
|
-
"[",
|
|
485
|
-
label,
|
|
486
|
-
"] ",
|
|
487
|
-
flash.text
|
|
488
|
-
] }, flash.id);
|
|
489
|
-
})
|
|
490
|
-
}
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// src/tui/screens/AddConfirm.tsx
|
|
495
|
-
import { join as join7 } from "path";
|
|
496
|
-
import { Box as Box8, Text as Text8 } from "ink";
|
|
497
|
-
import React3 from "react";
|
|
498
|
-
|
|
499
|
-
// src/agents.ts
|
|
500
|
-
import { existsSync } from "fs";
|
|
501
|
-
import { homedir } from "os";
|
|
502
|
-
import { join as join2 } from "path";
|
|
503
|
-
var home = homedir();
|
|
504
|
-
var codexHome = process.env.CODEX_HOME?.trim() || join2(home, ".codex");
|
|
505
|
-
var agents = {
|
|
506
|
-
amp: {
|
|
507
|
-
name: "amp",
|
|
508
|
-
displayName: "Amp",
|
|
509
|
-
skillsDir: ".agents/skills",
|
|
510
|
-
globalSkillsDir: join2(home, ".config/agents/skills"),
|
|
511
|
-
detectInstalled: async () => {
|
|
512
|
-
return existsSync(join2(home, ".config/amp"));
|
|
513
|
-
}
|
|
514
|
-
},
|
|
515
|
-
antigravity: {
|
|
516
|
-
name: "antigravity",
|
|
517
|
-
displayName: "Antigravity",
|
|
518
|
-
skillsDir: ".agent/skills",
|
|
519
|
-
globalSkillsDir: join2(home, ".gemini/antigravity/global_skills"),
|
|
520
|
-
detectInstalled: async () => {
|
|
521
|
-
return existsSync(join2(process.cwd(), ".agent")) || existsSync(join2(home, ".gemini/antigravity"));
|
|
508
|
+
});
|
|
509
|
+
let payload = null;
|
|
510
|
+
try {
|
|
511
|
+
payload = await response.json();
|
|
512
|
+
} catch {
|
|
513
|
+
payload = null;
|
|
522
514
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
name: "claude-code",
|
|
526
|
-
displayName: "Claude Code",
|
|
527
|
-
skillsDir: ".claude/skills",
|
|
528
|
-
globalSkillsDir: join2(home, ".claude/skills"),
|
|
529
|
-
detectInstalled: async () => {
|
|
530
|
-
return existsSync(join2(home, ".claude"));
|
|
515
|
+
if (payload?.success && payload.data) {
|
|
516
|
+
return payload.data;
|
|
531
517
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
displayName: "Clawdbot",
|
|
536
|
-
skillsDir: "skills",
|
|
537
|
-
globalSkillsDir: join2(home, ".clawdbot/skills"),
|
|
538
|
-
detectInstalled: async () => {
|
|
539
|
-
return existsSync(join2(home, ".clawdbot"));
|
|
518
|
+
if (payload?.success && payload.pending) {
|
|
519
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollIntervalMs));
|
|
520
|
+
continue;
|
|
540
521
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
522
|
+
const message = payload?.error || `Request failed (${response.status})`;
|
|
523
|
+
throw new Error(message);
|
|
524
|
+
}
|
|
525
|
+
throw new Error("Timed out waiting for markdown");
|
|
526
|
+
}
|
|
527
|
+
async function fetchUrlMarkdown(url) {
|
|
528
|
+
const response = await requestUrlMarkdown(url);
|
|
529
|
+
if (response.success && response.data) {
|
|
530
|
+
return response.data;
|
|
531
|
+
}
|
|
532
|
+
if (response.jobId) {
|
|
533
|
+
return await pollUrlMarkdown(response.jobId);
|
|
534
|
+
}
|
|
535
|
+
const message = response.error || "Failed to fetch markdown";
|
|
536
|
+
throw new Error(message);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/skills.ts
|
|
540
|
+
import { existsSync as existsSync2 } from "fs";
|
|
541
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
542
|
+
import { basename, dirname, join as join3 } from "path";
|
|
543
|
+
import matter from "gray-matter";
|
|
544
|
+
var SKIP_DIRS = ["node_modules", ".git", ".github", "dist", "build", "__pycache__"];
|
|
545
|
+
var DENIED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
546
|
+
".git",
|
|
547
|
+
"node_modules",
|
|
548
|
+
".github",
|
|
549
|
+
"playbooks",
|
|
550
|
+
"context",
|
|
551
|
+
"prompts",
|
|
552
|
+
"backups",
|
|
553
|
+
"backup",
|
|
554
|
+
"dist",
|
|
555
|
+
"deprecated"
|
|
556
|
+
]);
|
|
557
|
+
var normalizePath = (value) => value.replace(/^\/+/, "");
|
|
558
|
+
var normalizeRoot = (value) => normalizePath(value).replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
559
|
+
var isDeniedPath = (path) => {
|
|
560
|
+
const cleaned = normalizePath(path);
|
|
561
|
+
const segments = cleaned.split("/").map((segment) => segment.toLowerCase());
|
|
562
|
+
for (const segment of segments) {
|
|
563
|
+
if (!segment) continue;
|
|
564
|
+
if (segment === ".claude-plugin") {
|
|
565
|
+
continue;
|
|
549
566
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
name: "codex",
|
|
553
|
-
displayName: "Codex",
|
|
554
|
-
skillsDir: ".codex/skills",
|
|
555
|
-
globalSkillsDir: join2(codexHome, "skills"),
|
|
556
|
-
detectInstalled: async () => {
|
|
557
|
-
return existsSync(codexHome) || existsSync("/etc/codex");
|
|
567
|
+
if (DENIED_SEGMENTS.has(segment)) {
|
|
568
|
+
return true;
|
|
558
569
|
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
570
|
+
}
|
|
571
|
+
return false;
|
|
572
|
+
};
|
|
573
|
+
async function hasSkillMd(dir) {
|
|
574
|
+
try {
|
|
575
|
+
if (isDeniedPath(dir)) return false;
|
|
576
|
+
const skillPath = join3(dir, "SKILL.md");
|
|
577
|
+
const stats = await stat(skillPath);
|
|
578
|
+
return stats.isFile();
|
|
579
|
+
} catch {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async function parseSkillMd(skillMdPath) {
|
|
584
|
+
try {
|
|
585
|
+
if (isDeniedPath(skillMdPath)) return null;
|
|
586
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
587
|
+
const { data } = matter(content);
|
|
588
|
+
if (!data.name || !data.description) {
|
|
589
|
+
return null;
|
|
567
590
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
591
|
+
return {
|
|
592
|
+
name: data.name,
|
|
593
|
+
description: data.description,
|
|
594
|
+
path: dirname(skillMdPath),
|
|
595
|
+
rawContent: content,
|
|
596
|
+
metadata: data.metadata
|
|
597
|
+
};
|
|
598
|
+
} catch {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
|
|
603
|
+
const skillDirs = [];
|
|
604
|
+
if (depth > maxDepth) return skillDirs;
|
|
605
|
+
if (isDeniedPath(dir)) return skillDirs;
|
|
606
|
+
try {
|
|
607
|
+
if (await hasSkillMd(dir)) {
|
|
608
|
+
skillDirs.push(dir);
|
|
576
609
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
detectInstalled: async () => {
|
|
584
|
-
return existsSync(join2(home, ".config/crush"));
|
|
610
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
611
|
+
for (const entry of entries) {
|
|
612
|
+
if (entry.isDirectory() && !SKIP_DIRS.includes(entry.name)) {
|
|
613
|
+
const subDirs = await findSkillDirs(join3(dir, entry.name), depth + 1, maxDepth);
|
|
614
|
+
skillDirs.push(...subDirs);
|
|
615
|
+
}
|
|
585
616
|
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
skillsDir: ".goose/skills",
|
|
627
|
-
globalSkillsDir: join2(home, ".config/goose/skills"),
|
|
628
|
-
detectInstalled: async () => {
|
|
629
|
-
return existsSync(join2(home, ".config/goose"));
|
|
630
|
-
}
|
|
631
|
-
},
|
|
632
|
-
kilo: {
|
|
633
|
-
name: "kilo",
|
|
634
|
-
displayName: "Kilo Code",
|
|
635
|
-
skillsDir: ".kilocode/skills",
|
|
636
|
-
globalSkillsDir: join2(home, ".kilocode/skills"),
|
|
637
|
-
detectInstalled: async () => {
|
|
638
|
-
return existsSync(join2(home, ".kilocode"));
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
return skillDirs;
|
|
620
|
+
}
|
|
621
|
+
async function readMarketplacePluginRoots(basePath) {
|
|
622
|
+
const filePath = join3(basePath, ".claude-plugin", "marketplace.json");
|
|
623
|
+
if (!existsSync2(filePath)) return [];
|
|
624
|
+
try {
|
|
625
|
+
const raw = await readFile(filePath, "utf-8");
|
|
626
|
+
const parsed = JSON.parse(raw);
|
|
627
|
+
const roots = (parsed.plugins ?? []).map((plugin) => typeof plugin.source === "string" ? plugin.source : null).filter(Boolean);
|
|
628
|
+
return roots.map((root) => normalizeRoot(root)).filter(Boolean);
|
|
629
|
+
} catch {
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function collectSkillsFromRoot(root, seenSlugs, skills) {
|
|
634
|
+
if (!root || !existsSync2(root) || isDeniedPath(root)) return;
|
|
635
|
+
const skillDirs = await findSkillDirs(root);
|
|
636
|
+
for (const skillDir of skillDirs) {
|
|
637
|
+
const skill = await parseSkillMd(join3(skillDir, "SKILL.md"));
|
|
638
|
+
if (!skill) continue;
|
|
639
|
+
const slug = basename(skill.path).toLowerCase();
|
|
640
|
+
if (seenSlugs.has(slug)) continue;
|
|
641
|
+
skills.push(skill);
|
|
642
|
+
seenSlugs.add(slug);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async function listPluginSkillRoots(basePath) {
|
|
646
|
+
const pluginsDir = join3(basePath, "plugins");
|
|
647
|
+
if (!existsSync2(pluginsDir)) return [];
|
|
648
|
+
try {
|
|
649
|
+
const entries = await readdir(pluginsDir, { withFileTypes: true });
|
|
650
|
+
const roots = [];
|
|
651
|
+
for (const entry of entries) {
|
|
652
|
+
if (!entry.isDirectory()) continue;
|
|
653
|
+
const candidate = join3(pluginsDir, entry.name, "skills");
|
|
654
|
+
if (existsSync2(candidate)) {
|
|
655
|
+
roots.push(candidate);
|
|
656
|
+
}
|
|
639
657
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
658
|
+
return roots;
|
|
659
|
+
} catch {
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async function discoverSkills(basePath, subpath) {
|
|
664
|
+
const skills = [];
|
|
665
|
+
const seenSlugs = /* @__PURE__ */ new Set();
|
|
666
|
+
const searchPath = subpath ? join3(basePath, subpath) : basePath;
|
|
667
|
+
if (await hasSkillMd(searchPath)) {
|
|
668
|
+
const skill = await parseSkillMd(join3(searchPath, "SKILL.md"));
|
|
669
|
+
if (skill) {
|
|
670
|
+
skills.push(skill);
|
|
671
|
+
return skills;
|
|
648
672
|
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
673
|
+
}
|
|
674
|
+
const marketplaceRoots = await readMarketplacePluginRoots(searchPath);
|
|
675
|
+
for (const root of marketplaceRoots) {
|
|
676
|
+
const skillsRoot = root.toLowerCase().endsWith("/skills") ? root : `${root}/skills`;
|
|
677
|
+
await collectSkillsFromRoot(join3(searchPath, skillsRoot), seenSlugs, skills);
|
|
678
|
+
}
|
|
679
|
+
await collectSkillsFromRoot(join3(searchPath, "skills"), seenSlugs, skills);
|
|
680
|
+
const pluginRoots = await listPluginSkillRoots(searchPath);
|
|
681
|
+
for (const root of pluginRoots) {
|
|
682
|
+
await collectSkillsFromRoot(root, seenSlugs, skills);
|
|
683
|
+
}
|
|
684
|
+
await collectSkillsFromRoot(join3(searchPath, ".claude-plugin"), seenSlugs, skills);
|
|
685
|
+
const agentRoots = [
|
|
686
|
+
join3(searchPath, ".agent/skills"),
|
|
687
|
+
join3(searchPath, ".agents/skills"),
|
|
688
|
+
join3(searchPath, ".cline/skills"),
|
|
689
|
+
join3(searchPath, ".commandcode/skills"),
|
|
690
|
+
join3(searchPath, ".continue/skills"),
|
|
691
|
+
join3(searchPath, ".cursor/skills"),
|
|
692
|
+
join3(searchPath, ".factory/skills"),
|
|
693
|
+
join3(searchPath, ".github/skills"),
|
|
694
|
+
join3(searchPath, ".goose/skills"),
|
|
695
|
+
join3(searchPath, ".kilocode/skills"),
|
|
696
|
+
join3(searchPath, ".kiro/skills"),
|
|
697
|
+
join3(searchPath, ".neovate/skills"),
|
|
698
|
+
join3(searchPath, ".openhands/skills"),
|
|
699
|
+
join3(searchPath, ".pi/skills"),
|
|
700
|
+
join3(searchPath, ".qoder/skills"),
|
|
701
|
+
join3(searchPath, ".roo/skills"),
|
|
702
|
+
join3(searchPath, ".trae/skills"),
|
|
703
|
+
join3(searchPath, ".windsurf/skills"),
|
|
704
|
+
join3(searchPath, ".zencoder/skills")
|
|
705
|
+
];
|
|
706
|
+
for (const root of agentRoots) {
|
|
707
|
+
await collectSkillsFromRoot(root, seenSlugs, skills);
|
|
708
|
+
}
|
|
709
|
+
if (skills.length === 0) {
|
|
710
|
+
const allSkillDirs = await findSkillDirs(searchPath);
|
|
711
|
+
for (const skillDir of allSkillDirs) {
|
|
712
|
+
const skill = await parseSkillMd(join3(skillDir, "SKILL.md"));
|
|
713
|
+
if (!skill) continue;
|
|
714
|
+
const slug = basename(skill.path).toLowerCase();
|
|
715
|
+
if (seenSlugs.has(slug)) continue;
|
|
716
|
+
skills.push(skill);
|
|
717
|
+
seenSlugs.add(slug);
|
|
657
718
|
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
719
|
+
}
|
|
720
|
+
return skills;
|
|
721
|
+
}
|
|
722
|
+
function getSkillDisplayName(skill) {
|
|
723
|
+
return skill.name || basename(skill.path);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/flows/find-skill.ts
|
|
727
|
+
async function searchSkillDirectory(query, mode, limit = 10) {
|
|
728
|
+
const trimmed = query.trim();
|
|
729
|
+
if (!trimmed) {
|
|
730
|
+
return { mode, results: [], fallback: false };
|
|
731
|
+
}
|
|
732
|
+
if (mode === "semantic") {
|
|
733
|
+
try {
|
|
734
|
+
const results2 = await searchSkills(trimmed, "semantic", limit);
|
|
735
|
+
return { mode: "semantic", results: results2, fallback: false };
|
|
736
|
+
} catch {
|
|
737
|
+
const results2 = await searchSkills(trimmed, "lexical", limit);
|
|
738
|
+
return { mode: "lexical", results: results2, fallback: true };
|
|
666
739
|
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
740
|
+
}
|
|
741
|
+
const results = await searchSkills(trimmed, "lexical", limit);
|
|
742
|
+
return { mode: "lexical", results, fallback: false };
|
|
743
|
+
}
|
|
744
|
+
var normalizeSkillPath = (value) => value.replace(/^\/+/, "").replace(/\\/g, "/");
|
|
745
|
+
var toSkillDir = (skillPath) => {
|
|
746
|
+
const normalized = normalizeSkillPath(skillPath);
|
|
747
|
+
const cleaned = normalized.replace(/\/?SKILL\.md$/i, "").replace(/\/+$/, "");
|
|
748
|
+
return cleaned;
|
|
749
|
+
};
|
|
750
|
+
var ensureSkillMdPath = (skillPath) => {
|
|
751
|
+
const normalized = normalizeSkillPath(skillPath);
|
|
752
|
+
if (/\/?SKILL\.md$/i.test(normalized)) {
|
|
753
|
+
return normalized;
|
|
754
|
+
}
|
|
755
|
+
if (!normalized) {
|
|
756
|
+
return "SKILL.md";
|
|
757
|
+
}
|
|
758
|
+
return `${normalized.replace(/\/+$/, "")}/SKILL.md`;
|
|
759
|
+
};
|
|
760
|
+
var sanitizeRepoDir = (owner, repo) => `${owner}-${repo}`.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
761
|
+
async function prepareSkillsFromSearchResults(selected) {
|
|
762
|
+
if (selected.length === 0) {
|
|
763
|
+
throw new Error("Select at least one skill to install.");
|
|
764
|
+
}
|
|
765
|
+
const tempDir = await mkdtemp2(join4(tmpdir3(), "playbooks-search-"));
|
|
766
|
+
registerTempDir(tempDir);
|
|
767
|
+
try {
|
|
768
|
+
const repoMap = /* @__PURE__ */ new Map();
|
|
769
|
+
for (const result of selected) {
|
|
770
|
+
if (!result.repoOwner || !result.repoName || !result.path) {
|
|
771
|
+
throw new Error(`Missing repository data for ${result.name}.`);
|
|
772
|
+
}
|
|
773
|
+
const key = `${result.repoOwner.toLowerCase()}/${result.repoName.toLowerCase()}`;
|
|
774
|
+
const existing = repoMap.get(key);
|
|
775
|
+
if (existing) {
|
|
776
|
+
existing.entries.push(result);
|
|
777
|
+
} else {
|
|
778
|
+
repoMap.set(key, {
|
|
779
|
+
owner: result.repoOwner,
|
|
780
|
+
repo: result.repoName,
|
|
781
|
+
repoUrl: `https://github.com/${result.repoOwner}/${result.repoName}.git`,
|
|
782
|
+
entries: [result]
|
|
783
|
+
});
|
|
784
|
+
}
|
|
675
785
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
786
|
+
const repoDirs = /* @__PURE__ */ new Map();
|
|
787
|
+
const usedDirs = /* @__PURE__ */ new Set();
|
|
788
|
+
for (const [key, repoInfo] of repoMap) {
|
|
789
|
+
let dirName = sanitizeRepoDir(repoInfo.owner, repoInfo.repo);
|
|
790
|
+
let suffix = 1;
|
|
791
|
+
while (usedDirs.has(dirName)) {
|
|
792
|
+
dirName = `${sanitizeRepoDir(repoInfo.owner, repoInfo.repo)}-${suffix}`;
|
|
793
|
+
suffix += 1;
|
|
794
|
+
}
|
|
795
|
+
usedDirs.add(dirName);
|
|
796
|
+
const repoDir = join4(tempDir, dirName);
|
|
797
|
+
await cloneRepoTo(repoInfo.repoUrl, repoDir);
|
|
798
|
+
repoDirs.set(key, repoDir);
|
|
684
799
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
800
|
+
const skills = [];
|
|
801
|
+
const originBySkillName = /* @__PURE__ */ new Map();
|
|
802
|
+
for (const result of selected) {
|
|
803
|
+
if (!result.repoOwner || !result.repoName || !result.path) {
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
const repoKey = `${result.repoOwner.toLowerCase()}/${result.repoName.toLowerCase()}`;
|
|
807
|
+
const repoDir = repoDirs.get(repoKey);
|
|
808
|
+
if (!repoDir) {
|
|
809
|
+
throw new Error(`Missing clone for ${result.repoOwner}/${result.repoName}.`);
|
|
810
|
+
}
|
|
811
|
+
const skillDir = toSkillDir(result.path);
|
|
812
|
+
const subpath = skillDir ? skillDir : void 0;
|
|
813
|
+
const discovered = await discoverSkills(repoDir, subpath);
|
|
814
|
+
if (discovered.length === 0) {
|
|
815
|
+
throw new Error(`Skill not found in ${result.repoOwner}/${result.repoName}.`);
|
|
816
|
+
}
|
|
817
|
+
const expectedPath = join4(repoDir, skillDir);
|
|
818
|
+
const fallbackSkill = discovered[0];
|
|
819
|
+
if (!fallbackSkill) {
|
|
820
|
+
throw new Error(`Skill not found in ${result.repoOwner}/${result.repoName}.`);
|
|
821
|
+
}
|
|
822
|
+
const skill = discovered.find((entry) => entry.path === expectedPath) ?? fallbackSkill;
|
|
823
|
+
if (!existsSync3(join4(skill.path, "SKILL.md"))) {
|
|
824
|
+
throw new Error(`SKILL.md missing for ${result.name}.`);
|
|
825
|
+
}
|
|
826
|
+
skills.push(skill);
|
|
827
|
+
const displayName = getSkillDisplayName(skill);
|
|
828
|
+
originBySkillName.set(displayName, {
|
|
829
|
+
sourceType: "github",
|
|
830
|
+
source: `${result.repoOwner}/${result.repoName}`,
|
|
831
|
+
sourceUrl: `https://github.com/${result.repoOwner}/${result.repoName}.git`,
|
|
832
|
+
skillPath: ensureSkillMdPath(result.path)
|
|
833
|
+
});
|
|
693
834
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
globalSkillsDir: join2(home, ".qwen/skills"),
|
|
700
|
-
detectInstalled: async () => {
|
|
701
|
-
return existsSync(join2(home, ".qwen"));
|
|
835
|
+
return { tempDir, skills, originBySkillName };
|
|
836
|
+
} catch (error) {
|
|
837
|
+
try {
|
|
838
|
+
await cleanupTempDir(tempDir);
|
|
839
|
+
} catch {
|
|
702
840
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
841
|
+
throw error;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/flows/url-markdown-output.ts
|
|
846
|
+
var output = null;
|
|
847
|
+
function setUrlMarkdownOutput(next) {
|
|
848
|
+
output = next;
|
|
849
|
+
}
|
|
850
|
+
function consumeUrlMarkdownOutput() {
|
|
851
|
+
const current = output;
|
|
852
|
+
output = null;
|
|
853
|
+
return current;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/telemetry.ts
|
|
857
|
+
import { createHmac } from "crypto";
|
|
858
|
+
var API_BASE2 = process.env.PLAYBOOKS_API_URL?.trim() || "https://playbooks.com/api";
|
|
859
|
+
var TELEMETRY_URL = process.env.PLAYBOOKS_TELEMETRY_URL?.trim() || `${API_BASE2}/skill/t`;
|
|
860
|
+
var cliVersion = null;
|
|
861
|
+
function isCI() {
|
|
862
|
+
return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION);
|
|
863
|
+
}
|
|
864
|
+
function isEnabled() {
|
|
865
|
+
return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK && !process.env.PLAYBOOKS_DISABLE_TELEMETRY;
|
|
866
|
+
}
|
|
867
|
+
function setVersion(version2) {
|
|
868
|
+
cliVersion = version2;
|
|
869
|
+
}
|
|
870
|
+
function buildHeaders(body) {
|
|
871
|
+
const headers = {
|
|
872
|
+
"Content-Type": "application/json",
|
|
873
|
+
"User-Agent": "playbooks-cli"
|
|
874
|
+
};
|
|
875
|
+
if (cliVersion) {
|
|
876
|
+
headers["X-Playbooks-Version"] = cliVersion;
|
|
877
|
+
}
|
|
878
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
879
|
+
headers["X-Playbooks-Timestamp"] = timestamp;
|
|
880
|
+
const secret = process.env.PLAYBOOKS_TELEMETRY_SECRET;
|
|
881
|
+
if (secret) {
|
|
882
|
+
const signature = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
|
|
883
|
+
headers["X-Playbooks-Signature"] = signature;
|
|
884
|
+
}
|
|
885
|
+
return headers;
|
|
886
|
+
}
|
|
887
|
+
function trackInstall(payload) {
|
|
888
|
+
if (!isEnabled()) return;
|
|
889
|
+
try {
|
|
890
|
+
const body = JSON.stringify({
|
|
891
|
+
...payload,
|
|
892
|
+
version: cliVersion ?? void 0,
|
|
893
|
+
ci: isCI() || void 0
|
|
894
|
+
});
|
|
895
|
+
fetch(TELEMETRY_URL, {
|
|
896
|
+
method: "POST",
|
|
897
|
+
headers: buildHeaders(body),
|
|
898
|
+
body
|
|
899
|
+
}).catch(() => {
|
|
900
|
+
});
|
|
901
|
+
} catch {
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/tui/App.tsx
|
|
906
|
+
import { render } from "ink";
|
|
907
|
+
|
|
908
|
+
// src/tui/context/navigation.tsx
|
|
909
|
+
import React, { createContext, useCallback, useContext, useMemo, useState } from "react";
|
|
910
|
+
import { jsx } from "react/jsx-runtime";
|
|
911
|
+
var NavigationContext = createContext(null);
|
|
912
|
+
function useNavigation() {
|
|
913
|
+
const ctx = useContext(NavigationContext);
|
|
914
|
+
if (!ctx) throw new Error("NavigationContext not found");
|
|
915
|
+
return ctx;
|
|
916
|
+
}
|
|
917
|
+
function NavigationProvider({
|
|
918
|
+
children,
|
|
919
|
+
initialInvocation,
|
|
920
|
+
initialScreen
|
|
921
|
+
}) {
|
|
922
|
+
const [screen, setScreen] = useState(initialScreen);
|
|
923
|
+
const [stack, setStack] = useState([initialScreen]);
|
|
924
|
+
const [flashes, setFlashes] = useState([]);
|
|
925
|
+
const [invocation, setInvocation] = useState(initialInvocation);
|
|
926
|
+
const [addSkill, setAddSkill] = useState({});
|
|
927
|
+
const [findSkill, setFindSkill] = useState({ status: "idle" });
|
|
928
|
+
const [isTextInputActive, setTextInputActive] = useState(false);
|
|
929
|
+
const [textInputEscMode, setTextInputEscMode] = useState("back");
|
|
930
|
+
const [navAction, setNavAction] = useState("reset");
|
|
931
|
+
const [lastSource, setLastSource] = useState(initialInvocation.source ?? null);
|
|
932
|
+
const backHandlerRef = React.useRef(null);
|
|
933
|
+
const flashTimersRef = React.useRef(/* @__PURE__ */ new Map());
|
|
934
|
+
const navigateTo = useCallback((s) => {
|
|
935
|
+
setNavAction("push");
|
|
936
|
+
setStack((prev) => [...prev, s]);
|
|
937
|
+
setScreen(s);
|
|
938
|
+
}, []);
|
|
939
|
+
const resetTo = useCallback((s) => {
|
|
940
|
+
setNavAction("reset");
|
|
941
|
+
setStack([s]);
|
|
942
|
+
setScreen(s);
|
|
943
|
+
}, []);
|
|
944
|
+
const goBack = useCallback(() => {
|
|
945
|
+
setNavAction("pop");
|
|
946
|
+
let target;
|
|
947
|
+
if (stack.length <= 1) {
|
|
948
|
+
target = stack[0] ?? "main";
|
|
949
|
+
setStack([target]);
|
|
950
|
+
} else {
|
|
951
|
+
const next = stack.slice(0, -1);
|
|
952
|
+
target = next[next.length - 1] ?? "main";
|
|
953
|
+
setStack(next);
|
|
738
954
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
955
|
+
setScreen(target);
|
|
956
|
+
}, [stack]);
|
|
957
|
+
const setFlash = useCallback((msg) => {
|
|
958
|
+
if (msg === null) {
|
|
959
|
+
for (const timer2 of flashTimersRef.current.values()) {
|
|
960
|
+
clearTimeout(timer2);
|
|
961
|
+
}
|
|
962
|
+
flashTimersRef.current.clear();
|
|
963
|
+
setFlashes([]);
|
|
964
|
+
return;
|
|
747
965
|
}
|
|
748
|
-
|
|
749
|
-
};
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
966
|
+
const id = Date.now() + Math.random();
|
|
967
|
+
setFlashes((prev) => [...prev, { id, text: msg }]);
|
|
968
|
+
const timer = setTimeout(() => {
|
|
969
|
+
setFlashes((prev) => prev.filter((f) => f.id !== id));
|
|
970
|
+
flashTimersRef.current.delete(id);
|
|
971
|
+
}, 5e3);
|
|
972
|
+
flashTimersRef.current.set(id, timer);
|
|
973
|
+
}, []);
|
|
974
|
+
React.useEffect(() => {
|
|
975
|
+
return () => {
|
|
976
|
+
for (const timer of flashTimersRef.current.values()) {
|
|
977
|
+
clearTimeout(timer);
|
|
978
|
+
}
|
|
979
|
+
flashTimersRef.current.clear();
|
|
980
|
+
};
|
|
981
|
+
}, []);
|
|
982
|
+
React.useEffect(() => {
|
|
983
|
+
if (!addSkill.tempDir) return;
|
|
984
|
+
if (screen.startsWith("add-") || screen.startsWith("find-")) return;
|
|
985
|
+
cleanupTempDir(addSkill.tempDir).catch(() => {
|
|
986
|
+
});
|
|
987
|
+
}, [addSkill.tempDir, screen]);
|
|
988
|
+
const updateAddSkill = useCallback((patch) => {
|
|
989
|
+
setAddSkill((prev) => ({ ...prev, ...patch }));
|
|
990
|
+
}, []);
|
|
991
|
+
const resetAddSkill = useCallback(() => {
|
|
992
|
+
setAddSkill({});
|
|
993
|
+
}, []);
|
|
994
|
+
const updateFindSkill = useCallback((patch) => {
|
|
995
|
+
setFindSkill((prev) => ({ ...prev, ...patch }));
|
|
996
|
+
}, []);
|
|
997
|
+
const resetFindSkill = useCallback(() => {
|
|
998
|
+
setFindSkill({ status: "idle" });
|
|
999
|
+
}, []);
|
|
1000
|
+
const value = useMemo(
|
|
1001
|
+
() => ({
|
|
1002
|
+
screen,
|
|
1003
|
+
setScreen,
|
|
1004
|
+
navigateTo,
|
|
1005
|
+
resetTo,
|
|
1006
|
+
goBack,
|
|
1007
|
+
stack,
|
|
1008
|
+
flashes,
|
|
1009
|
+
setFlash,
|
|
1010
|
+
navAction,
|
|
1011
|
+
lastSource,
|
|
1012
|
+
setLastSource,
|
|
1013
|
+
getBackHandler: () => backHandlerRef.current,
|
|
1014
|
+
setBackHandler: (fn) => {
|
|
1015
|
+
backHandlerRef.current = fn;
|
|
1016
|
+
},
|
|
1017
|
+
invocation,
|
|
1018
|
+
setInvocation,
|
|
1019
|
+
addSkill,
|
|
1020
|
+
setAddSkill,
|
|
1021
|
+
updateAddSkill,
|
|
1022
|
+
resetAddSkill,
|
|
1023
|
+
findSkill,
|
|
1024
|
+
setFindSkill,
|
|
1025
|
+
updateFindSkill,
|
|
1026
|
+
resetFindSkill,
|
|
1027
|
+
isTextInputActive,
|
|
1028
|
+
setTextInputActive,
|
|
1029
|
+
textInputEscMode,
|
|
1030
|
+
setTextInputEscMode
|
|
1031
|
+
}),
|
|
1032
|
+
[
|
|
1033
|
+
screen,
|
|
1034
|
+
stack,
|
|
1035
|
+
flashes,
|
|
1036
|
+
navAction,
|
|
1037
|
+
navigateTo,
|
|
1038
|
+
resetTo,
|
|
1039
|
+
goBack,
|
|
1040
|
+
setFlash,
|
|
1041
|
+
lastSource,
|
|
1042
|
+
invocation,
|
|
1043
|
+
addSkill,
|
|
1044
|
+
updateAddSkill,
|
|
1045
|
+
resetAddSkill,
|
|
1046
|
+
findSkill,
|
|
1047
|
+
updateFindSkill,
|
|
1048
|
+
resetFindSkill,
|
|
1049
|
+
isTextInputActive,
|
|
1050
|
+
textInputEscMode
|
|
1051
|
+
]
|
|
1052
|
+
);
|
|
1053
|
+
return /* @__PURE__ */ jsx(NavigationContext.Provider, { value, children });
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/tui/ui/BrandHeader.tsx
|
|
1057
|
+
import { Box, Text } from "ink";
|
|
1058
|
+
import React2 from "react";
|
|
1059
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1060
|
+
var TARGET_TEXT = "playbooks";
|
|
1061
|
+
var TAGLINE = "Give your agents context to make them smarter.";
|
|
1062
|
+
var TEXT_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
1063
|
+
function randomChar(chars) {
|
|
1064
|
+
const pool = Array.isArray(chars) ? chars : chars.split("");
|
|
1065
|
+
return pool[Math.floor(Math.random() * pool.length)] ?? "";
|
|
1066
|
+
}
|
|
1067
|
+
function buildScrambleText(target, settles, progress) {
|
|
1068
|
+
const letters = target.split("");
|
|
1069
|
+
return letters.map((char, index) => {
|
|
1070
|
+
if (char === " ") return char;
|
|
1071
|
+
const settle = settles[index] ?? 1;
|
|
1072
|
+
if (progress >= settle) return char;
|
|
1073
|
+
return randomChar(TEXT_CHARS);
|
|
1074
|
+
}).join("");
|
|
1075
|
+
}
|
|
1076
|
+
function BrandHeader() {
|
|
1077
|
+
const [title, setTitle] = React2.useState(
|
|
1078
|
+
() => TARGET_TEXT.split("").map((char) => char === " " ? " " : randomChar(TEXT_CHARS)).join("")
|
|
1079
|
+
);
|
|
1080
|
+
const hasAnimated = React2.useRef(false);
|
|
1081
|
+
const textSettles = React2.useRef([]);
|
|
1082
|
+
React2.useEffect(() => {
|
|
1083
|
+
if (hasAnimated.current) return;
|
|
1084
|
+
hasAnimated.current = true;
|
|
1085
|
+
textSettles.current = TARGET_TEXT.split("").map(
|
|
1086
|
+
(char) => char === " " ? 0 : 0.2 + Math.random() * 0.6
|
|
1087
|
+
);
|
|
1088
|
+
const steps = 18;
|
|
1089
|
+
const interval = 50;
|
|
1090
|
+
let step = 0;
|
|
1091
|
+
const tick = () => {
|
|
1092
|
+
const progress = Math.min(1, step / steps);
|
|
1093
|
+
setTitle(buildScrambleText(TARGET_TEXT, textSettles.current, progress));
|
|
1094
|
+
step += 1;
|
|
1095
|
+
if (step > steps) {
|
|
1096
|
+
setTitle(TARGET_TEXT);
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
return true;
|
|
1100
|
+
};
|
|
1101
|
+
tick();
|
|
1102
|
+
const timer = setInterval(() => {
|
|
1103
|
+
if (!tick()) {
|
|
1104
|
+
clearInterval(timer);
|
|
1105
|
+
}
|
|
1106
|
+
}, interval);
|
|
1107
|
+
return () => {
|
|
1108
|
+
clearInterval(timer);
|
|
1109
|
+
};
|
|
1110
|
+
}, []);
|
|
1111
|
+
return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [
|
|
1112
|
+
/* @__PURE__ */ jsx2(Text, { bold: true, children: title }),
|
|
1113
|
+
/* @__PURE__ */ jsx2(Text, { dimColor: true, children: TAGLINE })
|
|
1114
|
+
] });
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/tui/ui/FlashBar.tsx
|
|
1118
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
1119
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1120
|
+
function classify(msg) {
|
|
1121
|
+
const m = msg.toLowerCase();
|
|
1122
|
+
if (/(error|failed|unable|denied|invalid|cannot|not found)/i.test(msg)) return "error";
|
|
1123
|
+
if (/(deleted|linked|unlinked|updated|created|saved|enabled|disabled|added|removed|✓)/i.test(msg))
|
|
1124
|
+
return "success";
|
|
1125
|
+
if (/(warn|deprecated|missing|retry|timeout)/i.test(msg)) return "warning";
|
|
1126
|
+
return "info";
|
|
1127
|
+
}
|
|
1128
|
+
function FlashBar({ align = "left" }) {
|
|
1129
|
+
const { flashes } = useNavigation();
|
|
1130
|
+
if (flashes.length === 0) return /* @__PURE__ */ jsx3(Box2, { height: 1 });
|
|
1131
|
+
return /* @__PURE__ */ jsx3(
|
|
1132
|
+
Box2,
|
|
1133
|
+
{
|
|
1134
|
+
flexDirection: "column",
|
|
1135
|
+
paddingX: 1,
|
|
1136
|
+
paddingY: 0,
|
|
1137
|
+
gap: 0,
|
|
1138
|
+
justifyContent: align === "center" ? "center" : "flex-start",
|
|
1139
|
+
children: flashes.map((flash) => {
|
|
1140
|
+
const kind = classify(flash.text);
|
|
1141
|
+
const color = kind === "success" ? "green" : kind === "error" ? "red" : kind === "warning" ? "yellow" : "cyan";
|
|
1142
|
+
const label = kind === "success" ? "\u2713" : kind === "error" ? "x" : kind === "warning" ? "!" : "i";
|
|
1143
|
+
return /* @__PURE__ */ jsxs2(Text2, { color, children: [
|
|
1144
|
+
"[",
|
|
1145
|
+
label,
|
|
1146
|
+
"] ",
|
|
1147
|
+
flash.text
|
|
1148
|
+
] }, flash.id);
|
|
1149
|
+
})
|
|
755
1150
|
}
|
|
756
|
-
|
|
757
|
-
return installed;
|
|
1151
|
+
);
|
|
758
1152
|
}
|
|
759
1153
|
|
|
1154
|
+
// src/tui/screens/AddConfirm.tsx
|
|
1155
|
+
import { join as join8 } from "path";
|
|
1156
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
1157
|
+
import React3 from "react";
|
|
1158
|
+
|
|
760
1159
|
// src/cli-utils.ts
|
|
761
1160
|
import { homedir as homedir2 } from "os";
|
|
762
1161
|
function shortenPath(fullPath, cwd) {
|
|
@@ -783,12 +1182,12 @@ import chalk from "chalk";
|
|
|
783
1182
|
|
|
784
1183
|
// src/installer/install.ts
|
|
785
1184
|
import { access, mkdir as mkdir2, rm as rm4, writeFile } from "fs/promises";
|
|
786
|
-
import { basename, join as
|
|
1185
|
+
import { basename as basename2, join as join7 } from "path";
|
|
787
1186
|
|
|
788
1187
|
// src/installer/files.ts
|
|
789
|
-
import { cp, lstat, mkdir, readdir, readlink, rm as rm3, symlink } from "fs/promises";
|
|
1188
|
+
import { cp, lstat, mkdir, readdir as readdir2, readlink, rm as rm3, symlink } from "fs/promises";
|
|
790
1189
|
import { platform } from "os";
|
|
791
|
-
import { join as
|
|
1190
|
+
import { join as join5, relative, resolve as resolve2 } from "path";
|
|
792
1191
|
var EXCLUDE_FILES = /* @__PURE__ */ new Set(["README.md", "metadata.json"]);
|
|
793
1192
|
var isExcluded = (name) => {
|
|
794
1193
|
if (EXCLUDE_FILES.has(name)) return true;
|
|
@@ -816,7 +1215,7 @@ async function createSymlink(target, linkPath) {
|
|
|
816
1215
|
}
|
|
817
1216
|
}
|
|
818
1217
|
}
|
|
819
|
-
const linkDir =
|
|
1218
|
+
const linkDir = join5(linkPath, "..");
|
|
820
1219
|
await mkdir(linkDir, { recursive: true });
|
|
821
1220
|
const relativePath = relative(linkDir, target);
|
|
822
1221
|
const symlinkType = platform() === "win32" ? "junction" : void 0;
|
|
@@ -828,13 +1227,13 @@ async function createSymlink(target, linkPath) {
|
|
|
828
1227
|
}
|
|
829
1228
|
async function copyDirectory(src, dest) {
|
|
830
1229
|
await mkdir(dest, { recursive: true });
|
|
831
|
-
const entries = await
|
|
1230
|
+
const entries = await readdir2(src, { withFileTypes: true });
|
|
832
1231
|
for (const entry of entries) {
|
|
833
1232
|
if (isExcluded(entry.name)) {
|
|
834
1233
|
continue;
|
|
835
1234
|
}
|
|
836
|
-
const srcPath =
|
|
837
|
-
const destPath =
|
|
1235
|
+
const srcPath = join5(src, entry.name);
|
|
1236
|
+
const destPath = join5(dest, entry.name);
|
|
838
1237
|
if (entry.isDirectory()) {
|
|
839
1238
|
await copyDirectory(srcPath, destPath);
|
|
840
1239
|
} else {
|
|
@@ -848,7 +1247,7 @@ async function copySkillDirectory(src, dest) {
|
|
|
848
1247
|
|
|
849
1248
|
// src/installer/paths.ts
|
|
850
1249
|
import { homedir as homedir3 } from "os";
|
|
851
|
-
import { join as
|
|
1250
|
+
import { join as join6, normalize as normalize2, resolve as resolve3, sep as sep2 } from "path";
|
|
852
1251
|
var AGENTS_DIR = ".agents";
|
|
853
1252
|
var SKILLS_SUBDIR = "skills";
|
|
854
1253
|
function sanitizeSkillName(name) {
|
|
@@ -870,7 +1269,7 @@ function isPathSafe(basePath, targetPath) {
|
|
|
870
1269
|
}
|
|
871
1270
|
function getCanonicalSkillsBase(options = {}) {
|
|
872
1271
|
const baseDir = options.global ? homedir3() : options.cwd || process.cwd();
|
|
873
|
-
return
|
|
1272
|
+
return join6(baseDir, AGENTS_DIR, SKILLS_SUBDIR);
|
|
874
1273
|
}
|
|
875
1274
|
function getCanonicalSkillsDir(global, cwd) {
|
|
876
1275
|
return getCanonicalSkillsBase({ global, cwd });
|
|
@@ -878,7 +1277,7 @@ function getCanonicalSkillsDir(global, cwd) {
|
|
|
878
1277
|
function getCanonicalPath(skillName, options = {}) {
|
|
879
1278
|
const sanitized = sanitizeSkillName(skillName);
|
|
880
1279
|
const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
|
|
881
|
-
const canonicalPath =
|
|
1280
|
+
const canonicalPath = join6(canonicalBase, sanitized);
|
|
882
1281
|
if (!isPathSafe(canonicalBase, canonicalPath)) {
|
|
883
1282
|
throw new Error("Invalid skill name: potential path traversal detected");
|
|
884
1283
|
}
|
|
@@ -890,9 +1289,9 @@ function getInstallTargets(rawSkillName, agentType, options = {}) {
|
|
|
890
1289
|
const cwd = options.cwd || process.cwd();
|
|
891
1290
|
const skillName = sanitizeSkillName(rawSkillName);
|
|
892
1291
|
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
893
|
-
const canonicalDir =
|
|
894
|
-
const agentBase = isGlobal ? agent.globalSkillsDir :
|
|
895
|
-
const agentDir =
|
|
1292
|
+
const canonicalDir = join6(canonicalBase, skillName);
|
|
1293
|
+
const agentBase = isGlobal ? agent.globalSkillsDir : join6(cwd, agent.skillsDir);
|
|
1294
|
+
const agentDir = join6(agentBase, skillName);
|
|
896
1295
|
return {
|
|
897
1296
|
skillName,
|
|
898
1297
|
canonicalBase,
|
|
@@ -906,7 +1305,7 @@ function getInstallTargets(rawSkillName, agentType, options = {}) {
|
|
|
906
1305
|
async function installSkillForAgent(skill, agentType, options = {}) {
|
|
907
1306
|
const isGlobal = options.global ?? false;
|
|
908
1307
|
const cwd = options.cwd || process.cwd();
|
|
909
|
-
const rawSkillName = skill.name ||
|
|
1308
|
+
const rawSkillName = skill.name || basename2(skill.path);
|
|
910
1309
|
const { canonicalBase, canonicalDir, agentBase, agentDir } = getInstallTargets(
|
|
911
1310
|
rawSkillName,
|
|
912
1311
|
agentType,
|
|
@@ -947,232 +1346,45 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
947
1346
|
await rm4(agentDir, { recursive: true, force: true });
|
|
948
1347
|
} catch {
|
|
949
1348
|
}
|
|
950
|
-
await mkdir2(agentDir, { recursive: true });
|
|
951
|
-
await copySkillDirectory(skill.path, agentDir);
|
|
952
|
-
return {
|
|
953
|
-
success: true,
|
|
954
|
-
path: agentDir,
|
|
955
|
-
canonicalPath: canonicalDir,
|
|
956
|
-
mode: "symlink",
|
|
957
|
-
symlinkFailed: true
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
return {
|
|
961
|
-
success: true,
|
|
962
|
-
path: agentDir,
|
|
963
|
-
canonicalPath: canonicalDir,
|
|
964
|
-
mode: "symlink"
|
|
965
|
-
};
|
|
966
|
-
} catch (error) {
|
|
967
|
-
return {
|
|
968
|
-
success: false,
|
|
969
|
-
path: agentDir,
|
|
970
|
-
mode: installMode,
|
|
971
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
async function isSkillInstalled(skillName, agentType, options = {}) {
|
|
976
|
-
const agent = agents[agentType];
|
|
977
|
-
const sanitized = sanitizeSkillName(skillName);
|
|
978
|
-
const targetBase = options.global ? agent.globalSkillsDir : join5(options.cwd || process.cwd(), agent.skillsDir);
|
|
979
|
-
const skillDir = join5(targetBase, sanitized);
|
|
980
|
-
if (!isPathSafe(targetBase, skillDir)) {
|
|
981
|
-
return false;
|
|
982
|
-
}
|
|
983
|
-
try {
|
|
984
|
-
await access(skillDir);
|
|
985
|
-
return true;
|
|
986
|
-
} catch {
|
|
987
|
-
return false;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// src/skills.ts
|
|
992
|
-
import { existsSync as existsSync2 } from "fs";
|
|
993
|
-
import { readFile, readdir as readdir2, stat } from "fs/promises";
|
|
994
|
-
import { basename as basename2, dirname, join as join6 } from "path";
|
|
995
|
-
import matter from "gray-matter";
|
|
996
|
-
var SKIP_DIRS = ["node_modules", ".git", ".github", "dist", "build", "__pycache__"];
|
|
997
|
-
var DENIED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
998
|
-
".git",
|
|
999
|
-
"node_modules",
|
|
1000
|
-
".github",
|
|
1001
|
-
"playbooks",
|
|
1002
|
-
"context",
|
|
1003
|
-
"prompts",
|
|
1004
|
-
"backups",
|
|
1005
|
-
"backup",
|
|
1006
|
-
"dist",
|
|
1007
|
-
"deprecated"
|
|
1008
|
-
]);
|
|
1009
|
-
var normalizePath = (value) => value.replace(/^\/+/, "");
|
|
1010
|
-
var normalizeRoot = (value) => normalizePath(value).replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
1011
|
-
var isDeniedPath = (path) => {
|
|
1012
|
-
const cleaned = normalizePath(path);
|
|
1013
|
-
const segments = cleaned.split("/").map((segment) => segment.toLowerCase());
|
|
1014
|
-
for (const segment of segments) {
|
|
1015
|
-
if (!segment) continue;
|
|
1016
|
-
if (segment === ".claude-plugin") {
|
|
1017
|
-
continue;
|
|
1018
|
-
}
|
|
1019
|
-
if (DENIED_SEGMENTS.has(segment)) {
|
|
1020
|
-
return true;
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
return false;
|
|
1024
|
-
};
|
|
1025
|
-
async function hasSkillMd(dir) {
|
|
1026
|
-
try {
|
|
1027
|
-
if (isDeniedPath(dir)) return false;
|
|
1028
|
-
const skillPath = join6(dir, "SKILL.md");
|
|
1029
|
-
const stats = await stat(skillPath);
|
|
1030
|
-
return stats.isFile();
|
|
1031
|
-
} catch {
|
|
1032
|
-
return false;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
async function parseSkillMd(skillMdPath) {
|
|
1036
|
-
try {
|
|
1037
|
-
if (isDeniedPath(skillMdPath)) return null;
|
|
1038
|
-
const content = await readFile(skillMdPath, "utf-8");
|
|
1039
|
-
const { data } = matter(content);
|
|
1040
|
-
if (!data.name || !data.description) {
|
|
1041
|
-
return null;
|
|
1042
|
-
}
|
|
1043
|
-
return {
|
|
1044
|
-
name: data.name,
|
|
1045
|
-
description: data.description,
|
|
1046
|
-
path: dirname(skillMdPath),
|
|
1047
|
-
rawContent: content,
|
|
1048
|
-
metadata: data.metadata
|
|
1049
|
-
};
|
|
1050
|
-
} catch {
|
|
1051
|
-
return null;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
|
|
1055
|
-
const skillDirs = [];
|
|
1056
|
-
if (depth > maxDepth) return skillDirs;
|
|
1057
|
-
if (isDeniedPath(dir)) return skillDirs;
|
|
1058
|
-
try {
|
|
1059
|
-
if (await hasSkillMd(dir)) {
|
|
1060
|
-
skillDirs.push(dir);
|
|
1061
|
-
}
|
|
1062
|
-
const entries = await readdir2(dir, { withFileTypes: true });
|
|
1063
|
-
for (const entry of entries) {
|
|
1064
|
-
if (entry.isDirectory() && !SKIP_DIRS.includes(entry.name)) {
|
|
1065
|
-
const subDirs = await findSkillDirs(join6(dir, entry.name), depth + 1, maxDepth);
|
|
1066
|
-
skillDirs.push(...subDirs);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
} catch {
|
|
1070
|
-
}
|
|
1071
|
-
return skillDirs;
|
|
1072
|
-
}
|
|
1073
|
-
async function readMarketplacePluginRoots(basePath) {
|
|
1074
|
-
const filePath = join6(basePath, ".claude-plugin", "marketplace.json");
|
|
1075
|
-
if (!existsSync2(filePath)) return [];
|
|
1076
|
-
try {
|
|
1077
|
-
const raw = await readFile(filePath, "utf-8");
|
|
1078
|
-
const parsed = JSON.parse(raw);
|
|
1079
|
-
const roots = (parsed.plugins ?? []).map((plugin) => typeof plugin.source === "string" ? plugin.source : null).filter(Boolean);
|
|
1080
|
-
return roots.map((root) => normalizeRoot(root)).filter(Boolean);
|
|
1081
|
-
} catch {
|
|
1082
|
-
return [];
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
async function collectSkillsFromRoot(root, seenSlugs, skills) {
|
|
1086
|
-
if (!root || !existsSync2(root) || isDeniedPath(root)) return;
|
|
1087
|
-
const skillDirs = await findSkillDirs(root);
|
|
1088
|
-
for (const skillDir of skillDirs) {
|
|
1089
|
-
const skill = await parseSkillMd(join6(skillDir, "SKILL.md"));
|
|
1090
|
-
if (!skill) continue;
|
|
1091
|
-
const slug = basename2(skill.path).toLowerCase();
|
|
1092
|
-
if (seenSlugs.has(slug)) continue;
|
|
1093
|
-
skills.push(skill);
|
|
1094
|
-
seenSlugs.add(slug);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
async function listPluginSkillRoots(basePath) {
|
|
1098
|
-
const pluginsDir = join6(basePath, "plugins");
|
|
1099
|
-
if (!existsSync2(pluginsDir)) return [];
|
|
1100
|
-
try {
|
|
1101
|
-
const entries = await readdir2(pluginsDir, { withFileTypes: true });
|
|
1102
|
-
const roots = [];
|
|
1103
|
-
for (const entry of entries) {
|
|
1104
|
-
if (!entry.isDirectory()) continue;
|
|
1105
|
-
const candidate = join6(pluginsDir, entry.name, "skills");
|
|
1106
|
-
if (existsSync2(candidate)) {
|
|
1107
|
-
roots.push(candidate);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
return roots;
|
|
1111
|
-
} catch {
|
|
1112
|
-
return [];
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
async function discoverSkills(basePath, subpath) {
|
|
1116
|
-
const skills = [];
|
|
1117
|
-
const seenSlugs = /* @__PURE__ */ new Set();
|
|
1118
|
-
const searchPath = subpath ? join6(basePath, subpath) : basePath;
|
|
1119
|
-
if (await hasSkillMd(searchPath)) {
|
|
1120
|
-
const skill = await parseSkillMd(join6(searchPath, "SKILL.md"));
|
|
1121
|
-
if (skill) {
|
|
1122
|
-
skills.push(skill);
|
|
1123
|
-
return skills;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
const marketplaceRoots = await readMarketplacePluginRoots(searchPath);
|
|
1127
|
-
for (const root of marketplaceRoots) {
|
|
1128
|
-
const skillsRoot = root.toLowerCase().endsWith("/skills") ? root : `${root}/skills`;
|
|
1129
|
-
await collectSkillsFromRoot(join6(searchPath, skillsRoot), seenSlugs, skills);
|
|
1130
|
-
}
|
|
1131
|
-
await collectSkillsFromRoot(join6(searchPath, "skills"), seenSlugs, skills);
|
|
1132
|
-
const pluginRoots = await listPluginSkillRoots(searchPath);
|
|
1133
|
-
for (const root of pluginRoots) {
|
|
1134
|
-
await collectSkillsFromRoot(root, seenSlugs, skills);
|
|
1135
|
-
}
|
|
1136
|
-
await collectSkillsFromRoot(join6(searchPath, ".claude-plugin"), seenSlugs, skills);
|
|
1137
|
-
const agentRoots = [
|
|
1138
|
-
join6(searchPath, ".agent/skills"),
|
|
1139
|
-
join6(searchPath, ".agents/skills"),
|
|
1140
|
-
join6(searchPath, ".cline/skills"),
|
|
1141
|
-
join6(searchPath, ".commandcode/skills"),
|
|
1142
|
-
join6(searchPath, ".continue/skills"),
|
|
1143
|
-
join6(searchPath, ".cursor/skills"),
|
|
1144
|
-
join6(searchPath, ".factory/skills"),
|
|
1145
|
-
join6(searchPath, ".github/skills"),
|
|
1146
|
-
join6(searchPath, ".goose/skills"),
|
|
1147
|
-
join6(searchPath, ".kilocode/skills"),
|
|
1148
|
-
join6(searchPath, ".kiro/skills"),
|
|
1149
|
-
join6(searchPath, ".neovate/skills"),
|
|
1150
|
-
join6(searchPath, ".openhands/skills"),
|
|
1151
|
-
join6(searchPath, ".pi/skills"),
|
|
1152
|
-
join6(searchPath, ".qoder/skills"),
|
|
1153
|
-
join6(searchPath, ".roo/skills"),
|
|
1154
|
-
join6(searchPath, ".trae/skills"),
|
|
1155
|
-
join6(searchPath, ".windsurf/skills"),
|
|
1156
|
-
join6(searchPath, ".zencoder/skills")
|
|
1157
|
-
];
|
|
1158
|
-
for (const root of agentRoots) {
|
|
1159
|
-
await collectSkillsFromRoot(root, seenSlugs, skills);
|
|
1160
|
-
}
|
|
1161
|
-
if (skills.length === 0) {
|
|
1162
|
-
const allSkillDirs = await findSkillDirs(searchPath);
|
|
1163
|
-
for (const skillDir of allSkillDirs) {
|
|
1164
|
-
const skill = await parseSkillMd(join6(skillDir, "SKILL.md"));
|
|
1165
|
-
if (!skill) continue;
|
|
1166
|
-
const slug = basename2(skill.path).toLowerCase();
|
|
1167
|
-
if (seenSlugs.has(slug)) continue;
|
|
1168
|
-
skills.push(skill);
|
|
1169
|
-
seenSlugs.add(slug);
|
|
1349
|
+
await mkdir2(agentDir, { recursive: true });
|
|
1350
|
+
await copySkillDirectory(skill.path, agentDir);
|
|
1351
|
+
return {
|
|
1352
|
+
success: true,
|
|
1353
|
+
path: agentDir,
|
|
1354
|
+
canonicalPath: canonicalDir,
|
|
1355
|
+
mode: "symlink",
|
|
1356
|
+
symlinkFailed: true
|
|
1357
|
+
};
|
|
1170
1358
|
}
|
|
1359
|
+
return {
|
|
1360
|
+
success: true,
|
|
1361
|
+
path: agentDir,
|
|
1362
|
+
canonicalPath: canonicalDir,
|
|
1363
|
+
mode: "symlink"
|
|
1364
|
+
};
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
return {
|
|
1367
|
+
success: false,
|
|
1368
|
+
path: agentDir,
|
|
1369
|
+
mode: installMode,
|
|
1370
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1371
|
+
};
|
|
1171
1372
|
}
|
|
1172
|
-
return skills;
|
|
1173
1373
|
}
|
|
1174
|
-
function
|
|
1175
|
-
|
|
1374
|
+
async function isSkillInstalled(skillName, agentType, options = {}) {
|
|
1375
|
+
const agent = agents[agentType];
|
|
1376
|
+
const sanitized = sanitizeSkillName(skillName);
|
|
1377
|
+
const targetBase = options.global ? agent.globalSkillsDir : join7(options.cwd || process.cwd(), agent.skillsDir);
|
|
1378
|
+
const skillDir = join7(targetBase, sanitized);
|
|
1379
|
+
if (!isPathSafe(targetBase, skillDir)) {
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
try {
|
|
1383
|
+
await access(skillDir);
|
|
1384
|
+
return true;
|
|
1385
|
+
} catch {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1176
1388
|
}
|
|
1177
1389
|
|
|
1178
1390
|
// src/flows/plan-summary.ts
|
|
@@ -1374,7 +1586,7 @@ function AddConfirmScreen() {
|
|
|
1374
1586
|
}, [lines]);
|
|
1375
1587
|
const agentPaths = targetAgents.length > 0 ? targetAgents.map((agent) => {
|
|
1376
1588
|
const config = agents[agent];
|
|
1377
|
-
const base = addSkill.installGlobally ? config.globalSkillsDir :
|
|
1589
|
+
const base = addSkill.installGlobally ? config.globalSkillsDir : join8(cwd, config.skillsDir);
|
|
1378
1590
|
return shortenPath(base, cwd);
|
|
1379
1591
|
}) : [];
|
|
1380
1592
|
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", padding: 1, children: [
|
|
@@ -1416,13 +1628,13 @@ import React5 from "react";
|
|
|
1416
1628
|
import { createHash } from "crypto";
|
|
1417
1629
|
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
1418
1630
|
import { homedir as homedir4 } from "os";
|
|
1419
|
-
import { dirname as dirname2, join as
|
|
1631
|
+
import { dirname as dirname2, join as join9 } from "path";
|
|
1420
1632
|
var AGENTS_DIR2 = ".agents";
|
|
1421
1633
|
var LOCK_FILE = ".skill-lock.json";
|
|
1422
1634
|
var CURRENT_VERSION = 3;
|
|
1423
1635
|
function getSkillLockPath(options = {}) {
|
|
1424
1636
|
const baseDir = options.global ? homedir4() : options.cwd || process.cwd();
|
|
1425
|
-
return
|
|
1637
|
+
return join9(baseDir, AGENTS_DIR2, LOCK_FILE);
|
|
1426
1638
|
}
|
|
1427
1639
|
async function readSkillLock(options = {}) {
|
|
1428
1640
|
const lockPath = getSkillLockPath(options);
|
|
@@ -1932,7 +2144,7 @@ function AddInstallScreen() {
|
|
|
1932
2144
|
}
|
|
1933
2145
|
|
|
1934
2146
|
// src/tui/screens/AddMode.tsx
|
|
1935
|
-
import { join as
|
|
2147
|
+
import { join as join10 } from "path";
|
|
1936
2148
|
import { Box as Box11 } from "ink";
|
|
1937
2149
|
import React7 from "react";
|
|
1938
2150
|
|
|
@@ -2003,7 +2215,7 @@ function AddModeScreen() {
|
|
|
2003
2215
|
const targetAgents = addSkill.targetAgents ?? [];
|
|
2004
2216
|
const agentPaths = targetAgents.length > 0 ? targetAgents.map((agent) => {
|
|
2005
2217
|
const config = agents[agent];
|
|
2006
|
-
const base = addSkill.installGlobally ? config.globalSkillsDir :
|
|
2218
|
+
const base = addSkill.installGlobally ? config.globalSkillsDir : join10(cwd, config.skillsDir);
|
|
2007
2219
|
return shortenPath(base, cwd);
|
|
2008
2220
|
}) : [];
|
|
2009
2221
|
const copyHint = agentPaths.length > 0 ? `Copy into: ${formatList(agentPaths, 3)}` : "Copy: duplicate into each agent folder";
|
|
@@ -2150,10 +2362,10 @@ function AddScopeScreen() {
|
|
|
2150
2362
|
}
|
|
2151
2363
|
|
|
2152
2364
|
// src/tui/screens/AddSkillSelect.tsx
|
|
2153
|
-
import { existsSync as
|
|
2154
|
-
import { mkdir as mkdir4, mkdtemp as
|
|
2155
|
-
import { tmpdir as
|
|
2156
|
-
import { basename as basename3, join as
|
|
2365
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2366
|
+
import { mkdir as mkdir4, mkdtemp as mkdtemp3, writeFile as writeFile3 } from "fs/promises";
|
|
2367
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
2368
|
+
import { basename as basename3, join as join12 } from "path";
|
|
2157
2369
|
import { Box as Box15, Text as Text13 } from "ink";
|
|
2158
2370
|
import React11 from "react";
|
|
2159
2371
|
|
|
@@ -2467,9 +2679,9 @@ async function resolveRemoteSkill(url) {
|
|
|
2467
2679
|
}
|
|
2468
2680
|
|
|
2469
2681
|
// src/marketplace.ts
|
|
2470
|
-
import { existsSync as
|
|
2682
|
+
import { existsSync as existsSync4, statSync } from "fs";
|
|
2471
2683
|
import { readFile as readFile3 } from "fs/promises";
|
|
2472
|
-
import { dirname as dirname3, join as
|
|
2684
|
+
import { dirname as dirname3, join as join11, posix } from "path";
|
|
2473
2685
|
function toRecord(value) {
|
|
2474
2686
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2475
2687
|
return value;
|
|
@@ -2534,14 +2746,14 @@ function isMarketplaceInput(input) {
|
|
|
2534
2746
|
return input.toLowerCase().endsWith("marketplace.json");
|
|
2535
2747
|
}
|
|
2536
2748
|
function resolveLocalMarketplacePath(input) {
|
|
2537
|
-
if (!
|
|
2749
|
+
if (!existsSync4(input)) return null;
|
|
2538
2750
|
const stats = statSync(input);
|
|
2539
2751
|
if (stats.isFile() && input.toLowerCase().endsWith("marketplace.json")) {
|
|
2540
2752
|
return input;
|
|
2541
2753
|
}
|
|
2542
2754
|
if (stats.isDirectory()) {
|
|
2543
|
-
const candidate =
|
|
2544
|
-
if (
|
|
2755
|
+
const candidate = join11(input, ".claude-plugin", "marketplace.json");
|
|
2756
|
+
if (existsSync4(candidate)) return candidate;
|
|
2545
2757
|
}
|
|
2546
2758
|
return null;
|
|
2547
2759
|
}
|
|
@@ -2669,8 +2881,8 @@ function resolvePluginSource(plugin, context) {
|
|
|
2669
2881
|
const src = plugin.source;
|
|
2670
2882
|
if (typeof src === "string") {
|
|
2671
2883
|
if (context.kind === "local" && context.baseDir) {
|
|
2672
|
-
const base =
|
|
2673
|
-
return { kind: "local", localDir:
|
|
2884
|
+
const base = join11(context.baseDir, pluginRoot);
|
|
2885
|
+
return { kind: "local", localDir: join11(base, src), overrides };
|
|
2674
2886
|
}
|
|
2675
2887
|
if (context.kind === "github" && context.gh) {
|
|
2676
2888
|
const basePath = posix.join(context.gh.basePath || "", pluginRoot || "");
|
|
@@ -2901,11 +3113,11 @@ function AddSkillSelectScreen() {
|
|
|
2901
3113
|
if (!resolved) {
|
|
2902
3114
|
throw new Error("Unable to fetch SKILL.md from that URL.");
|
|
2903
3115
|
}
|
|
2904
|
-
const tempDir2 = await
|
|
3116
|
+
const tempDir2 = await mkdtemp3(join12(tmpdir4(), "playbooks-skill-"));
|
|
2905
3117
|
registerTempDir(tempDir2);
|
|
2906
3118
|
tempDirForCleanup = tempDir2;
|
|
2907
3119
|
await mkdir4(tempDir2, { recursive: true });
|
|
2908
|
-
await writeFile3(
|
|
3120
|
+
await writeFile3(join12(tempDir2, "SKILL.md"), resolved.remoteSkill.content, "utf-8");
|
|
2909
3121
|
const skill = {
|
|
2910
3122
|
name: resolved.remoteSkill.installName,
|
|
2911
3123
|
description: resolved.remoteSkill.description,
|
|
@@ -2938,7 +3150,7 @@ function AddSkillSelectScreen() {
|
|
|
2938
3150
|
throw new Error("Local path is missing.");
|
|
2939
3151
|
}
|
|
2940
3152
|
skillsDir = parsed.localPath;
|
|
2941
|
-
if (!
|
|
3153
|
+
if (!existsSync5(skillsDir)) {
|
|
2942
3154
|
throw new Error(`Local path does not exist: ${skillsDir}`);
|
|
2943
3155
|
}
|
|
2944
3156
|
} else {
|
|
@@ -3321,220 +3533,6 @@ function AddTargetsScreen() {
|
|
|
3321
3533
|
// src/tui/screens/FindSkillResults.tsx
|
|
3322
3534
|
import { Box as Box18, Text as Text16 } from "ink";
|
|
3323
3535
|
import React15 from "react";
|
|
3324
|
-
|
|
3325
|
-
// src/flows/find-skill.ts
|
|
3326
|
-
import { existsSync as existsSync5 } from "fs";
|
|
3327
|
-
import { mkdtemp as mkdtemp3 } from "fs/promises";
|
|
3328
|
-
import { tmpdir as tmpdir4 } from "os";
|
|
3329
|
-
import { join as join12 } from "path";
|
|
3330
|
-
|
|
3331
|
-
// src/playbooks-api.ts
|
|
3332
|
-
var API_BASE2 = process.env.PLAYBOOKS_API_URL?.trim() || "https://playbooks.com/api";
|
|
3333
|
-
var USER_AGENT = "playbooks-cli";
|
|
3334
|
-
async function searchSkills(query, mode, limit = 10) {
|
|
3335
|
-
const url = new URL(`${API_BASE2}/skills`);
|
|
3336
|
-
url.searchParams.set("search", query);
|
|
3337
|
-
url.searchParams.set("limit", String(limit));
|
|
3338
|
-
url.searchParams.set("mode", mode);
|
|
3339
|
-
const response = await fetch(url.toString(), {
|
|
3340
|
-
headers: {
|
|
3341
|
-
"User-Agent": USER_AGENT
|
|
3342
|
-
}
|
|
3343
|
-
});
|
|
3344
|
-
let payload = null;
|
|
3345
|
-
try {
|
|
3346
|
-
payload = await response.json();
|
|
3347
|
-
} catch {
|
|
3348
|
-
payload = null;
|
|
3349
|
-
}
|
|
3350
|
-
if (!response.ok || !payload?.success) {
|
|
3351
|
-
const message = payload?.error || `Search failed (${response.status})`;
|
|
3352
|
-
throw new Error(message);
|
|
3353
|
-
}
|
|
3354
|
-
return Array.isArray(payload.data) ? payload.data : [];
|
|
3355
|
-
}
|
|
3356
|
-
async function requestUrlMarkdown(url) {
|
|
3357
|
-
const endpoint = new URL(`${API_BASE2}/url`);
|
|
3358
|
-
const response = await fetch(endpoint.toString(), {
|
|
3359
|
-
method: "POST",
|
|
3360
|
-
headers: {
|
|
3361
|
-
"User-Agent": USER_AGENT,
|
|
3362
|
-
"Content-Type": "application/json"
|
|
3363
|
-
},
|
|
3364
|
-
body: JSON.stringify({ url })
|
|
3365
|
-
});
|
|
3366
|
-
let payload = null;
|
|
3367
|
-
try {
|
|
3368
|
-
payload = await response.json();
|
|
3369
|
-
} catch {
|
|
3370
|
-
payload = null;
|
|
3371
|
-
}
|
|
3372
|
-
if (!response.ok && response.status !== 202) {
|
|
3373
|
-
const message = payload?.error || `Request failed (${response.status})`;
|
|
3374
|
-
throw new Error(message);
|
|
3375
|
-
}
|
|
3376
|
-
return payload ?? { success: false, error: `Request failed (${response.status})` };
|
|
3377
|
-
}
|
|
3378
|
-
async function pollUrlMarkdown(jobId, timeoutMs = 6e4, pollIntervalMs = 1e3) {
|
|
3379
|
-
const endpoint = new URL(`${API_BASE2}/url`);
|
|
3380
|
-
endpoint.searchParams.set("jobId", jobId);
|
|
3381
|
-
const deadline = Date.now() + timeoutMs;
|
|
3382
|
-
while (Date.now() < deadline) {
|
|
3383
|
-
const response = await fetch(endpoint.toString(), {
|
|
3384
|
-
headers: {
|
|
3385
|
-
"User-Agent": USER_AGENT
|
|
3386
|
-
}
|
|
3387
|
-
});
|
|
3388
|
-
let payload = null;
|
|
3389
|
-
try {
|
|
3390
|
-
payload = await response.json();
|
|
3391
|
-
} catch {
|
|
3392
|
-
payload = null;
|
|
3393
|
-
}
|
|
3394
|
-
if (payload?.success && payload.data) {
|
|
3395
|
-
return payload.data;
|
|
3396
|
-
}
|
|
3397
|
-
if (payload?.success && payload.pending) {
|
|
3398
|
-
await new Promise((resolve5) => setTimeout(resolve5, pollIntervalMs));
|
|
3399
|
-
continue;
|
|
3400
|
-
}
|
|
3401
|
-
const message = payload?.error || `Request failed (${response.status})`;
|
|
3402
|
-
throw new Error(message);
|
|
3403
|
-
}
|
|
3404
|
-
throw new Error("Timed out waiting for markdown");
|
|
3405
|
-
}
|
|
3406
|
-
async function fetchUrlMarkdown(url) {
|
|
3407
|
-
const response = await requestUrlMarkdown(url);
|
|
3408
|
-
if (response.success && response.data) {
|
|
3409
|
-
return response.data;
|
|
3410
|
-
}
|
|
3411
|
-
if (response.jobId) {
|
|
3412
|
-
return await pollUrlMarkdown(response.jobId);
|
|
3413
|
-
}
|
|
3414
|
-
const message = response.error || "Failed to fetch markdown";
|
|
3415
|
-
throw new Error(message);
|
|
3416
|
-
}
|
|
3417
|
-
|
|
3418
|
-
// src/flows/find-skill.ts
|
|
3419
|
-
async function searchSkillDirectory(query, mode, limit = 10) {
|
|
3420
|
-
const trimmed = query.trim();
|
|
3421
|
-
if (!trimmed) {
|
|
3422
|
-
return { mode, results: [], fallback: false };
|
|
3423
|
-
}
|
|
3424
|
-
if (mode === "semantic") {
|
|
3425
|
-
try {
|
|
3426
|
-
const results2 = await searchSkills(trimmed, "semantic", limit);
|
|
3427
|
-
return { mode: "semantic", results: results2, fallback: false };
|
|
3428
|
-
} catch {
|
|
3429
|
-
const results2 = await searchSkills(trimmed, "lexical", limit);
|
|
3430
|
-
return { mode: "lexical", results: results2, fallback: true };
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
const results = await searchSkills(trimmed, "lexical", limit);
|
|
3434
|
-
return { mode: "lexical", results, fallback: false };
|
|
3435
|
-
}
|
|
3436
|
-
var normalizeSkillPath = (value) => value.replace(/^\/+/, "").replace(/\\/g, "/");
|
|
3437
|
-
var toSkillDir = (skillPath) => {
|
|
3438
|
-
const normalized = normalizeSkillPath(skillPath);
|
|
3439
|
-
const cleaned = normalized.replace(/\/?SKILL\.md$/i, "").replace(/\/+$/, "");
|
|
3440
|
-
return cleaned;
|
|
3441
|
-
};
|
|
3442
|
-
var ensureSkillMdPath = (skillPath) => {
|
|
3443
|
-
const normalized = normalizeSkillPath(skillPath);
|
|
3444
|
-
if (/\/?SKILL\.md$/i.test(normalized)) {
|
|
3445
|
-
return normalized;
|
|
3446
|
-
}
|
|
3447
|
-
if (!normalized) {
|
|
3448
|
-
return "SKILL.md";
|
|
3449
|
-
}
|
|
3450
|
-
return `${normalized.replace(/\/+$/, "")}/SKILL.md`;
|
|
3451
|
-
};
|
|
3452
|
-
var sanitizeRepoDir = (owner, repo) => `${owner}-${repo}`.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
3453
|
-
async function prepareSkillsFromSearchResults(selected) {
|
|
3454
|
-
if (selected.length === 0) {
|
|
3455
|
-
throw new Error("Select at least one skill to install.");
|
|
3456
|
-
}
|
|
3457
|
-
const tempDir = await mkdtemp3(join12(tmpdir4(), "playbooks-search-"));
|
|
3458
|
-
registerTempDir(tempDir);
|
|
3459
|
-
try {
|
|
3460
|
-
const repoMap = /* @__PURE__ */ new Map();
|
|
3461
|
-
for (const result of selected) {
|
|
3462
|
-
if (!result.repoOwner || !result.repoName || !result.path) {
|
|
3463
|
-
throw new Error(`Missing repository data for ${result.name}.`);
|
|
3464
|
-
}
|
|
3465
|
-
const key = `${result.repoOwner.toLowerCase()}/${result.repoName.toLowerCase()}`;
|
|
3466
|
-
const existing = repoMap.get(key);
|
|
3467
|
-
if (existing) {
|
|
3468
|
-
existing.entries.push(result);
|
|
3469
|
-
} else {
|
|
3470
|
-
repoMap.set(key, {
|
|
3471
|
-
owner: result.repoOwner,
|
|
3472
|
-
repo: result.repoName,
|
|
3473
|
-
repoUrl: `https://github.com/${result.repoOwner}/${result.repoName}.git`,
|
|
3474
|
-
entries: [result]
|
|
3475
|
-
});
|
|
3476
|
-
}
|
|
3477
|
-
}
|
|
3478
|
-
const repoDirs = /* @__PURE__ */ new Map();
|
|
3479
|
-
const usedDirs = /* @__PURE__ */ new Set();
|
|
3480
|
-
for (const [key, repoInfo] of repoMap) {
|
|
3481
|
-
let dirName = sanitizeRepoDir(repoInfo.owner, repoInfo.repo);
|
|
3482
|
-
let suffix = 1;
|
|
3483
|
-
while (usedDirs.has(dirName)) {
|
|
3484
|
-
dirName = `${sanitizeRepoDir(repoInfo.owner, repoInfo.repo)}-${suffix}`;
|
|
3485
|
-
suffix += 1;
|
|
3486
|
-
}
|
|
3487
|
-
usedDirs.add(dirName);
|
|
3488
|
-
const repoDir = join12(tempDir, dirName);
|
|
3489
|
-
await cloneRepoTo(repoInfo.repoUrl, repoDir);
|
|
3490
|
-
repoDirs.set(key, repoDir);
|
|
3491
|
-
}
|
|
3492
|
-
const skills = [];
|
|
3493
|
-
const originBySkillName = /* @__PURE__ */ new Map();
|
|
3494
|
-
for (const result of selected) {
|
|
3495
|
-
if (!result.repoOwner || !result.repoName || !result.path) {
|
|
3496
|
-
continue;
|
|
3497
|
-
}
|
|
3498
|
-
const repoKey = `${result.repoOwner.toLowerCase()}/${result.repoName.toLowerCase()}`;
|
|
3499
|
-
const repoDir = repoDirs.get(repoKey);
|
|
3500
|
-
if (!repoDir) {
|
|
3501
|
-
throw new Error(`Missing clone for ${result.repoOwner}/${result.repoName}.`);
|
|
3502
|
-
}
|
|
3503
|
-
const skillDir = toSkillDir(result.path);
|
|
3504
|
-
const subpath = skillDir ? skillDir : void 0;
|
|
3505
|
-
const discovered = await discoverSkills(repoDir, subpath);
|
|
3506
|
-
if (discovered.length === 0) {
|
|
3507
|
-
throw new Error(`Skill not found in ${result.repoOwner}/${result.repoName}.`);
|
|
3508
|
-
}
|
|
3509
|
-
const expectedPath = join12(repoDir, skillDir);
|
|
3510
|
-
const fallbackSkill = discovered[0];
|
|
3511
|
-
if (!fallbackSkill) {
|
|
3512
|
-
throw new Error(`Skill not found in ${result.repoOwner}/${result.repoName}.`);
|
|
3513
|
-
}
|
|
3514
|
-
const skill = discovered.find((entry) => entry.path === expectedPath) ?? fallbackSkill;
|
|
3515
|
-
if (!existsSync5(join12(skill.path, "SKILL.md"))) {
|
|
3516
|
-
throw new Error(`SKILL.md missing for ${result.name}.`);
|
|
3517
|
-
}
|
|
3518
|
-
skills.push(skill);
|
|
3519
|
-
const displayName = getSkillDisplayName(skill);
|
|
3520
|
-
originBySkillName.set(displayName, {
|
|
3521
|
-
sourceType: "github",
|
|
3522
|
-
source: `${result.repoOwner}/${result.repoName}`,
|
|
3523
|
-
sourceUrl: `https://github.com/${result.repoOwner}/${result.repoName}.git`,
|
|
3524
|
-
skillPath: ensureSkillMdPath(result.path)
|
|
3525
|
-
});
|
|
3526
|
-
}
|
|
3527
|
-
return { tempDir, skills, originBySkillName };
|
|
3528
|
-
} catch (error) {
|
|
3529
|
-
try {
|
|
3530
|
-
await cleanupTempDir(tempDir);
|
|
3531
|
-
} catch {
|
|
3532
|
-
}
|
|
3533
|
-
throw error;
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
|
-
|
|
3537
|
-
// src/tui/screens/FindSkillResults.tsx
|
|
3538
3536
|
import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
3539
3537
|
var formatStars = (value) => {
|
|
3540
3538
|
if (!value || value <= 0) return "";
|
|
@@ -5148,6 +5146,7 @@ var version = package_default.version;
|
|
|
5148
5146
|
setVersion(version);
|
|
5149
5147
|
setupTempDirCleanup();
|
|
5150
5148
|
program.name("playbooks").description("Playbooks CLI").version(version);
|
|
5149
|
+
program.addHelpCommand();
|
|
5151
5150
|
var applyAddSkillOptions = (cmd) => cmd.option("-g, --global", "Install globally (user-level) instead of project-level").option(
|
|
5152
5151
|
"-a, --agent <agents...>",
|
|
5153
5152
|
"Target agents to install to (claude-code, codex, cursor, opencode, and more)"
|
|
@@ -5170,6 +5169,45 @@ async function launch(invocation, initialScreen) {
|
|
|
5170
5169
|
function initialAddSkillScreen(source) {
|
|
5171
5170
|
return source ? "add-skill-select" : "add-source";
|
|
5172
5171
|
}
|
|
5172
|
+
function formatAgentListMarkdown() {
|
|
5173
|
+
const entries = Object.values(agents).map((agent) => ({ name: agent.name, displayName: agent.displayName })).sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
5174
|
+
const lines = ["# Supported agents", ""];
|
|
5175
|
+
for (const entry of entries) {
|
|
5176
|
+
lines.push(`- ${entry.displayName} (\`${entry.name}\`)`);
|
|
5177
|
+
}
|
|
5178
|
+
return lines.join("\n");
|
|
5179
|
+
}
|
|
5180
|
+
function formatFindSkillMarkdown(query, outcome) {
|
|
5181
|
+
const lines = [`# Skill search results for "${query}"`];
|
|
5182
|
+
lines.push("Ordered by best match; official sources recommended.");
|
|
5183
|
+
if (outcome.fallback) {
|
|
5184
|
+
lines.push("_Note: semantic search unavailable. Showing fast results._");
|
|
5185
|
+
}
|
|
5186
|
+
if (outcome.results.length === 0) {
|
|
5187
|
+
lines.push("", "_No results._");
|
|
5188
|
+
return lines.join("\n");
|
|
5189
|
+
}
|
|
5190
|
+
lines.push("");
|
|
5191
|
+
for (const result of outcome.results.slice(0, 10)) {
|
|
5192
|
+
const description = result.shortDescription ?? result.description ?? "";
|
|
5193
|
+
const repo = result.repoOwner && result.repoName ? `${result.repoOwner}/${result.repoName}` : null;
|
|
5194
|
+
const skillName = result.skillSlug ?? result.name;
|
|
5195
|
+
if (!repo || !skillName) {
|
|
5196
|
+
continue;
|
|
5197
|
+
}
|
|
5198
|
+
const tag = result.isOfficial ? "[official]" : "[community]";
|
|
5199
|
+
lines.push(`- ${tag} npx playbooks add skill ${repo} --skill ${skillName}`);
|
|
5200
|
+
if (description) {
|
|
5201
|
+
lines.push(` ${truncateLine(description, 140)}`);
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
return lines.join("\n");
|
|
5205
|
+
}
|
|
5206
|
+
function truncateLine(value, maxLength) {
|
|
5207
|
+
if (value.length <= maxLength) return value;
|
|
5208
|
+
const sliced = value.slice(0, Math.max(0, maxLength - 1)).trimEnd();
|
|
5209
|
+
return sliced ? `${sliced}\u2026` : value.slice(0, maxLength);
|
|
5210
|
+
}
|
|
5173
5211
|
applyAddSkillOptions(
|
|
5174
5212
|
program.command("add-skill [source]", { hidden: true }).description("Legacy: use playbooks add skill").action(async (source, options) => {
|
|
5175
5213
|
await launch({ intent: "add-skill", source, options }, initialAddSkillScreen(source));
|
|
@@ -5185,6 +5223,9 @@ var listCmd = program.command("list").description("List installed resources");
|
|
|
5185
5223
|
listCmd.command("skill").description("List installed skills").action(async () => {
|
|
5186
5224
|
await launch({ intent: "list", options: {} }, "list");
|
|
5187
5225
|
});
|
|
5226
|
+
listCmd.command("agents").description("List supported agents").action(() => {
|
|
5227
|
+
console.log(formatAgentListMarkdown());
|
|
5228
|
+
});
|
|
5188
5229
|
var manageCmd = program.command("manage").description("Remove installed resources");
|
|
5189
5230
|
manageCmd.command("skill").description("Remove installed skills").action(async () => {
|
|
5190
5231
|
await launch({ intent: "manage", options: {} }, "manage");
|
|
@@ -5217,8 +5258,20 @@ skillCmd.action(async (source, options) => {
|
|
|
5217
5258
|
);
|
|
5218
5259
|
});
|
|
5219
5260
|
var findCmd = program.command("find").description("Search the playbooks directory");
|
|
5220
|
-
findCmd.command("skill").description("Find skills").action(async () => {
|
|
5221
|
-
|
|
5261
|
+
findCmd.command("skill [query]").description("Find skills").option("--semantic", "Use semantic search (falls back to fast search)").action(async (query, options) => {
|
|
5262
|
+
if (!query) {
|
|
5263
|
+
await launch({ intent: "find-skill", options: {} }, "find-skill-search");
|
|
5264
|
+
return;
|
|
5265
|
+
}
|
|
5266
|
+
const mode = options.semantic ? "semantic" : "lexical";
|
|
5267
|
+
try {
|
|
5268
|
+
const outcome = await searchSkillDirectory(query, mode, 10);
|
|
5269
|
+
console.log(formatFindSkillMarkdown(query, outcome));
|
|
5270
|
+
} catch (error) {
|
|
5271
|
+
const message = error instanceof Error ? error.message : "Search failed.";
|
|
5272
|
+
console.error(message);
|
|
5273
|
+
process.exit(1);
|
|
5274
|
+
}
|
|
5222
5275
|
});
|
|
5223
5276
|
program.command("get <url> [outKeyword] [outPath]").description("Fetch a URL as markdown").option("--json", "Output JSON metadata instead of raw markdown").action(
|
|
5224
5277
|
async (url, outKeyword, outPath, options) => {
|