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.
Files changed (2) hide show
  1. package/dist/index.js +1096 -1043
  2. 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.6",
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/flows/url-markdown-output.ts
108
- var output = null;
109
- function setUrlMarkdownOutput(next) {
110
- output = next;
111
- }
112
- function consumeUrlMarkdownOutput() {
113
- const current = output;
114
- output = null;
115
- return current;
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/telemetry.ts
119
- import { createHmac } from "crypto";
120
- var API_BASE = process.env.PLAYBOOKS_API_URL?.trim() || "https://playbooks.com/api";
121
- var TELEMETRY_URL = process.env.PLAYBOOKS_TELEMETRY_URL?.trim() || `${API_BASE}/skill/t`;
122
- var cliVersion = null;
123
- function isCI() {
124
- 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);
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 isEnabled() {
127
- return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK && !process.env.PLAYBOOKS_DISABLE_TELEMETRY;
392
+ function registerTempDir(dir) {
393
+ tempDirs.add(dir);
128
394
  }
129
- function setVersion(version2) {
130
- cliVersion = version2;
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(join(tmpdir2(), "add-skill-"));
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/tui/context/navigation.tsx
250
- import { jsx } from "react/jsx-runtime";
251
- var NavigationContext = createContext(null);
252
- function useNavigation() {
253
- const ctx = useContext(NavigationContext);
254
- if (!ctx) throw new Error("NavigationContext not found");
255
- return ctx;
256
- }
257
- function NavigationProvider({
258
- children,
259
- initialInvocation,
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
- const id = Date.now() + Math.random();
307
- setFlashes((prev) => [...prev, { id, text: msg }]);
308
- const timer = setTimeout(() => {
309
- setFlashes((prev) => prev.filter((f) => f.id !== id));
310
- flashTimersRef.current.delete(id);
311
- }, 5e3);
312
- flashTimersRef.current.set(id, timer);
313
- }, []);
314
- React.useEffect(() => {
315
- return () => {
316
- for (const timer of flashTimersRef.current.values()) {
317
- clearTimeout(timer);
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
- // src/tui/ui/BrandHeader.tsx
397
- import { Box, Text } from "ink";
398
- import React2 from "react";
399
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
400
- var TARGET_TEXT = "playbooks";
401
- var TAGLINE = "Give your agents context to make them smarter.";
402
- var TEXT_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
403
- function randomChar(chars) {
404
- const pool = Array.isArray(chars) ? chars : chars.split("");
405
- return pool[Math.floor(Math.random() * pool.length)] ?? "";
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 buildScrambleText(target, settles, progress) {
408
- const letters = target.split("");
409
- return letters.map((char, index) => {
410
- if (char === " ") return char;
411
- const settle = settles[index] ?? 1;
412
- if (progress >= settle) return char;
413
- return randomChar(TEXT_CHARS);
414
- }).join("");
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
- }, interval);
447
- return () => {
448
- clearInterval(timer);
449
- };
450
- }, []);
451
- return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [
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
- "claude-code": {
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
- clawdbot: {
534
- name: "clawdbot",
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
- cline: {
543
- name: "cline",
544
- displayName: "Cline",
545
- skillsDir: ".cline/skills",
546
- globalSkillsDir: join2(home, ".cline/skills"),
547
- detectInstalled: async () => {
548
- return existsSync(join2(home, ".cline"));
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
- codex: {
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
- "command-code": {
561
- name: "command-code",
562
- displayName: "Command Code",
563
- skillsDir: ".commandcode/skills",
564
- globalSkillsDir: join2(home, ".commandcode/skills"),
565
- detectInstalled: async () => {
566
- return existsSync(join2(home, ".commandcode"));
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
- continue: {
570
- name: "continue",
571
- displayName: "Continue",
572
- skillsDir: ".continue/skills",
573
- globalSkillsDir: join2(home, ".continue/skills"),
574
- detectInstalled: async () => {
575
- return existsSync(join2(process.cwd(), ".continue")) || existsSync(join2(home, ".continue"));
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
- crush: {
579
- name: "crush",
580
- displayName: "Crush",
581
- skillsDir: ".crush/skills",
582
- globalSkillsDir: join2(home, ".config/crush/skills"),
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
- cursor: {
588
- name: "cursor",
589
- displayName: "Cursor",
590
- skillsDir: ".cursor/skills",
591
- globalSkillsDir: join2(home, ".cursor/skills"),
592
- detectInstalled: async () => {
593
- return existsSync(join2(home, ".cursor"));
594
- }
595
- },
596
- droid: {
597
- name: "droid",
598
- displayName: "Droid",
599
- skillsDir: ".factory/skills",
600
- globalSkillsDir: join2(home, ".factory/skills"),
601
- detectInstalled: async () => {
602
- return existsSync(join2(home, ".factory"));
603
- }
604
- },
605
- "gemini-cli": {
606
- name: "gemini-cli",
607
- displayName: "Gemini CLI",
608
- skillsDir: ".gemini/skills",
609
- globalSkillsDir: join2(home, ".gemini/skills"),
610
- detectInstalled: async () => {
611
- return existsSync(join2(home, ".gemini"));
612
- }
613
- },
614
- "github-copilot": {
615
- name: "github-copilot",
616
- displayName: "GitHub Copilot",
617
- skillsDir: ".github/skills",
618
- globalSkillsDir: join2(home, ".copilot/skills"),
619
- detectInstalled: async () => {
620
- return existsSync(join2(process.cwd(), ".github")) || existsSync(join2(home, ".copilot"));
621
- }
622
- },
623
- goose: {
624
- name: "goose",
625
- displayName: "Goose",
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
- "kiro-cli": {
642
- name: "kiro-cli",
643
- displayName: "Kiro CLI",
644
- skillsDir: ".kiro/skills",
645
- globalSkillsDir: join2(home, ".kiro/skills"),
646
- detectInstalled: async () => {
647
- return existsSync(join2(home, ".kiro"));
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
- mcpjam: {
651
- name: "mcpjam",
652
- displayName: "MCPJam",
653
- skillsDir: ".mcpjam/skills",
654
- globalSkillsDir: join2(home, ".mcpjam/skills"),
655
- detectInstalled: async () => {
656
- return existsSync(join2(home, ".mcpjam"));
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
- opencode: {
660
- name: "opencode",
661
- displayName: "OpenCode",
662
- skillsDir: ".opencode/skills",
663
- globalSkillsDir: join2(home, ".config/opencode/skills"),
664
- detectInstalled: async () => {
665
- return existsSync(join2(home, ".config/opencode")) || existsSync(join2(home, ".claude/skills"));
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
- openhands: {
669
- name: "openhands",
670
- displayName: "OpenHands",
671
- skillsDir: ".openhands/skills",
672
- globalSkillsDir: join2(home, ".openhands/skills"),
673
- detectInstalled: async () => {
674
- return existsSync(join2(home, ".openhands"));
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
- pi: {
678
- name: "pi",
679
- displayName: "Pi",
680
- skillsDir: ".pi/skills",
681
- globalSkillsDir: join2(home, ".pi/agent/skills"),
682
- detectInstalled: async () => {
683
- return existsSync(join2(home, ".pi/agent"));
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
- qoder: {
687
- name: "qoder",
688
- displayName: "Qoder",
689
- skillsDir: ".qoder/skills",
690
- globalSkillsDir: join2(home, ".qoder/skills"),
691
- detectInstalled: async () => {
692
- return existsSync(join2(home, ".qoder"));
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
- "qwen-code": {
696
- name: "qwen-code",
697
- displayName: "Qwen Code",
698
- skillsDir: ".qwen/skills",
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
- roo: {
705
- name: "roo",
706
- displayName: "Roo Code",
707
- skillsDir: ".roo/skills",
708
- globalSkillsDir: join2(home, ".roo/skills"),
709
- detectInstalled: async () => {
710
- return existsSync(join2(home, ".roo"));
711
- }
712
- },
713
- trae: {
714
- name: "trae",
715
- displayName: "Trae",
716
- skillsDir: ".trae/skills",
717
- globalSkillsDir: join2(home, ".trae/skills"),
718
- detectInstalled: async () => {
719
- return existsSync(join2(home, ".trae"));
720
- }
721
- },
722
- windsurf: {
723
- name: "windsurf",
724
- displayName: "Windsurf",
725
- skillsDir: ".windsurf/skills",
726
- globalSkillsDir: join2(home, ".codeium/windsurf/skills"),
727
- detectInstalled: async () => {
728
- return existsSync(join2(home, ".codeium/windsurf"));
729
- }
730
- },
731
- zencoder: {
732
- name: "zencoder",
733
- displayName: "Zencoder",
734
- skillsDir: ".zencoder/skills",
735
- globalSkillsDir: join2(home, ".zencoder/skills"),
736
- detectInstalled: async () => {
737
- return existsSync(join2(home, ".zencoder"));
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
- neovate: {
741
- name: "neovate",
742
- displayName: "Neovate",
743
- skillsDir: ".neovate/skills",
744
- globalSkillsDir: join2(home, ".neovate/skills"),
745
- detectInstalled: async () => {
746
- return existsSync(join2(home, ".neovate"));
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
- async function detectInstalledAgents() {
751
- const installed = [];
752
- for (const [type, config] of Object.entries(agents)) {
753
- if (await config.detectInstalled()) {
754
- installed.push(type);
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 join5 } from "path";
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 join3, relative, resolve as resolve2 } from "path";
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 = join3(linkPath, "..");
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 readdir(src, { withFileTypes: true });
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 = join3(src, entry.name);
837
- const destPath = join3(dest, entry.name);
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 join4, normalize as normalize2, resolve as resolve3, sep as sep2 } from "path";
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 join4(baseDir, AGENTS_DIR, SKILLS_SUBDIR);
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 = join4(canonicalBase, sanitized);
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 = join4(canonicalBase, skillName);
894
- const agentBase = isGlobal ? agent.globalSkillsDir : join4(cwd, agent.skillsDir);
895
- const agentDir = join4(agentBase, skillName);
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 || basename(skill.path);
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 getSkillDisplayName(skill) {
1175
- return skill.name || basename2(skill.path);
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 : join7(cwd, config.skillsDir);
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 join8 } from "path";
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 join8(baseDir, AGENTS_DIR2, LOCK_FILE);
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 join9 } from "path";
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 : join9(cwd, config.skillsDir);
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 existsSync4 } from "fs";
2154
- import { mkdir as mkdir4, mkdtemp as mkdtemp2, writeFile as writeFile3 } from "fs/promises";
2155
- import { tmpdir as tmpdir3 } from "os";
2156
- import { basename as basename3, join as join11 } from "path";
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 existsSync3, statSync } from "fs";
2682
+ import { existsSync as existsSync4, statSync } from "fs";
2471
2683
  import { readFile as readFile3 } from "fs/promises";
2472
- import { dirname as dirname3, join as join10, posix } from "path";
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 (!existsSync3(input)) return null;
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 = join10(input, ".claude-plugin", "marketplace.json");
2544
- if (existsSync3(candidate)) return candidate;
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 = join10(context.baseDir, pluginRoot);
2673
- return { kind: "local", localDir: join10(base, src), overrides };
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 mkdtemp2(join11(tmpdir3(), "playbooks-skill-"));
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(join11(tempDir2, "SKILL.md"), resolved.remoteSkill.content, "utf-8");
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 (!existsSync4(skillsDir)) {
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
- await launch({ intent: "find-skill", options: {} }, "find-skill-search");
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) => {