@maccesar/aiskills 1.18.1 → 1.20.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.
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env python3
2
+ """Audit how a repository publishes to npm and what it installs from it.
3
+
4
+ Reads the repo, the npm configuration and the GitHub Actions setup, and reports
5
+ the state of each: credentials on disk, orphaned Actions secrets, how the
6
+ publishing workflow authenticates, version files that disagree, README badges
7
+ pointing at a package that does not exist, dependencies that run scripts at
8
+ install time, and the local npm version against v12.
9
+
10
+ python3 auditar_npm.py # the repo in the current directory
11
+ python3 auditar_npm.py ~/code/foo
12
+ python3 auditar_npm.py --no-network # skip the registry and gh lookups
13
+
14
+ Writes nothing. Every finding is a proposal for the user to approve.
15
+
16
+ Standard library only: runs on any macOS or Linux with Python 3.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import re
25
+ import subprocess
26
+ import sys
27
+ import urllib.error
28
+ import urllib.request
29
+ from pathlib import Path
30
+
31
+ OK, FALTA, REVISAR, INFO = "ok", "falta", "revisar", "info"
32
+
33
+ SIMBOLO = {OK: " ok ", FALTA: " MISS ", REVISAR: "CHECK ", INFO: " info "}
34
+
35
+ # Names that look like an npm publishing credential. Matched case-insensitively
36
+ # against secret names; the value of a secret is never read, here or anywhere.
37
+ PATRON_SECRETO_NPM = re.compile(r"NPM.*(TOKEN|AUTH|PUBLISH)|(TOKEN|AUTH).*NPM", re.I)
38
+
39
+ # The first npm version whose install-time defaults are the ones described in
40
+ # references/install-defaults.md.
41
+ NPM_V12 = 12
42
+
43
+ CONSULTAR_RED = True
44
+
45
+
46
+ # ---------------------------------------------------------------- utilities
47
+
48
+
49
+ def correr(comando: list[str], tiempo: int = 20) -> tuple[int, str]:
50
+ """Run a command and return (exit code, stdout). 127 means it is not installed."""
51
+ try:
52
+ r = subprocess.run(
53
+ comando, capture_output=True, text=True, timeout=tiempo, check=False
54
+ )
55
+ return r.returncode, r.stdout.strip()
56
+ except FileNotFoundError:
57
+ return 127, ""
58
+ except subprocess.TimeoutExpired:
59
+ return 124, ""
60
+
61
+
62
+ def existe(programa: str) -> bool:
63
+ return correr([programa, "--version"], tiempo=10)[0] not in (127, 124)
64
+
65
+
66
+ def leer_json(ruta: Path) -> dict | None:
67
+ try:
68
+ return json.loads(ruta.read_text(encoding="utf-8"))
69
+ except Exception:
70
+ return None
71
+
72
+
73
+ def leer_texto(ruta: Path) -> str:
74
+ try:
75
+ return ruta.read_text(encoding="utf-8", errors="replace")
76
+ except Exception:
77
+ return ""
78
+
79
+
80
+ def pedir(url: str, tiempo: int = 10) -> int | None:
81
+ """HTTP status of a URL, or None if it could not be reached."""
82
+ if not CONSULTAR_RED:
83
+ return None
84
+ peticion = urllib.request.Request(url, headers={"User-Agent": "auditar_npm"})
85
+ try:
86
+ with urllib.request.urlopen(peticion, timeout=tiempo) as r:
87
+ return r.status
88
+ except urllib.error.HTTPError as e:
89
+ return e.code
90
+ except Exception:
91
+ return None
92
+
93
+
94
+ class Reporte:
95
+ """Collects findings by section and prints them once, so the output reads as
96
+ a report rather than as a log of the order the checks happened to run in."""
97
+
98
+ def __init__(self) -> None:
99
+ self.secciones: list[tuple[str, list[tuple[str, str, str]]]] = []
100
+
101
+ def abrir(self, titulo: str) -> None:
102
+ self.secciones.append((titulo, []))
103
+
104
+ def add(self, estado: str, etiqueta: str, detalle: str = "") -> None:
105
+ if not self.secciones:
106
+ self.abrir("General")
107
+ self.secciones[-1][1].append((estado, etiqueta, detalle))
108
+
109
+ def imprimir(self) -> int:
110
+ faltantes = 0
111
+ for titulo, filas in self.secciones:
112
+ if not filas:
113
+ continue
114
+ print(f"\n{titulo}")
115
+ print("-" * len(titulo))
116
+ for estado, etiqueta, detalle in filas:
117
+ faltantes += estado == FALTA
118
+ linea = f"[{SIMBOLO[estado]}] {etiqueta}"
119
+ if detalle:
120
+ linea += f" — {detalle}"
121
+ print(linea)
122
+ print()
123
+ return faltantes
124
+
125
+
126
+ # ---------------------------------------------------------------- checks
127
+
128
+
129
+ def revisar_npmrc(proyecto: Path, r: Reporte) -> None:
130
+ """Credentials on disk, and whether the registry still accepts them.
131
+
132
+ A token line is reported by its presence only. Reading or printing the value
133
+ would put a live credential in a transcript, which is how a token becomes one
134
+ that has to be revoked.
135
+ """
136
+ r.abrir("npm configuration")
137
+
138
+ for etiqueta, ruta in (
139
+ ("~/.npmrc", Path.home() / ".npmrc"),
140
+ (".npmrc (project)", proyecto / ".npmrc"),
141
+ ):
142
+ if not ruta.exists():
143
+ r.add(INFO, etiqueta, "not present")
144
+ continue
145
+
146
+ texto = leer_texto(ruta)
147
+ if re.search(r"^\s*(//.*:)?_auth(Token)?\s*=", texto, re.M):
148
+ r.add(
149
+ REVISAR,
150
+ f"{etiqueta}: token line",
151
+ "an _authToken is present. Classic tokens were revoked in Dec 2025; "
152
+ "if this predates that, it is dead weight producing 401s (npm logout)",
153
+ )
154
+ else:
155
+ r.add(OK, f"{etiqueta}: no token line", "")
156
+
157
+ if re.search(r"^\s*ignore-scripts\s*=\s*true", texto, re.M):
158
+ r.add(
159
+ INFO,
160
+ f"{etiqueta}: ignore-scripts=true",
161
+ "this machine already behaves like npm v12; CI probably does not",
162
+ )
163
+
164
+ if CONSULTAR_RED and existe("npm"):
165
+ codigo, salida = correr(["npm", "whoami"])
166
+ if codigo == 0 and salida:
167
+ r.add(OK, "npm session", f"authenticated as {salida}")
168
+ else:
169
+ r.add(
170
+ INFO,
171
+ "npm session",
172
+ "not authenticated (npm login opens a two-hour session; "
173
+ "irrelevant if publishing happens from Actions)",
174
+ )
175
+
176
+
177
+ def version_npm(r: Reporte) -> None:
178
+ r.abrir("npm version")
179
+
180
+ codigo, salida = correr(["npm", "--version"])
181
+ if codigo != 0 or not salida:
182
+ r.add(REVISAR, "npm", "not found on PATH")
183
+ return
184
+
185
+ try:
186
+ mayor = int(salida.split(".")[0])
187
+ except ValueError:
188
+ r.add(INFO, "npm", salida)
189
+ return
190
+
191
+ if mayor >= NPM_V12:
192
+ r.add(OK, "npm", f"{salida} — install-time defaults are on")
193
+ else:
194
+ r.add(
195
+ INFO,
196
+ "npm",
197
+ f"{salida} — v12 defaults not applied yet; 11.16.0+ warns about them first",
198
+ )
199
+
200
+
201
+ def repo_github(proyecto: Path) -> str | None:
202
+ """owner/repo as GitHub spells it, read from the API rather than the folder name."""
203
+ codigo, salida = correr(
204
+ ["git", "-C", str(proyecto), "remote", "get-url", "origin"]
205
+ )
206
+ if codigo != 0 or not salida:
207
+ return None
208
+
209
+ m = re.search(r"github\.com[:/]+([^/]+)/(.+?)(?:\.git)?$", salida)
210
+ if not m:
211
+ return None
212
+ local = f"{m.group(1)}/{m.group(2)}"
213
+
214
+ if CONSULTAR_RED and existe("gh"):
215
+ codigo, canonico = correr(["gh", "api", f"repos/{local}", "--jq", ".full_name"])
216
+ if codigo == 0 and canonico:
217
+ return canonico
218
+ return local
219
+
220
+
221
+ def revisar_secretos(proyecto: Path, repo: str | None, r: Reporte) -> None:
222
+ """Actions secrets that look like npm credentials, and whether anything uses them."""
223
+ r.abrir("GitHub Actions secrets")
224
+
225
+ if not repo:
226
+ r.add(INFO, "repository", "no GitHub remote — skipping")
227
+ return
228
+ if not CONSULTAR_RED:
229
+ r.add(INFO, "secrets", "skipped (--no-network)")
230
+ return
231
+ if not existe("gh"):
232
+ r.add(INFO, "secrets", "gh is not installed — cannot list them")
233
+ return
234
+
235
+ codigo, salida = correr(["gh", "secret", "list", "--repo", repo])
236
+ if codigo != 0:
237
+ r.add(INFO, "secrets", f"could not read them for {repo}")
238
+ return
239
+
240
+ nombres = [l.split()[0] for l in salida.splitlines() if l.strip()]
241
+ sospechosos = [n for n in nombres if PATRON_SECRETO_NPM.search(n)]
242
+
243
+ if not sospechosos:
244
+ r.add(OK, "no npm credentials stored", f"{len(nombres)} secret(s) in {repo}")
245
+ return
246
+
247
+ usados = leer_texto_workflows(proyecto)
248
+ for nombre in sospechosos:
249
+ if nombre in usados:
250
+ r.add(
251
+ REVISAR,
252
+ f"secret {nombre}",
253
+ "referenced by a workflow — token auth, replaceable by OIDC",
254
+ )
255
+ else:
256
+ r.add(
257
+ FALTA,
258
+ f"secret {nombre} is orphaned",
259
+ f"no workflow references it. gh secret delete {nombre} --repo {repo}",
260
+ )
261
+
262
+
263
+ def leer_texto_workflows(proyecto: Path) -> str:
264
+ carpeta = proyecto / ".github" / "workflows"
265
+ if not carpeta.is_dir():
266
+ return ""
267
+ return "\n".join(sin_comentarios(leer_texto(f)) for f in sorted(carpeta.glob("*.y*ml")))
268
+
269
+
270
+ def sin_comentarios(texto: str) -> str:
271
+ """YAML with its comments removed.
272
+
273
+ A well-commented trusted-publishing workflow explains that it carries no
274
+ NPM_TOKEN — and reading that sentence as a credential reference is exactly
275
+ how a correct file gets reported as broken.
276
+ """
277
+ texto = re.sub(r"(?m)^\s*#.*$", "", texto)
278
+ return re.sub(r"(?m)\s#.*$", "", texto)
279
+
280
+
281
+ def revisar_workflows(proyecto: Path, r: Reporte) -> None:
282
+ """Which workflow publishes, and how it proves who it is."""
283
+ r.abrir("Publishing workflow")
284
+
285
+ carpeta = proyecto / ".github" / "workflows"
286
+ archivos = sorted(carpeta.glob("*.y*ml")) if carpeta.is_dir() else []
287
+
288
+ # A package.json marked private is an application or a toolchain, not
289
+ # something that gets published — the absence of a publishing workflow is
290
+ # the correct state, not a finding.
291
+ pkg = leer_json(proyecto / "package.json") or {}
292
+ if pkg.get("private") is True:
293
+ r.add(INFO, "private package", "never published; nothing to automate")
294
+ return
295
+
296
+ if not archivos:
297
+ r.add(
298
+ FALTA,
299
+ "no workflow publishes this package",
300
+ "every release needs an interactive login (two-hour session). "
301
+ "See references/trusted-publishing.md",
302
+ )
303
+ return
304
+
305
+ publicadores = [f for f in archivos if "npm publish" in sin_comentarios(leer_texto(f))]
306
+ if not publicadores:
307
+ r.add(
308
+ FALTA,
309
+ "no workflow runs npm publish",
310
+ f"{len(archivos)} workflow(s) present, none publishes",
311
+ )
312
+ return
313
+
314
+ for archivo in publicadores:
315
+ texto = sin_comentarios(leer_texto(archivo))
316
+ nombre = archivo.name
317
+
318
+ oidc = re.search(r"^\s*id-token:\s*write", texto, re.M)
319
+ token = re.search(r"NODE_AUTH_TOKEN|NPM_TOKEN|secrets\.\w*NPM", texto)
320
+
321
+ if oidc and not token:
322
+ r.add(OK, f"{nombre}: OIDC", "trusted publishing, no stored secret")
323
+ elif oidc and token:
324
+ r.add(
325
+ REVISAR,
326
+ f"{nombre}: OIDC and a token",
327
+ "a token reference puts the publish back on token auth and drops provenance",
328
+ )
329
+ elif token:
330
+ r.add(
331
+ FALTA,
332
+ f"{nombre}: token auth",
333
+ "long-lived credential; 2FA-bypass tokens lose direct publish ~Jan 2027",
334
+ )
335
+ else:
336
+ r.add(REVISAR, f"{nombre}: no visible credential", "check how it authenticates")
337
+
338
+ if re.search(r"tags:\s*$|-\s*['\"]?v\*", texto, re.M):
339
+ r.add(OK, f"{nombre}: trigger", "runs on a pushed tag")
340
+ else:
341
+ r.add(REVISAR, f"{nombre}: trigger", "does not look tag-driven")
342
+
343
+ if not re.search(r"GITHUB_REF_NAME|github\.ref_name", texto):
344
+ r.add(
345
+ REVISAR,
346
+ f"{nombre}: no version guard",
347
+ "nothing compares the tag against the version files before publishing",
348
+ )
349
+
350
+
351
+ def revisar_paquete(proyecto: Path, r: Reporte) -> None:
352
+ """The manifest itself: scope, and version files that must agree."""
353
+ r.abrir("Package manifest")
354
+
355
+ pkg = leer_json(proyecto / "package.json")
356
+ if not pkg:
357
+ r.add(INFO, "package.json", "not an npm project")
358
+ return
359
+
360
+ nombre = pkg.get("name", "")
361
+ version = pkg.get("version", "")
362
+ r.add(INFO, "package", f"{nombre}@{version}")
363
+
364
+ if nombre.startswith("@"):
365
+ r.add(
366
+ INFO,
367
+ "scoped package",
368
+ "badges and registry URLs must carry the scope; the unscoped name is a different package",
369
+ )
370
+
371
+ plugin_path = proyecto / ".claude-plugin" / "plugin.json"
372
+ if plugin_path.exists():
373
+ plugin = leer_json(plugin_path) or {}
374
+ if plugin.get("version") == version:
375
+ r.add(OK, "plugin.json version", f"in sync at {version}")
376
+ else:
377
+ r.add(
378
+ FALTA,
379
+ "plugin.json is out of sync",
380
+ f"package.json {version} vs plugin.json {plugin.get('version')} — "
381
+ "marketplace users keep the cached old code",
382
+ )
383
+
384
+
385
+ def revisar_badges(proyecto: Path, r: Reporte) -> None:
386
+ """shields.io badges asking the registry for a package name that does not exist.
387
+
388
+ The failure is silent: shields renders "package not found" instead of an error,
389
+ so a broken badge survives for months and hides whatever it was reporting.
390
+ """
391
+ r.abrir("README badges")
392
+
393
+ readme = proyecto / "README.md"
394
+ if not readme.exists():
395
+ r.add(INFO, "README.md", "not present")
396
+ return
397
+
398
+ texto = leer_texto(readme)
399
+ nombres = set(re.findall(r"img\.shields\.io/npm/[a-z]+/([^)\s\]]+)", texto))
400
+ if not nombres:
401
+ r.add(INFO, "npm badges", "none")
402
+ return
403
+
404
+ pkg = leer_json(proyecto / "package.json") or {}
405
+ esperado = pkg.get("name", "")
406
+
407
+ for crudo in sorted(nombres):
408
+ nombre = crudo.replace("%2F", "/").rstrip("?").split("?")[0]
409
+ if esperado and nombre == esperado:
410
+ r.add(OK, f"badge {nombre}", "matches package.json")
411
+ continue
412
+
413
+ estado = pedir(f"https://registry.npmjs.org/{nombre.replace('/', '%2F')}")
414
+ if estado == 200:
415
+ r.add(REVISAR, f"badge {nombre}", f"resolves, but package.json says {esperado}")
416
+ elif estado is None:
417
+ r.add(REVISAR, f"badge {nombre}", f"could not verify; package.json says {esperado}")
418
+ else:
419
+ r.add(
420
+ FALTA,
421
+ f"badge {nombre} points at nothing",
422
+ f"registry answers {estado}; this package is {esperado}",
423
+ )
424
+
425
+
426
+ def revisar_scripts_instalacion(proyecto: Path, r: Reporte) -> None:
427
+ """What a user on npm v12 would be asked to approve when installing this tree."""
428
+ r.abrir("Install-time scripts (npm v12)")
429
+
430
+ modulos = proyecto / "node_modules"
431
+ if not modulos.is_dir():
432
+ r.add(INFO, "node_modules", "not installed — run npm install to measure this")
433
+ return
434
+
435
+ con_scripts: list[str] = []
436
+ node_gyp: list[str] = []
437
+
438
+ for manifiesto in modulos.glob("*/package.json"):
439
+ datos = leer_json(manifiesto)
440
+ if not datos:
441
+ continue
442
+ scripts = datos.get("scripts") or {}
443
+ if any(k in scripts for k in ("preinstall", "install", "postinstall")):
444
+ con_scripts.append(datos.get("name", manifiesto.parent.name))
445
+ if "node-gyp" in json.dumps(datos.get("dependencies") or {}):
446
+ node_gyp.append(datos.get("name", manifiesto.parent.name))
447
+
448
+ # Scoped packages live one level deeper.
449
+ for manifiesto in modulos.glob("@*/*/package.json"):
450
+ datos = leer_json(manifiesto)
451
+ if not datos:
452
+ continue
453
+ scripts = datos.get("scripts") or {}
454
+ if any(k in scripts for k in ("preinstall", "install", "postinstall")):
455
+ con_scripts.append(datos.get("name", manifiesto.parent.name))
456
+
457
+ if not con_scripts and not node_gyp:
458
+ r.add(OK, "no dependency runs install scripts", "npm v12 installs this cleanly")
459
+ return
460
+
461
+ for nombre in sorted(set(con_scripts)):
462
+ r.add(REVISAR, f"{nombre} declares an install script", "needs approval on npm v12")
463
+ for nombre in sorted(set(node_gyp) - set(con_scripts)):
464
+ r.add(REVISAR, f"{nombre} pulls node-gyp", "implicit build, off by default on npm v12")
465
+
466
+ r.add(
467
+ INFO,
468
+ "authoritative check",
469
+ "npm approve-scripts --allow-scripts-pending, then commit the allowlist",
470
+ )
471
+
472
+
473
+ def main() -> int:
474
+ global CONSULTAR_RED
475
+
476
+ p = argparse.ArgumentParser(
477
+ description="Audit how a repository publishes to npm and what it installs from it."
478
+ )
479
+ p.add_argument("ruta", nargs="?", default=".", help="project directory (default: .)")
480
+ p.add_argument(
481
+ "--no-network",
482
+ action="store_true",
483
+ help="skip the registry, gh and npm whoami lookups",
484
+ )
485
+ args = p.parse_args()
486
+
487
+ CONSULTAR_RED = not args.no_network
488
+
489
+ proyecto = Path(os.path.expanduser(args.ruta)).resolve()
490
+ if not proyecto.is_dir():
491
+ p.error(f"{args.ruta} is not a directory")
492
+
493
+ print(f"\nnpm supply-chain audit of {proyecto}")
494
+
495
+ r = Reporte()
496
+ version_npm(r)
497
+ revisar_npmrc(proyecto, r)
498
+ revisar_paquete(proyecto, r)
499
+ revisar_workflows(proyecto, r)
500
+ revisar_secretos(proyecto, repo_github(proyecto), r)
501
+ revisar_badges(proyecto, r)
502
+ revisar_scripts_instalacion(proyecto, r)
503
+
504
+ faltan = r.imprimir()
505
+ return 1 if faltan else 0
506
+
507
+
508
+ if __name__ == "__main__":
509
+ sys.exit(main())
@@ -25,10 +25,12 @@ The SKILL.md alone is an **index** of references. The detail you need to give ac
25
25
 
26
26
  ### Step 2 — Output contract
27
27
 
28
- Every design recommendation, ratio, value, or rule you cite MUST be backed by a citation in the form:
28
+ Every design recommendation, ratio, value, or rule you cite carries its citation inline:
29
29
 
30
30
  `[source: references/<file>.md]`
31
31
 
32
+ The citation is what separates a value you read from one that merely sounded right — written down, the two look identical, and the reader has no way to tell them apart. Cite while writing rather than collecting sources at the end, because by then you are reconstructing where something came from instead of recording it.
33
+
32
34
  Example: *"Use weight and color, not just font size, to establish hierarchy [source: references/02-page-mechanics.md]"*
33
35
 
34
36
  ### Step 3 — If you must answer from memory
@@ -2,6 +2,7 @@
2
2
  name: seo-launch
3
3
  description: 'Audit and then install everything a site needs to be indexed and to render a proper card when its link is shared: head tags, Open Graph and Twitter card, the 1200x630 og:image, favicon and apple-touch-icon, robots.txt, sitemap.xml, an .htaccess with one canonical domain, JSON-LD, and the Search Console handover. Works on static sites and on Laravel or plain PHP projects. Use when the user says the link shows a grey box with no preview in WhatsApp, asks why Google cannot find the site, is putting a new domain live, or asks for an SEO review, meta tags, og:image, sitemap or robots.txt — even when they never say "SEO". Not for: keyword research, writing the content itself, backlinks, paid ads, or analytics dashboards.'
4
4
  allowed-tools: Read, Grep, Glob, Bash, Edit, Write, AskUserQuestion
5
+ compatibility: Requires Python 3 (standard library only) and network access to audit a live site. Stage 2 uses ImageMagick to generate the images.
5
6
  ---
6
7
 
7
8
  # SEO Launch
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: stitch-showcase
3
3
  description: 'Turns Google Stitch design exports (zips holding `code.html` + `screen.png`) into a navigable showcase — gallery, viewer, component catalog — in about three seconds, and enriches it on demand. Use this for anything involving those exports: "organiza mis diseños de Stitch", "arma el muestrario", "organize my Stitch designs", "build the showcase", "tengo los zips de Stitch", "mis exports de Stitch", or a bare path to a folder of design zips. Also for maintaining one that already exists: "optimiza el showcase", "mejora las descripciones", "agrega estas pantallas nuevas", "el cliente pidió otra pantalla", "estandariza los navbars", "make all the footers the same". Not for: Figma or Sketch exports, loose screenshots, redesigning the screens themselves, or building the real app from them.'
4
+ compatibility: Requires Python 3 (standard library only) to extract the zips and build the showcase.
4
5
  ---
5
6
 
6
7
  # stitch-showcase
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vscode-extension-dev
3
- description: 'VS Code extension development grounded in the official Extension API docs. Use this whenever someone is creating, scaffolding, debugging, testing, bundling or publishing a VS Code extension — TreeView, QuickPick, Webview, StatusBar, SecretStorage, Language Server Protocol, Debug Adapter Protocol, notebooks — and also when they never say "extension" but the work clearly is one: editing `package.json` `contributes` / `activationEvents` / `keybindings`, an `activate(context)` function, importing from `vscode`, leaking disposables, `yo code`, `vsce`, `.vscodeignore`, bundling with esbuild, publishing to the Marketplace or Open VSX, Webview CSP/nonce/postMessage, or testing with @vscode/test-electron. Not for: configuring your own editor, Claude Code plugins or MCP servers, or general TypeScript/Node questions with no extension host involved.'
3
+ description: 'VS Code extension development grounded in the official Extension API docs. Use this whenever someone is creating, scaffolding, designing, debugging, testing, bundling or publishing a VS Code extension — TreeView, QuickPick, Webview, StatusBar, commands, configuration, SecretStorage, progress indicators, FileSystemWatcher, Diagnostics, Language Server Protocol, Debug Adapter Protocol, notebooks — and also when they never say "extension" but the work clearly is one: editing `package.json` `contributes` / `activationEvents` / `keybindings`, an `activate(context)` function, importing from `vscode`, leaking disposables, `yo code`, `vsce`, `.vscodeignore`, bundling with esbuild or webpack, publishing to the Marketplace or Open VSX, Webview CSP/nonce/postMessage, or testing with @vscode/test-electron. Not for: configuring your own editor (user settings, keybindings, installing extensions), Claude Code plugins, skills or MCP servers, or general TypeScript/Node questions with no extension host involved.'
4
4
  ---
5
5
 
6
6
  # VS Code Extension Development Skill
@@ -32,10 +32,12 @@ The SKILL.md alone is an **index** of references. The detail you need to give ac
32
32
 
33
33
  ### Step 2 — Output contract
34
34
 
35
- Every API symbol, configuration key, command, or behavior you cite MUST be backed by a citation in the form:
35
+ Every API symbol, configuration key, command, or behavior you cite carries its citation inline:
36
36
 
37
37
  `[source: references/<file>.md]`
38
38
 
39
+ The citation is what separates a value you read from one that merely sounded right — written down, the two look identical, and the reader has no way to tell them apart. Cite while writing rather than collecting sources at the end, because by then you are reconstructing where something came from instead of recording it.
40
+
39
41
  Example: *"Push all subscriptions to `context.subscriptions` so they are disposed on deactivation [source: references/api-additional.md]"*
40
42
 
41
43
  ### Step 3 — If you must answer from memory