claude-code-tutor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +315 -0
  2. package/dist/chunk-A6ZZXTHB.js +65 -0
  3. package/dist/chunk-OYDUGORC.js +75 -0
  4. package/dist/cli.js +1362 -0
  5. package/dist/meta-TZOSLFST.js +13 -0
  6. package/dist/tutor-memory-M6GNB3LQ.js +19 -0
  7. package/package.json +61 -0
  8. package/skills-fallback/cc-agent-teams/SKILL.md +38 -0
  9. package/skills-fallback/cc-best-practices/SKILL.md +34 -0
  10. package/skills-fallback/cc-changelog/SKILL.md +20 -0
  11. package/skills-fallback/cc-channels/SKILL.md +34 -0
  12. package/skills-fallback/cc-cli-reference/SKILL.md +16 -0
  13. package/skills-fallback/cc-commands/SKILL.md +12 -0
  14. package/skills-fallback/cc-common-workflows-ask-claude-about-its-capabilities/SKILL.md +43 -0
  15. package/skills-fallback/cc-common-workflows-create-pull-requests/SKILL.md +26 -0
  16. package/skills-fallback/cc-common-workflows-fix-bugs-efficiently/SKILL.md +30 -0
  17. package/skills-fallback/cc-common-workflows-get-notified-when-claude-needs-your-attention/SKILL.md +95 -0
  18. package/skills-fallback/cc-common-workflows-handle-documentation/SKILL.md +36 -0
  19. package/skills-fallback/cc-common-workflows-next-steps/SKILL.md +22 -0
  20. package/skills-fallback/cc-common-workflows-refactor-code/SKILL.md +36 -0
  21. package/skills-fallback/cc-common-workflows-reference-files-and-directories/SKILL.md +47 -0
  22. package/skills-fallback/cc-common-workflows-resume-previous-conversations/SKILL.md +57 -0
  23. package/skills-fallback/cc-common-workflows-run-claude-on-a-schedule/SKILL.md +22 -0
  24. package/skills-fallback/cc-common-workflows-run-parallel-claude-code-sessions-with-git-worktrees/SKILL.md +43 -0
  25. package/skills-fallback/cc-common-workflows-understand-new-codebases/SKILL.md +81 -0
  26. package/skills-fallback/cc-common-workflows-use-claude-as-a-unix-style-utility/SKILL.md +83 -0
  27. package/skills-fallback/cc-common-workflows-use-extended-thinking-thinking-mode/SKILL.md +15 -0
  28. package/skills-fallback/cc-common-workflows-use-plan-mode-for-safe-code-analysis/SKILL.md +80 -0
  29. package/skills-fallback/cc-common-workflows-use-specialized-subagents/SKILL.md +69 -0
  30. package/skills-fallback/cc-common-workflows-work-with-images/SKILL.md +71 -0
  31. package/skills-fallback/cc-common-workflows-work-with-tests/SKILL.md +36 -0
  32. package/skills-fallback/cc-github-actions/SKILL.md +44 -0
  33. package/skills-fallback/cc-headless/SKILL.md +40 -0
  34. package/skills-fallback/cc-hooks-config/SKILL.md +32 -0
  35. package/skills-fallback/cc-hooks-events/SKILL.md +24 -0
  36. package/skills-fallback/cc-hooks-guide/SKILL.md +59 -0
  37. package/skills-fallback/cc-hooks-io/SKILL.md +11 -0
  38. package/skills-fallback/cc-learning-path/SKILL.md +77 -0
  39. package/skills-fallback/cc-mcp-add-mcp-servers-from-json-configuration/SKILL.md +36 -0
  40. package/skills-fallback/cc-mcp-authenticate-with-remote-mcp-servers/SKILL.md +52 -0
  41. package/skills-fallback/cc-mcp-import-mcp-servers-from-claude-desktop/SKILL.md +28 -0
  42. package/skills-fallback/cc-mcp-installing-mcp-servers/SKILL.md +86 -0
  43. package/skills-fallback/cc-mcp-managed-mcp-configuration/SKILL.md +55 -0
  44. package/skills-fallback/cc-mcp-mcp-installation-scopes/SKILL.md +57 -0
  45. package/skills-fallback/cc-mcp-mcp-output-limits-and-warnings/SKILL.md +46 -0
  46. package/skills-fallback/cc-mcp-popular-mcp-servers/SKILL.md +11 -0
  47. package/skills-fallback/cc-mcp-practical-examples/SKILL.md +102 -0
  48. package/skills-fallback/cc-mcp-respond-to-mcp-elicitation-requests/SKILL.md +20 -0
  49. package/skills-fallback/cc-mcp-scale-with-mcp-tool-search/SKILL.md +29 -0
  50. package/skills-fallback/cc-mcp-use-claude-code-as-an-mcp-server/SKILL.md +59 -0
  51. package/skills-fallback/cc-mcp-use-mcp-prompts-as-commands/SKILL.md +40 -0
  52. package/skills-fallback/cc-mcp-use-mcp-resources/SKILL.md +45 -0
  53. package/skills-fallback/cc-mcp-use-mcp-servers-from-claude-ai/SKILL.md +35 -0
  54. package/skills-fallback/cc-mcp-what-you-can-do-with-mcp/SKILL.md +12 -0
  55. package/skills-fallback/cc-memory/SKILL.md +28 -0
  56. package/skills-fallback/cc-model-config/SKILL.md +27 -0
  57. package/skills-fallback/cc-permissions/SKILL.md +38 -0
  58. package/skills-fallback/cc-plugins/SKILL.md +44 -0
  59. package/skills-fallback/cc-sandboxing/SKILL.md +22 -0
  60. package/skills-fallback/cc-scheduled-tasks/SKILL.md +20 -0
  61. package/skills-fallback/cc-settings-configuration-scopes/SKILL.md +57 -0
  62. package/skills-fallback/cc-settings-environment-variables/SKILL.md +13 -0
  63. package/skills-fallback/cc-settings-plugin-configuration/SKILL.md +11 -0
  64. package/skills-fallback/cc-settings-see-also/SKILL.md +13 -0
  65. package/skills-fallback/cc-settings-settings-files/SKILL.md +38 -0
  66. package/skills-fallback/cc-settings-subagent-configuration/SKILL.md +16 -0
  67. package/skills-fallback/cc-settings-tools-available-to-claude/SKILL.md +13 -0
  68. package/skills-fallback/cc-skills-guide/SKILL.md +23 -0
  69. package/skills-fallback/cc-sub-agents/SKILL.md +30 -0
  70. package/skills-fallback/cc-tutor/SKILL.md +108 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1362 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getMetaPath,
4
+ getVersion,
5
+ isStale,
6
+ readMeta,
7
+ writeMeta
8
+ } from "./chunk-OYDUGORC.js";
9
+ import {
10
+ getBaseDir,
11
+ readMemory,
12
+ updateMemory,
13
+ writeMemory
14
+ } from "./chunk-A6ZZXTHB.js";
15
+
16
+ // src/cli.ts
17
+ import { createRequire } from "module";
18
+ import { Command } from "commander";
19
+
20
+ // src/commands/init.ts
21
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
22
+ import { join as join4 } from "path";
23
+ import { createInterface } from "readline";
24
+
25
+ // src/config/skills-registry.ts
26
+ var SKILLS_REGISTRY = [
27
+ // ── STATIC SKILLS (tutor and learning path) ──────────────────
28
+ {
29
+ id: "cc-tutor",
30
+ sourceUrl: null,
31
+ name: "cc-tutor",
32
+ description: "Tutor interactivo de Claude Code. \xDAsame cuando el usuario parezca ser nuevo en Claude Code, pregunte c\xF3mo empezar, qu\xE9 puede hacer, c\xF3mo funciona, o pida que lo gu\xEDes o ense\xF1es. Tambi\xE9n respondo sobre hooks, MCP, sub-agents y features avanzadas. Detecto el idioma del usuario autom\xE1ticamente y me adapto.",
33
+ splitStrategy: "none",
34
+ tokenBudget: 900,
35
+ priority: "critical",
36
+ static: true
37
+ },
38
+ {
39
+ id: "cc-learning-path",
40
+ sourceUrl: null,
41
+ name: "cc-learning-path",
42
+ description: "Ruta de aprendizaje estructurada de Claude Code por niveles: beginner, intermediate y advanced. \xDAsame cuando el usuario quiera ver el curr\xEDculo completo, saber qu\xE9 aprender despu\xE9s, o pida un plan de estudio. Inv\xF3came con /cc-learning-path.",
43
+ splitStrategy: "none",
44
+ tokenBudget: 700,
45
+ priority: "high",
46
+ static: true
47
+ },
48
+ // ── CRITICAL (from official docs) ────────────────────────────
49
+ {
50
+ id: "cc-changelog",
51
+ sourceUrl: "https://code.claude.com/docs/en/changelog.md",
52
+ name: "cc-changelog",
53
+ description: "Changelog y novedades recientes de Claude Code. \xDAsame cuando pregunten qu\xE9 hay de nuevo, qu\xE9 cambi\xF3 en una versi\xF3n, o qu\xE9 features se agregaron recientemente.",
54
+ splitStrategy: "none",
55
+ tokenBudget: 800,
56
+ priority: "critical"
57
+ },
58
+ {
59
+ id: "cc-hooks-events",
60
+ sourceUrl: "https://code.claude.com/docs/en/hooks.md",
61
+ name: "cc-hooks-events",
62
+ description: "Tabla de todos los hook events de Claude Code: SessionStart, PreToolUse, PostToolUse, Stop, Notification, ConfigChange, FileChanged, CwdChanged, WorktreeCreate, Elicitation y m\xE1s. Cu\xE1ndo se dispara cada uno.",
63
+ splitStrategy: "manual",
64
+ manualSections: [
65
+ {
66
+ id: "cc-hooks-events",
67
+ heading: "Hook lifecycle",
68
+ description: "Tabla de eventos y cu\xE1ndo se disparan"
69
+ },
70
+ {
71
+ id: "cc-hooks-config",
72
+ heading: "Configuration",
73
+ description: "Configuraci\xF3n de hooks en settings.json, matchers, campos"
74
+ },
75
+ {
76
+ id: "cc-hooks-io",
77
+ heading: "Hook input and output",
78
+ description: "Esquemas JSON de input/output, exit codes, decisiones"
79
+ }
80
+ ],
81
+ priority: "critical"
82
+ },
83
+ {
84
+ id: "cc-hooks-guide",
85
+ sourceUrl: "https://code.claude.com/docs/en/hooks-guide.md",
86
+ name: "cc-hooks-guide",
87
+ description: "Gu\xEDa pr\xE1ctica de hooks con ejemplos: notificaciones, auto-format, bloquear archivos, re-inyectar contexto, auto-approve.",
88
+ splitStrategy: "none",
89
+ tokenBudget: 700,
90
+ priority: "critical"
91
+ },
92
+ {
93
+ id: "cc-mcp",
94
+ sourceUrl: "https://code.claude.com/docs/en/mcp.md",
95
+ name: "cc-mcp",
96
+ description: "Configuraci\xF3n de MCP servers en Claude Code: instalar, scopes (local/project/user), autenticaci\xF3n OAuth, tool search, managed MCP, recursos y prompts MCP.",
97
+ splitStrategy: "sections",
98
+ tokenBudget: 700,
99
+ priority: "critical"
100
+ },
101
+ {
102
+ id: "cc-settings",
103
+ sourceUrl: "https://code.claude.com/docs/en/settings.md",
104
+ name: "cc-settings",
105
+ description: "Todas las opciones de configuraci\xF3n de Claude Code en settings.json: permisos, hooks, plugins, sandbox, env vars, precedencia de scopes.",
106
+ splitStrategy: "sections",
107
+ tokenBudget: 700,
108
+ priority: "critical"
109
+ },
110
+ {
111
+ id: "cc-permissions",
112
+ sourceUrl: "https://code.claude.com/docs/en/permissions.md",
113
+ name: "cc-permissions",
114
+ description: "Sistema de permisos de Claude Code: sintaxis de reglas, modos (auto, plan, bypassPermissions), tool-specific rules, wildcards.",
115
+ splitStrategy: "none",
116
+ tokenBudget: 600,
117
+ priority: "critical"
118
+ },
119
+ // ── HIGH PRIORITY ─────────────────────────────────────────────
120
+ {
121
+ id: "cc-sub-agents",
122
+ sourceUrl: "https://code.claude.com/docs/en/sub-agents.md",
123
+ name: "cc-sub-agents",
124
+ description: "Configuraci\xF3n de sub-agents en Claude Code: frontmatter fields, modelos, tools disponibles, persistent memory, hooks en subagents.",
125
+ splitStrategy: "none",
126
+ tokenBudget: 700,
127
+ priority: "high"
128
+ },
129
+ {
130
+ id: "cc-agent-teams",
131
+ sourceUrl: "https://code.claude.com/docs/en/agent-teams.md",
132
+ name: "cc-agent-teams",
133
+ description: "Agent teams en Claude Code: coordinar m\xFAltiples sesiones, asignar tareas, hablar con teammates, display modes, token costs.",
134
+ splitStrategy: "none",
135
+ tokenBudget: 600,
136
+ priority: "high"
137
+ },
138
+ {
139
+ id: "cc-skills-guide",
140
+ sourceUrl: "https://code.claude.com/docs/en/skills.md",
141
+ name: "cc-skills-guide",
142
+ description: "C\xF3mo crear y configurar Skills en Claude Code: frontmatter, invocation control, subagent skills, path-specific, bundled skills.",
143
+ splitStrategy: "none",
144
+ tokenBudget: 600,
145
+ priority: "high"
146
+ },
147
+ {
148
+ id: "cc-memory",
149
+ sourceUrl: "https://code.claude.com/docs/en/memory.md",
150
+ name: "cc-memory",
151
+ description: "CLAUDE.md y auto-memory en Claude Code: d\xF3nde poner archivos, import de archivos adicionales, rules directory, gesti\xF3n para equipos.",
152
+ splitStrategy: "none",
153
+ tokenBudget: 600,
154
+ priority: "high"
155
+ },
156
+ {
157
+ id: "cc-cli-reference",
158
+ sourceUrl: "https://code.claude.com/docs/en/cli-reference.md",
159
+ name: "cc-cli-reference",
160
+ description: "Referencia completa CLI de Claude Code: flags, comandos, system prompt flags, opciones de headless.",
161
+ splitStrategy: "none",
162
+ tokenBudget: 500,
163
+ priority: "high"
164
+ },
165
+ {
166
+ id: "cc-commands",
167
+ sourceUrl: "https://code.claude.com/docs/en/commands.md",
168
+ name: "cc-commands",
169
+ description: "Comandos built-in de Claude Code: /batch, /debug, /loop, /simplify, /claude-api, /compact, /memory y m\xE1s.",
170
+ splitStrategy: "none",
171
+ tokenBudget: 500,
172
+ priority: "high"
173
+ },
174
+ {
175
+ id: "cc-model-config",
176
+ sourceUrl: "https://code.claude.com/docs/en/model-config.md",
177
+ name: "cc-model-config",
178
+ description: "Configuraci\xF3n de modelos en Claude Code: aliases (opusplan, fast-mode), restrict model selection, extended context, env vars para pinning.",
179
+ splitStrategy: "none",
180
+ tokenBudget: 500,
181
+ priority: "high"
182
+ },
183
+ // ── MEDIUM PRIORITY ────────────────────────────────────────────
184
+ {
185
+ id: "cc-plugins",
186
+ sourceUrl: "https://code.claude.com/docs/en/plugins.md",
187
+ name: "cc-plugins",
188
+ description: "Crear plugins para Claude Code: estructura, skills, LSP servers, MCP servers incluidos, distribuci\xF3n en marketplaces.",
189
+ splitStrategy: "none",
190
+ tokenBudget: 600,
191
+ priority: "medium"
192
+ },
193
+ {
194
+ id: "cc-channels",
195
+ sourceUrl: "https://code.claude.com/docs/en/channels.md",
196
+ name: "cc-channels",
197
+ description: "Channels en Claude Code: push events a sesiones activas, webhooks, alertas de CI, chat messages desde MCP server.",
198
+ splitStrategy: "none",
199
+ tokenBudget: 500,
200
+ priority: "medium"
201
+ },
202
+ {
203
+ id: "cc-scheduled-tasks",
204
+ sourceUrl: "https://code.claude.com/docs/en/scheduled-tasks.md",
205
+ name: "cc-scheduled-tasks",
206
+ description: "Tareas programadas en Claude Code: /loop, cron syntax, one-time reminders, gesti\xF3n de tasks recurrentes.",
207
+ splitStrategy: "none",
208
+ tokenBudget: 400,
209
+ priority: "medium"
210
+ },
211
+ {
212
+ id: "cc-headless",
213
+ sourceUrl: "https://code.claude.com/docs/en/headless.md",
214
+ name: "cc-headless",
215
+ description: "Modo headless y Agent SDK de Claude Code: uso program\xE1tico desde CLI, Python y TypeScript, output estructurado, bare mode.",
216
+ splitStrategy: "none",
217
+ tokenBudget: 500,
218
+ priority: "medium"
219
+ },
220
+ {
221
+ id: "cc-sandboxing",
222
+ sourceUrl: "https://code.claude.com/docs/en/sandboxing.md",
223
+ name: "cc-sandboxing",
224
+ description: "Sandboxing en Claude Code: filesystem isolation, network isolation, c\xF3mo habilitar, configurar paths permitidos, relaci\xF3n con permisos.",
225
+ splitStrategy: "none",
226
+ tokenBudget: 400,
227
+ priority: "medium"
228
+ },
229
+ {
230
+ id: "cc-common-workflows",
231
+ sourceUrl: "https://code.claude.com/docs/en/common-workflows.md",
232
+ name: "cc-common-workflows",
233
+ description: "Flujos de trabajo comunes en Claude Code: worktrees paralelos, git workflows, plan mode, extended thinking, pipe input/output.",
234
+ splitStrategy: "sections",
235
+ tokenBudget: 700,
236
+ priority: "medium"
237
+ },
238
+ {
239
+ id: "cc-best-practices",
240
+ sourceUrl: "https://code.claude.com/docs/en/best-practices.md",
241
+ name: "cc-best-practices",
242
+ description: "Best practices de Claude Code: dar contexto efectivo, gestionar contexto, course-correct, evitar patrones de fallo comunes.",
243
+ splitStrategy: "none",
244
+ tokenBudget: 600,
245
+ priority: "medium"
246
+ },
247
+ {
248
+ id: "cc-github-actions",
249
+ sourceUrl: "https://code.claude.com/docs/en/github-actions.md",
250
+ name: "cc-github-actions",
251
+ description: "Claude Code en GitHub Actions: setup, @claude mentions, configuraci\xF3n con Bedrock y Vertex, security considerations.",
252
+ splitStrategy: "none",
253
+ tokenBudget: 500,
254
+ priority: "medium"
255
+ }
256
+ ];
257
+
258
+ // src/config/loader.ts
259
+ import { cosmiconfig } from "cosmiconfig";
260
+ var DEFAULTS = {
261
+ skills: ["critical", "high"],
262
+ maxAge: 86400,
263
+ transformer: "auto",
264
+ silent: false
265
+ };
266
+ var ALL_PRIORITIES = ["critical", "high", "medium"];
267
+ function normalizeSkillsFilter(raw) {
268
+ if (raw === "all") {
269
+ return [...ALL_PRIORITIES];
270
+ }
271
+ if (Array.isArray(raw) && raw.every((x) => typeof x === "string")) {
272
+ return raw.length > 0 ? raw : DEFAULTS.skills;
273
+ }
274
+ return DEFAULTS.skills;
275
+ }
276
+ async function loadConfig() {
277
+ const explorer = cosmiconfig("pulse");
278
+ try {
279
+ const result = await explorer.search();
280
+ if (result && result.config) {
281
+ const c = result.config;
282
+ const merged = { ...DEFAULTS, ...c };
283
+ return {
284
+ ...merged,
285
+ skills: normalizeSkillsFilter(c.skills !== void 0 ? c.skills : merged.skills)
286
+ };
287
+ }
288
+ } catch {
289
+ }
290
+ return { ...DEFAULTS };
291
+ }
292
+
293
+ // src/core/hook-manager.ts
294
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
295
+ import { join, dirname } from "path";
296
+
297
+ // src/utils/platform.ts
298
+ import { execFile } from "child_process";
299
+ import { readFileSync, existsSync } from "fs";
300
+ function detectPlatform() {
301
+ if (process.platform === "win32") return "windows";
302
+ if (process.platform === "linux" && existsSync("/proc/version")) {
303
+ try {
304
+ const version = readFileSync("/proc/version", "utf-8");
305
+ if (/microsoft/i.test(version)) return "wsl";
306
+ } catch {
307
+ }
308
+ }
309
+ return "unix";
310
+ }
311
+ function getHookCommand() {
312
+ const platform = detectPlatform();
313
+ if (platform === "windows") {
314
+ return "npx.cmd pulse sync --if-stale 86400 --silent & npx.cmd pulse greet --once";
315
+ }
316
+ return "npx pulse sync --if-stale 86400 --silent && npx pulse greet --once || true";
317
+ }
318
+ function isClaudeCliAvailable() {
319
+ return new Promise((resolve) => {
320
+ const cmd = process.platform === "win32" ? "claude.cmd" : "claude";
321
+ const child = execFile(cmd, ["--version"], { timeout: 5e3 }, (err) => {
322
+ if (err) {
323
+ if (process.platform === "win32" && cmd === "claude.cmd") {
324
+ execFile("claude", ["--version"], { timeout: 5e3 }, (err2) => {
325
+ resolve(!err2);
326
+ });
327
+ return;
328
+ }
329
+ resolve(false);
330
+ return;
331
+ }
332
+ resolve(true);
333
+ });
334
+ child.on("error", () => resolve(false));
335
+ });
336
+ }
337
+
338
+ // src/core/hook-manager.ts
339
+ function settingsPath() {
340
+ return join(process.cwd(), ".claude", "settings.json");
341
+ }
342
+ function readSettings() {
343
+ const path = settingsPath();
344
+ try {
345
+ return JSON.parse(readFileSync2(path, "utf-8"));
346
+ } catch {
347
+ return {};
348
+ }
349
+ }
350
+ function writeSettings(settings) {
351
+ const path = settingsPath();
352
+ const dir = dirname(path);
353
+ if (!existsSync2(dir)) {
354
+ mkdirSync(dir, { recursive: true });
355
+ }
356
+ writeFileSync(path, JSON.stringify(settings, null, 2) + "\n", "utf-8");
357
+ }
358
+ function hasPulseHook(entries) {
359
+ return entries.some((e) => e._pulse === true);
360
+ }
361
+ function addHook() {
362
+ const settings = readSettings();
363
+ if (!settings.hooks) {
364
+ settings.hooks = {};
365
+ }
366
+ if (!settings.hooks.SessionStart) {
367
+ settings.hooks.SessionStart = [];
368
+ }
369
+ const sessionStart = settings.hooks.SessionStart;
370
+ if (hasPulseHook(sessionStart)) {
371
+ return;
372
+ }
373
+ sessionStart.push({
374
+ _pulse: true,
375
+ matcher: "",
376
+ hooks: [{ type: "command", command: getHookCommand() }]
377
+ });
378
+ writeSettings(settings);
379
+ }
380
+ function removeHook() {
381
+ const path = settingsPath();
382
+ if (!existsSync2(path)) return;
383
+ const settings = readSettings();
384
+ if (!settings.hooks) return;
385
+ const sessionStart = settings.hooks.SessionStart;
386
+ if (!sessionStart) return;
387
+ if (!hasPulseHook(sessionStart)) return;
388
+ const filtered = sessionStart.filter((e) => e._pulse !== true);
389
+ if (filtered.length > 0) {
390
+ settings.hooks.SessionStart = filtered;
391
+ } else {
392
+ delete settings.hooks.SessionStart;
393
+ }
394
+ if (Object.keys(settings.hooks).length === 0) {
395
+ delete settings.hooks;
396
+ }
397
+ writeSettings(settings);
398
+ }
399
+
400
+ // src/core/fetcher.ts
401
+ import { request } from "undici";
402
+ import pLimit from "p-limit";
403
+
404
+ // src/utils/logger.ts
405
+ var silent = false;
406
+ function setSilent(value) {
407
+ silent = value;
408
+ }
409
+ function log(...args) {
410
+ if (!silent) console.log(...args);
411
+ }
412
+ function warn(...args) {
413
+ if (!silent) console.warn(...args);
414
+ }
415
+ function success(...args) {
416
+ if (!silent) console.log(...args);
417
+ }
418
+ function error(...args) {
419
+ console.error(...args);
420
+ }
421
+
422
+ // src/core/fetcher.ts
423
+ var REQUEST_TIMEOUT = 1e4;
424
+ var MAX_RETRIES = 2;
425
+ async function sleep(ms) {
426
+ return new Promise((resolve) => setTimeout(resolve, ms));
427
+ }
428
+ async function fetchDoc(url, etag) {
429
+ const headers = {};
430
+ if (etag) {
431
+ headers["If-None-Match"] = etag;
432
+ }
433
+ let lastError;
434
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
435
+ if (attempt > 0) {
436
+ const delay = 1e3 * 2 ** (attempt - 1);
437
+ await sleep(delay);
438
+ }
439
+ try {
440
+ const res = await request(url, {
441
+ headers,
442
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT)
443
+ });
444
+ if (res.statusCode === 304) {
445
+ return {
446
+ url,
447
+ content: "",
448
+ changed: false,
449
+ etag,
450
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
451
+ };
452
+ }
453
+ if (res.statusCode < 200 || res.statusCode >= 300) {
454
+ throw new Error(`HTTP ${res.statusCode} fetching ${url}`);
455
+ }
456
+ const content = await res.body.text();
457
+ const newEtag = res.headers["etag"];
458
+ return {
459
+ url,
460
+ content,
461
+ changed: true,
462
+ etag: newEtag ?? etag,
463
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
464
+ };
465
+ } catch (err) {
466
+ lastError = err instanceof Error ? err : new Error(String(err));
467
+ if (attempt < MAX_RETRIES) {
468
+ warn(`Fetch attempt ${attempt + 1} failed for ${url}, retrying...`);
469
+ }
470
+ }
471
+ }
472
+ throw lastError ?? new Error(`Failed to fetch ${url}`);
473
+ }
474
+
475
+ // src/core/transformer-static.ts
476
+ import { unified } from "unified";
477
+ import remarkParse from "remark-parse";
478
+ import remarkStringify from "remark-stringify";
479
+ var JSX_TAG_RE = /^<\/?[A-Z][A-Za-z]*[\s/>]/;
480
+ var INTERNAL_LINK_RE = /^\/docs\//;
481
+ function isJsxNode(node) {
482
+ if (node.type === "html" && JSX_TAG_RE.test(node.value)) return true;
483
+ return false;
484
+ }
485
+ function isImageNode(node) {
486
+ return node.type === "image";
487
+ }
488
+ function isInternalLink(node) {
489
+ if (node.type === "link" && INTERNAL_LINK_RE.test(node.url)) return true;
490
+ return false;
491
+ }
492
+ function filterChildren(nodes) {
493
+ const result = [];
494
+ for (const node of nodes) {
495
+ if (isJsxNode(node) || isImageNode(node)) continue;
496
+ if (isInternalLink(node)) {
497
+ if ("children" in node && node.children.length > 0) {
498
+ result.push(...filterChildren(node.children));
499
+ }
500
+ continue;
501
+ }
502
+ if ("children" in node && Array.isArray(node.children)) {
503
+ const filtered = filterChildren(node.children);
504
+ result.push({ ...node, children: filtered });
505
+ } else {
506
+ result.push(node);
507
+ }
508
+ }
509
+ return result;
510
+ }
511
+ function cleanAst() {
512
+ return (tree) => {
513
+ tree.children = filterChildren(tree.children);
514
+ };
515
+ }
516
+ function transformStatic(rawMarkdown, tokenBudget) {
517
+ const processor = unified().use(remarkParse).use(cleanAst).use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" });
518
+ const result = processor.processSync(rawMarkdown);
519
+ const cleaned = String(result);
520
+ const charBudget = tokenBudget * 4;
521
+ if (cleaned.length <= charBudget) {
522
+ return cleaned;
523
+ }
524
+ return truncateAtSectionBoundary(cleaned, charBudget);
525
+ }
526
+ function truncateAtSectionBoundary(text, charBudget) {
527
+ const lines = text.split("\n");
528
+ let length = 0;
529
+ let lastHeadingIndex = 0;
530
+ for (let i = 0; i < lines.length; i++) {
531
+ length += lines[i].length + 1;
532
+ if (/^#{1,3} /.test(lines[i])) {
533
+ lastHeadingIndex = i;
534
+ }
535
+ if (length > charBudget) {
536
+ const cutIndex = lastHeadingIndex > 0 ? lastHeadingIndex : i;
537
+ return lines.slice(0, cutIndex).join("\n").trim();
538
+ }
539
+ }
540
+ return text;
541
+ }
542
+
543
+ // src/core/transformer-claude.ts
544
+ import { execFile as execFile2 } from "child_process";
545
+ var TRANSFORM_PROMPT = `You are a documentation condenser for Claude Code skills.
546
+ Transform the following markdown documentation into a concise skill reference.
547
+
548
+ Rules:
549
+ - Keep ALL code examples exactly as-is \u2014 do not modify, summarize, or omit any code block
550
+ - Keep ALL reference tables intact
551
+ - Convert prose paragraphs to concise bullet points
552
+ - Remove navigation elements, breadcrumbs, and "see also" links
553
+ - Conserve syntax details, parameters, edge cases, and caveats
554
+ - Output in English
555
+ - Start directly with the content \u2014 no frontmatter, no title like "# Skill"
556
+ - Respect the token budget \u2014 be concise but preserve technical accuracy
557
+ - Prioritize: code examples > tables > parameter lists > prose explanations
558
+
559
+ Transform this documentation:`;
560
+ var cachedCliAvailable;
561
+ async function checkCliAvailable() {
562
+ if (cachedCliAvailable !== void 0) return cachedCliAvailable;
563
+ try {
564
+ cachedCliAvailable = await isClaudeCliAvailable();
565
+ } catch {
566
+ cachedCliAvailable = false;
567
+ }
568
+ return cachedCliAvailable;
569
+ }
570
+ async function transformWithClaude(rawMarkdown, tokenBudget) {
571
+ const available = await checkCliAvailable();
572
+ if (!available) {
573
+ warn("Claude CLI not available, skipping AI transformation");
574
+ return null;
575
+ }
576
+ const cmd = process.platform === "win32" ? "claude" : "claude";
577
+ const args = [
578
+ "--print",
579
+ "--model",
580
+ "claude-haiku-4-5-20251001",
581
+ "--max-tokens",
582
+ String(tokenBudget)
583
+ ];
584
+ const input = `${TRANSFORM_PROMPT}
585
+
586
+ ${rawMarkdown}`;
587
+ return new Promise((resolve) => {
588
+ const child = execFile2(cmd, args, {
589
+ timeout: 3e4,
590
+ maxBuffer: 1024 * 1024,
591
+ encoding: "utf-8"
592
+ }, (err, stdout) => {
593
+ if (err) {
594
+ if (err.killed) {
595
+ warn("Claude CLI transformation timed out (30s)");
596
+ } else {
597
+ warn(`Claude CLI transformation failed: ${err.message}`);
598
+ }
599
+ resolve(null);
600
+ return;
601
+ }
602
+ const result = stdout.trim();
603
+ if (!result) {
604
+ warn("Claude CLI returned empty output");
605
+ resolve(null);
606
+ return;
607
+ }
608
+ resolve(result);
609
+ });
610
+ if (child.stdin) {
611
+ child.stdin.write(input);
612
+ child.stdin.end();
613
+ }
614
+ });
615
+ }
616
+
617
+ // src/core/splitter.ts
618
+ function splitDocument(content, strategy, manualSections) {
619
+ switch (strategy) {
620
+ case "sections":
621
+ return splitBySections(content);
622
+ case "manual":
623
+ return splitByManual(content, manualSections ?? []);
624
+ case "none":
625
+ default:
626
+ return [{ id: "", content }];
627
+ }
628
+ }
629
+ function splitBySections(markdown) {
630
+ const lines = markdown.split("\n");
631
+ const sections = [];
632
+ const preambleLines = [];
633
+ let currentHeading = null;
634
+ let currentLines = [];
635
+ for (const line of lines) {
636
+ const h2Match = line.match(/^## (.+)/);
637
+ if (h2Match) {
638
+ if (currentLines.length > 0) {
639
+ if (currentHeading === null) {
640
+ preambleLines.push(...currentLines);
641
+ } else {
642
+ sections.push({
643
+ id: toKebabCase(currentHeading),
644
+ content: currentLines.join("\n").trim()
645
+ });
646
+ }
647
+ }
648
+ currentHeading = h2Match[1].trim();
649
+ if (preambleLines.length > 0) {
650
+ currentLines = [...preambleLines, "", line];
651
+ preambleLines.length = 0;
652
+ } else {
653
+ currentLines = [line];
654
+ }
655
+ continue;
656
+ }
657
+ currentLines.push(line);
658
+ }
659
+ if (currentLines.length === 0) {
660
+ return sections;
661
+ }
662
+ if (currentHeading === null) {
663
+ return [{ id: "document", content: currentLines.join("\n").trim() }];
664
+ }
665
+ sections.push({
666
+ id: toKebabCase(currentHeading),
667
+ content: currentLines.join("\n").trim()
668
+ });
669
+ return sections;
670
+ }
671
+ function splitByManual(markdown, manualSections) {
672
+ const results = [];
673
+ for (let i = 0; i < manualSections.length; i++) {
674
+ const section = manualSections[i];
675
+ const startIdx = findHeadingIndex(markdown, section.heading);
676
+ if (startIdx === -1) {
677
+ warn(`Heading not found: "${section.heading}" \u2014 skipping section ${section.id}`);
678
+ continue;
679
+ }
680
+ let endIdx = markdown.length;
681
+ if (i + 1 < manualSections.length) {
682
+ const nextIdx = findHeadingIndex(markdown, manualSections[i + 1].heading);
683
+ if (nextIdx !== -1) {
684
+ endIdx = nextIdx;
685
+ }
686
+ }
687
+ const content = markdown.slice(startIdx, endIdx).trim();
688
+ results.push({ id: section.id, content });
689
+ }
690
+ return results;
691
+ }
692
+ function findHeadingIndex(markdown, heading) {
693
+ const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
694
+ const regex = new RegExp(`^#{1,6}\\s+${escapedHeading}`, "m");
695
+ const match = regex.exec(markdown);
696
+ return match ? match.index : -1;
697
+ }
698
+ function toKebabCase(str) {
699
+ return str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
700
+ }
701
+
702
+ // src/core/transformer.ts
703
+ function generateFrontmatter(name, description, source, transformedWith) {
704
+ const syncedAt = (/* @__PURE__ */ new Date()).toISOString();
705
+ return [
706
+ "---",
707
+ `name: ${name}`,
708
+ `description: ${JSON.stringify(description)}`,
709
+ "invocation: auto",
710
+ "_pulse: true",
711
+ `_syncedAt: "${syncedAt}"`,
712
+ `_source: "${source}"`,
713
+ "---",
714
+ ""
715
+ ].join("\n");
716
+ }
717
+ async function transformSkill(def, rawContent) {
718
+ const tokenBudget = def.tokenBudget ?? 600;
719
+ const source = def.sourceUrl ?? "static";
720
+ if (def.static) {
721
+ log(` \u2192 ${def.id}: fixed (static skill)`);
722
+ const frontmatter = generateFrontmatter(def.id, def.description, "static", "fixed");
723
+ return [{
724
+ id: def.id,
725
+ filename: `.claude/skills/${def.id}/SKILL.md`,
726
+ content: frontmatter + rawContent,
727
+ transformedWith: "fixed"
728
+ }];
729
+ }
730
+ let transformed;
731
+ let method;
732
+ const claudeResult = await transformWithClaude(rawContent, tokenBudget);
733
+ if (claudeResult) {
734
+ transformed = claudeResult;
735
+ method = "claude";
736
+ log(` \u2192 ${def.id}: transformed with Claude`);
737
+ } else {
738
+ transformed = transformStatic(rawContent, tokenBudget);
739
+ method = "static";
740
+ log(` \u2192 ${def.id}: transformed with static fallback`);
741
+ }
742
+ const strategy = def.splitStrategy ?? "none";
743
+ const sections = splitDocument(transformed, strategy, def.manualSections);
744
+ return sections.map((section) => {
745
+ const skillId = section.id ? `${def.id}-${section.id}` : def.id;
746
+ const finalId = def.manualSections?.find((ms) => ms.id === section.id) ? section.id : skillId;
747
+ const sectionDef = def.manualSections?.find((ms) => ms.id === section.id);
748
+ const description = sectionDef?.description ?? def.description;
749
+ const frontmatter = generateFrontmatter(finalId, description, source, method);
750
+ return {
751
+ id: finalId,
752
+ filename: `.claude/skills/${finalId}/SKILL.md`,
753
+ content: frontmatter + section.content,
754
+ transformedWith: method
755
+ };
756
+ });
757
+ }
758
+
759
+ // src/core/installer.ts
760
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
761
+ import { join as join2 } from "path";
762
+ function skillsBase() {
763
+ return join2(process.cwd(), ".claude", "skills");
764
+ }
765
+ function skillFilePath(id) {
766
+ return join2(skillsBase(), id, "SKILL.md");
767
+ }
768
+ function hasPulseFrontmatter(content) {
769
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
770
+ return match !== null && /^_pulse:\s*true$/m.test(match[1]);
771
+ }
772
+ function ensurePulseFrontmatter(content) {
773
+ if (hasPulseFrontmatter(content)) return content;
774
+ return `---
775
+ _pulse: true
776
+ ---
777
+ ${content}`;
778
+ }
779
+ function isManaged(skillDir) {
780
+ const filePath = join2(skillDir, "SKILL.md");
781
+ try {
782
+ const raw = readFileSync3(filePath, "utf-8");
783
+ const match = raw.match(/^---\n([\s\S]*?)\n---/);
784
+ if (!match) return false;
785
+ return /^_pulse:\s*true$/m.test(match[1]);
786
+ } catch {
787
+ return false;
788
+ }
789
+ }
790
+ function install(skills) {
791
+ for (const skill of skills) {
792
+ const dir = join2(skillsBase(), skill.id);
793
+ const filePath = skillFilePath(skill.id);
794
+ if (existsSync3(filePath) && !isManaged(dir)) {
795
+ continue;
796
+ }
797
+ mkdirSync2(dir, { recursive: true });
798
+ writeFileSync2(filePath, ensurePulseFrontmatter(skill.content), "utf-8");
799
+ }
800
+ }
801
+
802
+ // src/commands/sync.ts
803
+ import { readFileSync as readFileSync4 } from "fs";
804
+ import { join as join3, dirname as dirname2 } from "path";
805
+ import { fileURLToPath } from "url";
806
+ function getSkillsFallbackDir() {
807
+ const thisFile = fileURLToPath(import.meta.url);
808
+ const root = join3(dirname2(thisFile), "..", "..");
809
+ return join3(root, "skills-fallback");
810
+ }
811
+ function loadStaticSkillContent(skillId) {
812
+ const fallbackDir = getSkillsFallbackDir();
813
+ const skillPath = join3(fallbackDir, skillId, "SKILL.md");
814
+ return readFileSync4(skillPath, "utf-8");
815
+ }
816
+ async function syncCore(options) {
817
+ if (options.silent) {
818
+ setSilent(true);
819
+ }
820
+ const config = await loadConfig();
821
+ if (options.ifStale) {
822
+ const maxAge = parseInt(options.ifStale, 10);
823
+ if (!isNaN(maxAge) && !isStale(maxAge)) {
824
+ setSilent(false);
825
+ return [];
826
+ }
827
+ }
828
+ let skills;
829
+ if (options.skills && options.skills.length > 0) {
830
+ skills = SKILLS_REGISTRY.filter((s) => options.skills.includes(s.id));
831
+ } else {
832
+ const priorities = config.skills ?? ["critical", "high"];
833
+ skills = SKILLS_REGISTRY.filter((s) => priorities.includes(s.priority) || priorities.includes("all"));
834
+ }
835
+ const meta = readMeta();
836
+ const results = [];
837
+ let hasErrors = false;
838
+ log("Syncing skills...");
839
+ for (const skill of skills) {
840
+ try {
841
+ if (skill.static) {
842
+ const content = loadStaticSkillContent(skill.id);
843
+ install([{ id: skill.id, content }]);
844
+ meta.skills[skill.id] = {
845
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
846
+ transformedWith: "fixed"
847
+ };
848
+ results.push({ id: skill.id, action: "updated", transformer: "fixed" });
849
+ log(` \u2713 ${skill.id} (static)`);
850
+ continue;
851
+ }
852
+ if (!skill.sourceUrl) {
853
+ results.push({ id: skill.id, action: "skipped" });
854
+ continue;
855
+ }
856
+ const etag = options.force ? void 0 : meta.etags[skill.sourceUrl];
857
+ const fetchResult = await fetchDoc(skill.sourceUrl, etag);
858
+ if (!fetchResult.changed) {
859
+ results.push({ id: skill.id, action: "skipped", transformer: meta.skills[skill.id]?.transformedWith });
860
+ log(` \xB7 ${skill.id} unchanged (304)`);
861
+ continue;
862
+ }
863
+ if (fetchResult.etag) {
864
+ meta.etags[skill.sourceUrl] = fetchResult.etag;
865
+ }
866
+ const transformed = await transformSkill(skill, fetchResult.content);
867
+ install(transformed.map((t) => ({ id: t.id, content: t.content })));
868
+ for (const t of transformed) {
869
+ meta.skills[t.id] = {
870
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
871
+ transformedWith: t.transformedWith,
872
+ etag: fetchResult.etag
873
+ };
874
+ }
875
+ results.push({ id: skill.id, action: "updated", transformer: transformed[0]?.transformedWith });
876
+ log(` \u2713 ${skill.id} updated`);
877
+ } catch (err) {
878
+ hasErrors = true;
879
+ const msg = err instanceof Error ? err.message : String(err);
880
+ warn(` \u2717 ${skill.id} failed: ${msg}`);
881
+ results.push({ id: skill.id, action: "failed", error: msg });
882
+ }
883
+ }
884
+ meta.version = (await import("./meta-TZOSLFST.js")).readMeta().version;
885
+ meta.lastSync = (/* @__PURE__ */ new Date()).toISOString();
886
+ meta.lastSyncStatus = hasErrors ? results.some((r) => r.action === "updated") ? "partial" : "failed" : "success";
887
+ if (hasErrors) {
888
+ const errors = results.filter((r) => r.action === "failed").map((r) => r.error);
889
+ meta.lastSyncError = errors.join("; ");
890
+ } else {
891
+ delete meta.lastSyncError;
892
+ }
893
+ writeMeta(meta);
894
+ if (!options.silent && results.length > 0) {
895
+ log("");
896
+ log(" Skill ID Action Transformer");
897
+ log(" " + "\u2500".repeat(50));
898
+ for (const r of results) {
899
+ const id = r.id.padEnd(22);
900
+ const action = r.action.padEnd(10);
901
+ const transformer = r.transformer ?? "";
902
+ log(` ${id} ${action} ${transformer}`);
903
+ }
904
+ log("");
905
+ }
906
+ setSilent(false);
907
+ return results;
908
+ }
909
+ function registerSync(program2) {
910
+ program2.command("sync").description("Fetch, transform, and install skills").option("--force", "Ignore ETags, re-download everything").option("--if-stale <seconds>", "Only sync if last sync is older than N seconds").option("--silent", "Suppress all non-error output").option("--skills <ids...>", "Sync only specific skill IDs").action(async (opts) => {
911
+ try {
912
+ await syncCore(opts);
913
+ } catch (err) {
914
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
915
+ `);
916
+ process.exit(1);
917
+ }
918
+ });
919
+ }
920
+
921
+ // src/commands/init.ts
922
+ async function confirm(message) {
923
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
924
+ return new Promise((resolve) => {
925
+ rl.question(message, (answer) => {
926
+ rl.close();
927
+ resolve(answer.toLowerCase().startsWith("y"));
928
+ });
929
+ });
930
+ }
931
+ function registerInit(program2) {
932
+ program2.command("init").description("Initialize pulse in the current project").option("--force", "Re-initialize even if already set up").option("--yes", "Skip confirmation prompt").option("--skills <ids...>", "Install only specific skill IDs").action(async (opts) => {
933
+ try {
934
+ const metaPath = getMetaPath();
935
+ if (existsSync4(metaPath) && !opts.force) {
936
+ const meta2 = readMeta();
937
+ log("pulse is already initialized.");
938
+ log(` Last sync: ${meta2.lastSync || "never"}`);
939
+ log(` Status: ${meta2.lastSyncStatus}`);
940
+ log(` Skills: ${Object.keys(meta2.skills).length} installed`);
941
+ log("");
942
+ log("Use --force to re-initialize.");
943
+ return;
944
+ }
945
+ const claudeDir = join4(process.cwd(), ".claude");
946
+ const skillsDir = join4(claudeDir, "skills");
947
+ if (!existsSync4(claudeDir)) {
948
+ mkdirSync3(claudeDir, { recursive: true });
949
+ }
950
+ if (!existsSync4(skillsDir)) {
951
+ mkdirSync3(skillsDir, { recursive: true });
952
+ }
953
+ const config = await loadConfig();
954
+ let skillsToInstall = SKILLS_REGISTRY;
955
+ if (opts.skills && opts.skills.length > 0) {
956
+ skillsToInstall = SKILLS_REGISTRY.filter((s) => opts.skills.includes(s.id));
957
+ } else {
958
+ const priorities = config.skills ?? ["critical", "high"];
959
+ skillsToInstall = SKILLS_REGISTRY.filter(
960
+ (s) => priorities.includes(s.priority) || priorities.includes("all")
961
+ );
962
+ }
963
+ log("Skills to install:");
964
+ log("");
965
+ for (const skill of skillsToInstall) {
966
+ const tag = skill.static ? " (static)" : "";
967
+ log(` [${skill.priority}] ${skill.id}${tag}`);
968
+ }
969
+ log("");
970
+ log(`Total: ${skillsToInstall.length} skills`);
971
+ log("");
972
+ if (!opts.yes) {
973
+ const accepted = await confirm("Proceed? (y/N) ");
974
+ if (!accepted) {
975
+ log("Aborted.");
976
+ return;
977
+ }
978
+ }
979
+ log("");
980
+ const syncOpts = {
981
+ force: true,
982
+ skills: opts.skills
983
+ };
984
+ await syncCore(syncOpts);
985
+ addHook();
986
+ log("SessionStart hook added to .claude/settings.json");
987
+ const meta = readMeta();
988
+ meta.firstSessionDone = false;
989
+ writeMeta(meta);
990
+ log("");
991
+ success("pulse initialized successfully!");
992
+ log(` Skills installed: ${Object.keys(meta.skills).length}`);
993
+ log(" Hook: SessionStart \u2713");
994
+ log(" Next auto-sync: in ~24 hours (via hook)");
995
+ log("");
996
+ log("Tip: commit .claude/skills/cc-*/ to share skills with your team.");
997
+ } catch (err) {
998
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : err}
999
+ `);
1000
+ process.exit(1);
1001
+ }
1002
+ });
1003
+ }
1004
+
1005
+ // src/commands/greet.ts
1006
+ var WELCOME_MESSAGE = `claude-code-tutor is active \u2014 your skills are synchronized with the official Claude Code docs.
1007
+
1008
+ - Use /cc-tutor for an interactive guided tour of Claude Code features
1009
+ - Use /cc-learning-path to see the full structured curriculum by level
1010
+ - Skills auto-update daily via the SessionStart hook
1011
+
1012
+ The user has a pulse tutor memory at ~/.claude/pulse/memory.json. Read it only if the cc-tutor skill activates.
1013
+
1014
+ Happy coding!`;
1015
+ function registerGreet(program2) {
1016
+ program2.command("greet").description("Output welcome context for SessionStart hook").option("--once", "Only greet on first session").action((opts) => {
1017
+ const meta = readMeta();
1018
+ if (opts.once && meta.firstSessionDone) {
1019
+ return;
1020
+ }
1021
+ const output = JSON.stringify({ additionalContext: WELCOME_MESSAGE });
1022
+ process.stdout.write(output + "\n");
1023
+ if (opts.once) {
1024
+ meta.firstSessionDone = true;
1025
+ writeMeta(meta);
1026
+ }
1027
+ });
1028
+ }
1029
+
1030
+ // src/commands/list.ts
1031
+ import { existsSync as existsSync5 } from "fs";
1032
+ import { join as join5 } from "path";
1033
+ var MAX_AGE_SECONDS = 86400;
1034
+ function statusIcon(skillId, meta) {
1035
+ const skillMeta = meta.skills[skillId];
1036
+ if (!skillMeta) {
1037
+ const skillPath = join5(process.cwd(), ".claude", "skills", skillId, "SKILL.md");
1038
+ if (!existsSync5(skillPath)) return "\u2717";
1039
+ return "?";
1040
+ }
1041
+ if (!skillMeta.syncedAt) return "\u2717";
1042
+ const elapsed = (Date.now() - new Date(skillMeta.syncedAt).getTime()) / 1e3;
1043
+ return elapsed > MAX_AGE_SECONDS ? "\u26A0" : "\u2713";
1044
+ }
1045
+ function registerList(program2) {
1046
+ program2.command("list").description("List all skills and their sync status").action(async () => {
1047
+ const meta = readMeta();
1048
+ log(" Status Skill ID Last Sync Transformer");
1049
+ log(" " + "\u2500".repeat(70));
1050
+ for (const skill of SKILLS_REGISTRY) {
1051
+ const icon = statusIcon(skill.id, meta);
1052
+ const skillMeta = meta.skills[skill.id];
1053
+ const id = skill.id.padEnd(22);
1054
+ const lastSync = skillMeta?.syncedAt ? new Date(skillMeta.syncedAt).toLocaleString() : "";
1055
+ const syncPad = lastSync.padEnd(20);
1056
+ const transformer = skillMeta?.transformedWith ?? "";
1057
+ log(` ${icon} ${id} ${syncPad} ${transformer}`);
1058
+ }
1059
+ log("");
1060
+ const installed = Object.keys(meta.skills).length;
1061
+ log(` ${installed} / ${SKILLS_REGISTRY.length} skills tracked`);
1062
+ });
1063
+ }
1064
+
1065
+ // src/commands/status.ts
1066
+ import { readFileSync as readFileSync5 } from "fs";
1067
+ import { join as join6 } from "path";
1068
+ function relativeTime(isoDate) {
1069
+ const ms = Date.now() - new Date(isoDate).getTime();
1070
+ if (isNaN(ms)) return "unknown";
1071
+ const seconds = Math.floor(ms / 1e3);
1072
+ if (seconds < 60) return `${seconds}s ago`;
1073
+ const minutes = Math.floor(seconds / 60);
1074
+ if (minutes < 60) return `${minutes}m ago`;
1075
+ const hours = Math.floor(minutes / 60);
1076
+ if (hours < 24) return `${hours}h ago`;
1077
+ const days = Math.floor(hours / 24);
1078
+ return `${days}d ago`;
1079
+ }
1080
+ function checkHookInstalled() {
1081
+ const settingsPath2 = join6(process.cwd(), ".claude", "settings.json");
1082
+ try {
1083
+ const settings = JSON.parse(readFileSync5(settingsPath2, "utf-8"));
1084
+ const hooks = settings?.hooks?.SessionStart;
1085
+ if (!Array.isArray(hooks)) return false;
1086
+ return hooks.some((h) => h._pulse === true);
1087
+ } catch {
1088
+ return false;
1089
+ }
1090
+ }
1091
+ function registerStatus(program2) {
1092
+ program2.command("status").description("Show pulse status overview").action(async () => {
1093
+ const meta = readMeta();
1094
+ const platform = detectPlatform();
1095
+ let claudeAvailable = false;
1096
+ try {
1097
+ claudeAvailable = await isClaudeCliAvailable();
1098
+ } catch {
1099
+ }
1100
+ const hookInstalled = checkHookInstalled();
1101
+ const installed = Object.keys(meta.skills).length;
1102
+ log(` pulse v${getVersion()}`);
1103
+ log(` Directory: ${process.cwd()}`);
1104
+ log(` Skills: ${installed} / ${SKILLS_REGISTRY.length} installed`);
1105
+ if (meta.lastSync) {
1106
+ const syncTime = relativeTime(meta.lastSync);
1107
+ log(` Last sync: ${syncTime} (${meta.lastSyncStatus})`);
1108
+ if (meta.lastSyncStatus === "failed" && meta.lastSyncError) {
1109
+ log(` Last error: ${meta.lastSyncError}`);
1110
+ }
1111
+ } else {
1112
+ log(" Last sync: never");
1113
+ }
1114
+ log(` Hook SessionStart: ${hookInstalled ? "\u2713 installed" : "\u2717 not installed"}`);
1115
+ log(` Claude CLI: ${claudeAvailable ? "\u2713 available (transformer: claude)" : "\u2717 not available (transformer: static)"}`);
1116
+ log(` Platform: ${platform}`);
1117
+ log(` firstSessionDone: ${meta.firstSessionDone}`);
1118
+ });
1119
+ }
1120
+
1121
+ // src/commands/uninstall.ts
1122
+ import { readdirSync, rmSync, existsSync as existsSync7 } from "fs";
1123
+ import { join as join7 } from "path";
1124
+ import { createInterface as createInterface2 } from "readline";
1125
+ function registerUninstall(program2) {
1126
+ program2.command("uninstall").description("Remove pulse from the current project").option("--keep-hook", "Leave the SessionStart hook in place").option("--keep-skills", "Leave installed skill files in place").option("--purge-memory", "Also delete tutor memory (~/.claude/pulse/)").action(async (opts) => {
1127
+ if (!opts.keepHook) {
1128
+ removeHook();
1129
+ log(" \u2713 SessionStart hook removed");
1130
+ } else {
1131
+ log(" \xB7 SessionStart hook kept (--keep-hook)");
1132
+ }
1133
+ if (!opts.keepSkills) {
1134
+ const skillsDir = join7(process.cwd(), ".claude", "skills");
1135
+ if (existsSync7(skillsDir)) {
1136
+ let removed = 0;
1137
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
1138
+ for (const entry of entries) {
1139
+ if (!entry.isDirectory()) continue;
1140
+ if (!entry.name.startsWith("cc-")) continue;
1141
+ const dirPath = join7(skillsDir, entry.name);
1142
+ if (isManaged(dirPath)) {
1143
+ rmSync(dirPath, { recursive: true, force: true });
1144
+ removed++;
1145
+ log(` \u2713 Removed ${entry.name}`);
1146
+ }
1147
+ }
1148
+ if (removed === 0) {
1149
+ log(" \xB7 No pulse-managed skills found");
1150
+ }
1151
+ }
1152
+ } else {
1153
+ log(" \xB7 Skills kept (--keep-skills)");
1154
+ }
1155
+ const metaPath = getMetaPath();
1156
+ if (existsSync7(metaPath)) {
1157
+ rmSync(metaPath);
1158
+ log(" \u2713 Meta file removed");
1159
+ }
1160
+ if (opts.purgeMemory) {
1161
+ const pulseDir = getBaseDir();
1162
+ if (existsSync7(pulseDir)) {
1163
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1164
+ const yes = await new Promise((resolve) => {
1165
+ rl.question("Delete all tutor memory and session logs? (y/N) ", (answer) => {
1166
+ rl.close();
1167
+ resolve(answer.trim().toLowerCase() === "y");
1168
+ });
1169
+ });
1170
+ if (yes) {
1171
+ rmSync(pulseDir, { recursive: true, force: true });
1172
+ log(" \u2713 Tutor memory purged");
1173
+ } else {
1174
+ log(" \xB7 Tutor memory preserved (cancelled)");
1175
+ }
1176
+ } else {
1177
+ log(" \xB7 No tutor memory found");
1178
+ }
1179
+ }
1180
+ log("");
1181
+ success("pulse has been uninstalled.");
1182
+ });
1183
+ }
1184
+
1185
+ // src/commands/memory.ts
1186
+ import { readFile, rm } from "fs/promises";
1187
+ import { createInterface as createInterface3 } from "readline";
1188
+ import { join as join8 } from "path";
1189
+ async function confirm2(message) {
1190
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
1191
+ return new Promise((resolve) => {
1192
+ rl.question(`${message} (y/N) `, (answer) => {
1193
+ rl.close();
1194
+ resolve(answer.trim().toLowerCase() === "y");
1195
+ });
1196
+ });
1197
+ }
1198
+ function formatDate(iso) {
1199
+ if (!iso) return "N/A";
1200
+ return new Date(iso).toLocaleDateString();
1201
+ }
1202
+ async function showProgress() {
1203
+ const memory = await readMemory();
1204
+ if (!memory) {
1205
+ log("No tutor sessions recorded yet. Start a session with the cc-tutor skill.");
1206
+ return;
1207
+ }
1208
+ log("\n\u{1F4CB} Tutor Memory");
1209
+ log("\u2500".repeat(40));
1210
+ log(` Name: ${memory.name || "(not set)"}`);
1211
+ log(` Level: ${memory.level}`);
1212
+ log(` Language: ${memory.language}`);
1213
+ log(` Joined: ${formatDate(memory.joinedAt)}`);
1214
+ if (memory.lastSession) {
1215
+ log(`
1216
+ Last session: ${formatDate(memory.lastSession.date)}`);
1217
+ if (memory.lastSession.topicsCovered.length > 0) {
1218
+ log(` Topics: ${memory.lastSession.topicsCovered.join(", ")}`);
1219
+ }
1220
+ if (memory.lastSession.endNote) {
1221
+ log(` Note: ${memory.lastSession.endNote}`);
1222
+ }
1223
+ }
1224
+ const topicEntries = Object.entries(memory.topics);
1225
+ if (topicEntries.length > 0) {
1226
+ const completed = topicEntries.filter(([, t]) => t.status === "completed").length;
1227
+ const inProgress = topicEntries.filter(([, t]) => t.status === "in-progress").length;
1228
+ const pending = topicEntries.filter(([, t]) => t.status === "pending").length;
1229
+ log(`
1230
+ Topics: ${completed} completed, ${inProgress} in progress, ${pending} pending`);
1231
+ for (const [id, topic] of topicEntries) {
1232
+ const icon = topic.status === "completed" ? "\u2713" : topic.status === "in-progress" ? "\u2026" : "\u25CB";
1233
+ log(` ${icon} ${id}`);
1234
+ }
1235
+ }
1236
+ const exerciseEntries = Object.entries(memory.exercises);
1237
+ if (exerciseEntries.length > 0) {
1238
+ log(`
1239
+ Exercises:`);
1240
+ for (const [id, ex] of exerciseEntries) {
1241
+ const icon = ex.status === "completed" ? "\u2713" : ex.status === "in-progress" ? "\u2026" : "\u25CB";
1242
+ log(` ${icon} ${id}`);
1243
+ }
1244
+ }
1245
+ if (memory.frequentQuestions.length > 0) {
1246
+ log(`
1247
+ Frequent questions:`);
1248
+ for (const q of memory.frequentQuestions) {
1249
+ log(` (${q.count}x) ${q.question}`);
1250
+ }
1251
+ }
1252
+ if (memory.nextSteps.length > 0) {
1253
+ log(`
1254
+ Next steps:`);
1255
+ for (const step of memory.nextSteps) {
1256
+ log(` \u2192 ${step.topicId}: ${step.reason}`);
1257
+ }
1258
+ }
1259
+ log("");
1260
+ }
1261
+ function registerMemory(program2) {
1262
+ program2.command("memory").description("View and manage tutor memory").option("--reset", "Delete memory (keeps session logs)").option("--export", "Print memory as JSON to stdout").option("--import <file>", "Replace memory from a JSON file").option("--update <json>", "Merge a JSON patch into memory").option("--exercise <id>", "Update an exercise entry").option("--status <status>", "Exercise status (use with --exercise)").option("--next-step <desc>", "Add a next step recommendation").option("--reason <reason>", "Reason for the next step").action(async (opts) => {
1263
+ if (opts.reset) {
1264
+ const yes = await confirm2("Delete tutor memory? Session logs will be preserved.");
1265
+ if (!yes) {
1266
+ log("Reset cancelled.");
1267
+ return;
1268
+ }
1269
+ const memoryFile = join8(getBaseDir(), "memory.json");
1270
+ try {
1271
+ await rm(memoryFile);
1272
+ success("Memory reset successfully.");
1273
+ } catch {
1274
+ log("No memory file to delete.");
1275
+ }
1276
+ return;
1277
+ }
1278
+ if (opts.export) {
1279
+ const memory = await readMemory();
1280
+ if (!memory) {
1281
+ error("No memory file found.");
1282
+ process.exitCode = 1;
1283
+ return;
1284
+ }
1285
+ process.stdout.write(JSON.stringify(memory, null, 2) + "\n");
1286
+ return;
1287
+ }
1288
+ if (opts.import) {
1289
+ try {
1290
+ const raw = await readFile(opts.import, "utf-8");
1291
+ const data = JSON.parse(raw);
1292
+ await writeMemory(data);
1293
+ success("Memory imported successfully.");
1294
+ } catch (err) {
1295
+ error(`Failed to import: ${err instanceof Error ? err.message : err}`);
1296
+ process.exitCode = 1;
1297
+ }
1298
+ return;
1299
+ }
1300
+ if (opts.update) {
1301
+ try {
1302
+ const patch = JSON.parse(opts.update);
1303
+ await updateMemory(patch);
1304
+ success("Memory updated.");
1305
+ } catch (err) {
1306
+ error(`Invalid JSON: ${err instanceof Error ? err.message : err}`);
1307
+ process.exitCode = 1;
1308
+ }
1309
+ return;
1310
+ }
1311
+ if (opts.exercise) {
1312
+ const status = opts.status;
1313
+ if (!status) {
1314
+ error("--exercise requires --status <pending|in-progress|completed>");
1315
+ process.exitCode = 1;
1316
+ return;
1317
+ }
1318
+ const memory = await readMemory() ?? (await import("./tutor-memory-M6GNB3LQ.js")).getDefaultMemory();
1319
+ const existing = memory.exercises[opts.exercise] ?? { status: "pending" };
1320
+ existing.status = status;
1321
+ if (status === "completed") {
1322
+ existing.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1323
+ }
1324
+ if (status === "in-progress" && !existing.assignedAt) {
1325
+ existing.assignedAt = (/* @__PURE__ */ new Date()).toISOString();
1326
+ }
1327
+ memory.exercises[opts.exercise] = existing;
1328
+ await writeMemory(memory);
1329
+ success(`Exercise "${opts.exercise}" updated to ${status}.`);
1330
+ return;
1331
+ }
1332
+ if (opts.nextStep) {
1333
+ const memory = await readMemory() ?? (await import("./tutor-memory-M6GNB3LQ.js")).getDefaultMemory();
1334
+ memory.nextSteps.push({
1335
+ topicId: opts.nextStep,
1336
+ reason: opts.reason ?? "",
1337
+ suggestedAt: (/* @__PURE__ */ new Date()).toISOString()
1338
+ });
1339
+ await writeMemory(memory);
1340
+ success(`Next step added: ${opts.nextStep}`);
1341
+ return;
1342
+ }
1343
+ await showProgress();
1344
+ });
1345
+ }
1346
+
1347
+ // src/cli.ts
1348
+ var require2 = createRequire(import.meta.url);
1349
+ var pkg = require2("../package.json");
1350
+ var program = new Command();
1351
+ program.name("pulse").version(pkg.version).description(pkg.description).configureOutput({
1352
+ writeErr: (str) => process.stderr.write(str),
1353
+ writeOut: (str) => process.stdout.write(str)
1354
+ });
1355
+ registerInit(program);
1356
+ registerSync(program);
1357
+ registerGreet(program);
1358
+ registerList(program);
1359
+ registerStatus(program);
1360
+ registerUninstall(program);
1361
+ registerMemory(program);
1362
+ program.parse();