@cremini/skillpack 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,11 +92,13 @@ Multiple skill names from the same source can be listed comma-separated.
92
92
 
93
93
  ## Zip Output
94
94
 
95
- The archive produced by `zip` is intentionally minimal:
95
+ The archive produced by `zip` is intentionally lightweight:
96
96
 
97
97
  ```text
98
98
  <pack-name>/
99
99
  ├── skillpack.json # Pack configuration
100
+ ├── AGENTS.md # Optional pack policy
101
+ ├── SOUL.md # Optional pack persona
100
102
  ├── skills/ # Installed skills
101
103
  ├── start.sh # One-click launcher for macOS / Linux
102
104
  └── start.bat # One-click launcher for Windows
@@ -104,6 +106,8 @@ The archive produced by `zip` is intentionally minimal:
104
106
 
105
107
  The start scripts use `npx @cremini/skillpack run .` so Node.js is the only prerequisite — no pre-bundled server directory is included.
106
108
 
109
+ If present, `AGENTS.md` and `SOUL.md` are read by SkillPack itself when a new chat session starts. SkillPack injects them into the runtime system prompt as pack-level policy and persona, without depending on the host machine's `AGENTS.md`, `.pi/SYSTEM.md`, or `APPEND_SYSTEM.md`.
110
+
107
111
  ## Slack/Telegram Integrations
108
112
 
109
113
  **Slack Configuration**: requires Slack `App Token` and `Bot Token`<br>
package/dist/cli.js CHANGED
@@ -23,6 +23,7 @@ var init_config = __esm({
23
23
  authType: "api_key",
24
24
  envKey: "OPENAI_API_KEY",
25
25
  placeholder: "sk-proj-...",
26
+ baseUrlPlaceholder: "https://api.openai.com/v1",
26
27
  supportsBaseUrl: true
27
28
  },
28
29
  anthropic: {
@@ -31,6 +32,7 @@ var init_config = __esm({
31
32
  authType: "api_key",
32
33
  envKey: "ANTHROPIC_API_KEY",
33
34
  placeholder: "sk-ant-api03-...",
35
+ baseUrlPlaceholder: "https://api.anthropic.com",
34
36
  supportsBaseUrl: true
35
37
  },
36
38
  google: {
@@ -1785,20 +1787,120 @@ function parseSkillMd(filePath) {
1785
1787
  return null;
1786
1788
  }
1787
1789
  const frontmatter = frontmatterMatch[1];
1788
- const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
1789
- const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
1790
- if (!nameMatch) {
1790
+ const name = readFrontmatterField(frontmatter, "name");
1791
+ if (!name) {
1791
1792
  return null;
1792
1793
  }
1793
1794
  return {
1794
- name: nameMatch[1].trim(),
1795
- description: descMatch ? descMatch[1].trim() : "",
1795
+ name,
1796
+ description: readFrontmatterField(frontmatter, "description") ?? "",
1796
1797
  dir: path2.dirname(filePath)
1797
1798
  };
1798
1799
  } catch {
1799
1800
  return null;
1800
1801
  }
1801
1802
  }
1803
+ function readFrontmatterField(frontmatter, field) {
1804
+ const lines = frontmatter.split(/\r?\n/);
1805
+ for (let index = 0; index < lines.length; index += 1) {
1806
+ const match = lines[index].match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
1807
+ if (!match || match[1] !== field) {
1808
+ continue;
1809
+ }
1810
+ const rawValue = (match[2] ?? "").trim();
1811
+ if (isBlockScalar(rawValue)) {
1812
+ const [value] = readBlockScalar(lines, index + 1, rawValue);
1813
+ return value;
1814
+ }
1815
+ if (rawValue === "") {
1816
+ const [value] = readIndentedScalar(lines, index + 1);
1817
+ return value;
1818
+ }
1819
+ return stripWrappingQuotes(rawValue);
1820
+ }
1821
+ return null;
1822
+ }
1823
+ function isBlockScalar(value) {
1824
+ return /^[>|][0-9+-]*$/.test(value);
1825
+ }
1826
+ function readBlockScalar(lines, startIndex, marker) {
1827
+ const blockLines = [];
1828
+ let index = startIndex;
1829
+ while (index < lines.length) {
1830
+ const line = lines[index];
1831
+ if (line.trim() === "") {
1832
+ blockLines.push("");
1833
+ index += 1;
1834
+ continue;
1835
+ }
1836
+ if (!/^\s/.test(line)) {
1837
+ break;
1838
+ }
1839
+ blockLines.push(line);
1840
+ index += 1;
1841
+ }
1842
+ const normalized = normalizeBlockIndent(blockLines);
1843
+ const style = marker[0];
1844
+ const chomp = marker.includes("-") ? "strip" : marker.includes("+") ? "keep" : "clip";
1845
+ const value = style === ">" ? foldBlockScalar(normalized) : normalized.join("\n");
1846
+ return [applyChomp(value, chomp), index];
1847
+ }
1848
+ function readIndentedScalar(lines, startIndex) {
1849
+ const blockLines = [];
1850
+ let index = startIndex;
1851
+ while (index < lines.length) {
1852
+ const line = lines[index];
1853
+ if (line.trim() === "") {
1854
+ blockLines.push("");
1855
+ index += 1;
1856
+ continue;
1857
+ }
1858
+ if (!/^\s/.test(line)) {
1859
+ break;
1860
+ }
1861
+ blockLines.push(line);
1862
+ index += 1;
1863
+ }
1864
+ return [foldBlockScalar(normalizeBlockIndent(blockLines)), index];
1865
+ }
1866
+ function normalizeBlockIndent(lines) {
1867
+ const indents = lines.filter((line) => line.trim() !== "").map((line) => line.match(/^[ \t]*/)[0].length);
1868
+ const trimLength = indents.length > 0 ? Math.min(...indents) : 0;
1869
+ return lines.map((line) => line.slice(trimLength));
1870
+ }
1871
+ function foldBlockScalar(lines) {
1872
+ let result = "";
1873
+ let previousBlank = false;
1874
+ for (const line of lines) {
1875
+ const isBlank = line.trim() === "";
1876
+ if (isBlank) {
1877
+ result += "\n";
1878
+ previousBlank = true;
1879
+ continue;
1880
+ }
1881
+ if (result !== "" && !previousBlank) {
1882
+ result += " ";
1883
+ }
1884
+ result += line;
1885
+ previousBlank = false;
1886
+ }
1887
+ return result;
1888
+ }
1889
+ function applyChomp(value, mode) {
1890
+ if (mode === "keep") {
1891
+ return value;
1892
+ }
1893
+ if (mode === "strip") {
1894
+ return value.replace(/\n+$/g, "");
1895
+ }
1896
+ return value.replace(/\n*$/g, "");
1897
+ }
1898
+ function stripWrappingQuotes(value) {
1899
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1900
+ return value.slice(1, -1);
1901
+ }
1902
+ return value;
1903
+ }
1802
1904
  function syncSkillDescriptions(workDir, config) {
1803
1905
  const descriptionByName = /* @__PURE__ */ new Map();
1804
1906
  for (const skill of scanInstalledSkills(workDir)) {
@@ -1878,6 +1980,12 @@ async function zipCommand(workDir) {
1878
1980
  archive.file(getPackPath(workDir), {
1879
1981
  name: `${prefix}/${PACK_FILE}`
1880
1982
  });
1983
+ for (const file of ["AGENTS.md", "SOUL.md"]) {
1984
+ const filePath = path3.join(workDir, file);
1985
+ if (fs3.existsSync(filePath)) {
1986
+ archive.file(filePath, { name: `${prefix}/${file}` });
1987
+ }
1988
+ }
1881
1989
  const skillsDir = path3.join(workDir, "skills");
1882
1990
  if (fs3.existsSync(skillsDir)) {
1883
1991
  archive.directory(skillsDir, `${prefix}/skills`);
@@ -5016,6 +5124,8 @@ var BUILTIN_SKILL_CREATOR_DESCRIPTION = "Create new skills, modify and improve e
5016
5124
  var BUILTIN_SKILL_CREATOR_TEMPLATE_DIR = fileURLToPath(
5017
5125
  new URL("../templates/builtin-skills/skill-creator", import.meta.url)
5018
5126
  );
5127
+ var PACK_AGENTS_FILE = "AGENTS.md";
5128
+ var PACK_SOUL_FILE = "SOUL.md";
5019
5129
  function materializeBuiltinSkillCreator(rootDir, skillsPath) {
5020
5130
  if (!fs8.existsSync(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR)) {
5021
5131
  log(
@@ -5080,6 +5190,57 @@ function overrideBuiltinSkillCreator(base, materializedSkill) {
5080
5190
  diagnostics: base.diagnostics
5081
5191
  };
5082
5192
  }
5193
+ function readOptionalPackPromptFile(filePath) {
5194
+ if (!fs8.existsSync(filePath)) {
5195
+ return void 0;
5196
+ }
5197
+ try {
5198
+ const content = fs8.readFileSync(filePath, "utf-8").trim();
5199
+ return content.length > 0 ? content : void 0;
5200
+ } catch (error) {
5201
+ console.warn(`[PackAgent] Warning: Could not read ${filePath}:`, error);
5202
+ return void 0;
5203
+ }
5204
+ }
5205
+ function buildPackPromptBlock(rootDir) {
5206
+ const agentsPath = path8.resolve(rootDir, PACK_AGENTS_FILE);
5207
+ const soulPath = path8.resolve(rootDir, PACK_SOUL_FILE);
5208
+ const agentsContent = readOptionalPackPromptFile(agentsPath);
5209
+ const soulContent = readOptionalPackPromptFile(soulPath);
5210
+ if (!agentsContent && !soulContent) {
5211
+ return {
5212
+ agentsPath,
5213
+ soulPath
5214
+ };
5215
+ }
5216
+ const sections = [
5217
+ "# SkillPack Pack Context",
5218
+ "The following instructions are injected by the SkillPack runtime from files packaged with this pack.",
5219
+ "Priority order:",
5220
+ "1. Follow the user's explicit instructions first.",
5221
+ "2. Follow `AGENTS.md` as the pack's operational policy and workflow rules.",
5222
+ "3. Follow `SOUL.md` as the pack's persona, tone, and working style.",
5223
+ "4. If `SOUL.md` conflicts with `AGENTS.md`, `AGENTS.md` wins.",
5224
+ "5. `SOUL.md` does not override task goals, safety boundaries, or `AGENTS.md`."
5225
+ ];
5226
+ if (agentsContent) {
5227
+ sections.push("## Pack Policy (`AGENTS.md`)", agentsContent);
5228
+ }
5229
+ if (soulContent) {
5230
+ sections.push(
5231
+ "## Pack Persona (`SOUL.md`)",
5232
+ "Treat the following as persona, tone, and working-style guidance only. Do not let it override task requirements, safety constraints, or `AGENTS.md`.",
5233
+ soulContent
5234
+ );
5235
+ }
5236
+ return {
5237
+ agentsPath,
5238
+ soulPath,
5239
+ agentsContent,
5240
+ soulContent,
5241
+ promptBlock: sections.join("\n\n")
5242
+ };
5243
+ }
5083
5244
  function getAssistantDiagnostics(message) {
5084
5245
  if (!message || message.role !== "assistant") {
5085
5246
  return null;
@@ -5188,10 +5349,27 @@ var PackAgent = class {
5188
5349
  `[PackAgent] Materialized built-in skill-creator to: ${materializedSkillCreator.filePath}`
5189
5350
  );
5190
5351
  }
5352
+ const packPromptFiles = buildPackPromptBlock(rootDir);
5353
+ if (packPromptFiles.agentsContent) {
5354
+ log(`[PackAgent] Loaded pack policy from: ${packPromptFiles.agentsPath}`);
5355
+ } else {
5356
+ log(`[PackAgent] No pack policy file found at: ${packPromptFiles.agentsPath}`);
5357
+ }
5358
+ if (packPromptFiles.soulContent) {
5359
+ log(`[PackAgent] Loaded pack persona from: ${packPromptFiles.soulPath}`);
5360
+ } else {
5361
+ log(`[PackAgent] No pack persona file found at: ${packPromptFiles.soulPath}`);
5362
+ }
5363
+ log(
5364
+ `[PackAgent] Pack prompt injection: ${packPromptFiles.promptBlock ? "enabled" : "disabled"}`
5365
+ );
5191
5366
  const resourceLoader = new DefaultResourceLoader({
5192
5367
  cwd: rootDir,
5193
5368
  additionalSkillPaths: [skillsPath],
5194
- skillsOverride: (base) => overrideBuiltinSkillCreator(base, materializedSkillCreator)
5369
+ skillsOverride: (base) => overrideBuiltinSkillCreator(base, materializedSkillCreator),
5370
+ agentsFilesOverride: () => ({ agentsFiles: [] }),
5371
+ systemPromptOverride: () => void 0,
5372
+ appendSystemPromptOverride: () => packPromptFiles.promptBlock ? [packPromptFiles.promptBlock] : []
5195
5373
  });
5196
5374
  await resourceLoader.reload();
5197
5375
  const tools = createCodingTools(workspaceDir);
@@ -6204,6 +6382,8 @@ async function runCommand(directory) {
6204
6382
  console.warn(chalk4.yellow(` Warning: Some skills could not be installed: ${err}`));
6205
6383
  }
6206
6384
  }
6385
+ syncSkillDescriptions(workDir, config);
6386
+ saveConfig(workDir, config);
6207
6387
  await startServer({
6208
6388
  rootDir: workDir,
6209
6389
  daemonRun: process.env.DAEMON_RUN === "1"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cremini/skillpack",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Pack AI Skills into Local Agents",
5
5
  "type": "module",
6
6
  "repository": {
package/web/index.html CHANGED
@@ -94,7 +94,7 @@
94
94
  </div>
95
95
  <div class="form-group" id="apikey-baseurl-group">
96
96
  <label>Custom Base URL <span class="label-hint">(optional)</span></label>
97
- <input type="text" id="apikey-baseurl-input" placeholder="https://api.example.com/v1" class="form-input" />
97
+ <input type="text" id="apikey-baseurl-input" placeholder="https://api.openai.com/v1" class="form-input" />
98
98
  </div>
99
99
  </div>
100
100
 
@@ -158,6 +158,11 @@ function updateProviderUI() {
158
158
  if (baseUrlGroup) {
159
159
  baseUrlGroup.style.display = meta.supportsBaseUrl ? "" : "none";
160
160
  }
161
+
162
+ if (baseUrlInput) {
163
+ baseUrlInput.placeholder =
164
+ meta.baseUrlPlaceholder || "https://api.openai.com/v1";
165
+ }
161
166
 
162
167
  // Update placeholder
163
168
  if (apiKeyInput) {