portable-agent-layer 0.58.0 → 0.59.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.
@@ -12,7 +12,9 @@ the single source of truth for skill quality.
12
12
 
13
13
  3. **Concise and concrete.** Assume the model is already smart — only add what it doesn't already know. Every step has a verb and an object; no "as needed" or "appropriately." Keep the SKILL.md body well under 500 lines; push long reference material into sibling files linked one level deep.
14
14
 
15
- A personal skill **may** contain this user's own contexttheir paths, project names, preferences, conventions. That is the point of a personal skill.
15
+ 4. **Self-contained and portable.** Everything the skill needs lives inside its own folder`SKILL.md` plus a `tools/` subdir for scripts and any reference files — so it travels intact on export/import. Don't reach into sibling skills or reference files outside the folder. Prefer `$HOME`/`~` or an env var over a hardcoded absolute path like `/Users/you/…` so the skill still works on another machine; `pal cli skill doctor` warns (never errors) on machine-specific absolute paths.
16
+
17
+ A personal skill **may** contain this user's own context — their paths, project names, preferences, conventions. That is the point of a personal skill; portability (rule 4) is a preference, not a hard rule — a deliberate machine-specific mount is fine.
16
18
 
17
19
  ## Skill anatomy
18
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.58.0",
3
+ "version": "0.59.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { existsSync, readdirSync, readFileSync } from "node:fs";
13
- import { basename, resolve } from "node:path";
13
+ import { basename, extname, relative, resolve } from "node:path";
14
14
  import { palHome } from "../hooks/lib/paths";
15
15
 
16
16
  type Level = "pass" | "warn" | "error";
@@ -41,6 +41,44 @@ const MAX_NAME = 64;
41
41
  const MAX_DESCRIPTION = 1024;
42
42
  const MAX_BODY_LINES = 500;
43
43
 
44
+ /** File extensions worth scanning for hardcoded paths (SKILL.md + its scripts). */
45
+ const SCANNABLE_EXT = new Set([".md", ".ts", ".js", ".mjs", ".cjs", ".sh", ".py"]);
46
+
47
+ /** Machine/user-specific absolute paths that will not survive an export to
48
+ * another machine or user: POSIX home dirs and Windows user profiles. Portable
49
+ * forms ($HOME, ~, %USERPROFILE%, env vars) are deliberately not matched. */
50
+ const ABSOLUTE_PATH_RE =
51
+ /(?:\/(?:Users|home)\/[A-Za-z0-9._-]+|\/root\/[A-Za-z0-9._-]|[A-Za-z]:\\Users\\[A-Za-z0-9._-]+)/;
52
+
53
+ /** Collect SKILL.md and sibling script files, skipping vendored/VCS trees. */
54
+ function collectSkillFiles(skillDir: string): string[] {
55
+ const out: string[] = [];
56
+ const walk = (dir: string) => {
57
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
58
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
59
+ const full = resolve(dir, entry.name);
60
+ if (entry.isDirectory()) walk(full);
61
+ else if (entry.isFile() && SCANNABLE_EXT.has(extname(entry.name))) out.push(full);
62
+ }
63
+ };
64
+ walk(skillDir);
65
+ return out;
66
+ }
67
+
68
+ /** Find machine-specific absolute paths across a skill's files. */
69
+ function findAbsolutePaths(skillDir: string): string[] {
70
+ const hits: string[] = [];
71
+ for (const file of collectSkillFiles(skillDir)) {
72
+ const rel = relative(skillDir, file).replaceAll("\\", "/");
73
+ const lines = readFileSync(file, "utf-8").split("\n");
74
+ for (let i = 0; i < lines.length; i++) {
75
+ const m = ABSOLUTE_PATH_RE.exec(lines[i]);
76
+ if (m) hits.push(`${rel}:${i + 1} → ${m[0]}`);
77
+ }
78
+ }
79
+ return hits;
80
+ }
81
+
44
82
  /** Split a SKILL.md into frontmatter fields and body. */
45
83
  function parseSkill(content: string): ParsedSkill {
46
84
  const parts = content.split(/^---\s*$/m);
@@ -238,6 +276,23 @@ export function lintSkill(skillDir: string): DoctorReport {
238
276
  ? add("warn", "paths", "Windows-style backslash path found — use forward slashes")
239
277
  : add("pass", "paths", "forward-slash paths");
240
278
 
279
+ // ── machine-specific absolute paths (portability) ──
280
+ // A personal skill MAY legitimately hardcode a machine-specific path (e.g. a
281
+ // cloud-mount vault), so this is a warning, never an error — it flags paths
282
+ // that will not survive being exported to another machine or user.
283
+ const absHits = findAbsolutePaths(skillDir);
284
+ if (absHits.length > 0) {
285
+ const shown = absHits.slice(0, 3).join("; ");
286
+ const more = absHits.length > 3 ? ` (+${absHits.length - 3} more)` : "";
287
+ add(
288
+ "warn",
289
+ "paths.absolute",
290
+ `hardcoded absolute path(s) that won't be portable across machines: ${shown}${more} — prefer $HOME/~ or an env var, or ignore if this is an intentional machine-specific mount`
291
+ );
292
+ } else {
293
+ add("pass", "paths.absolute", "no machine-specific absolute paths");
294
+ }
295
+
241
296
  const errors = findings.filter((f) => f.level === "error").length;
242
297
  const warnings = findings.filter((f) => f.level === "warn").length;
243
298
  return { dir: skillDir, name, findings, errors, warnings };