@iceinvein/agent-skills 0.1.17 → 0.1.18

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
@@ -87,7 +87,7 @@ Skills that control how the agent communicates — output compression, token eff
87
87
  ## Commands
88
88
 
89
89
  ```
90
- bunx @iceinvein/agent-skills install <skill> [--tool <tool>] [-g]
90
+ bunx @iceinvein/agent-skills install <skill> [--tool <tool>] [--activation <mode>] [-g]
91
91
  bunx @iceinvein/agent-skills remove <skill> [-g]
92
92
  bunx @iceinvein/agent-skills update <skill> [-g]
93
93
  bunx @iceinvein/agent-skills list
@@ -97,8 +97,18 @@ bunx @iceinvein/agent-skills info <skill>
97
97
  | Flag | |
98
98
  |------|---|
99
99
  | `--tool <tool>` | Target a specific tool: `claude`, `cursor`, `codex`, `gemini` |
100
+ | `--activation <mode>` | For skills that support it: `session` (manual `/skill`) or `global` (auto via `SessionStart` hook). Claude Code only. |
100
101
  | `-g, --global` | Install to home directory (available in all projects) |
101
102
 
103
+ ## Activation Modes (Claude Code)
104
+
105
+ Some skills — like `terse` — support activation modes. Pick one at install time:
106
+
107
+ - **session** (default) — invoke the skill manually with `/<skill>` each session
108
+ - **global** — auto-activate every Claude Code session via a `SessionStart` hook in `.claude/settings.json`
109
+
110
+ If a skill declares activation support and you're installing for Claude Code interactively, the CLI prompts you. Use `--activation session` or `--activation global` for scripted installs. `remove` strips the hook cleanly; `update` preserves your choice across version bumps.
111
+
102
112
  ## Supported Tools
103
113
 
104
114
  | Tool | Prompt Skills | MCP Skills |
package/dist/cli/index.js CHANGED
@@ -25,6 +25,7 @@ async function detectTools(cwd) {
25
25
  // src/cli/types.ts
26
26
  var TOOL_NAMES = ["claude", "cursor", "codex", "gemini"];
27
27
  var SKILL_TYPES = ["prompt", "code", "hybrid"];
28
+ var ACTIVATION_MODES = ["session", "global"];
28
29
  function validateManifest(data) {
29
30
  if (typeof data !== "object" || data === null) {
30
31
  return { ok: false, error: "Manifest must be an object" };
@@ -56,6 +57,29 @@ function validateManifest(data) {
56
57
  if (typeof d.install !== "object" || d.install === null) {
57
58
  return { ok: false, error: "Missing 'install' configuration" };
58
59
  }
60
+ if (d.activation !== undefined) {
61
+ if (typeof d.activation !== "object" || d.activation === null) {
62
+ return { ok: false, error: "'activation' must be an object" };
63
+ }
64
+ const a = d.activation;
65
+ if (!Array.isArray(a.modes) || a.modes.length === 0) {
66
+ return { ok: false, error: "'activation.modes' must be a non-empty array" };
67
+ }
68
+ for (const m of a.modes) {
69
+ if (!ACTIVATION_MODES.includes(m)) {
70
+ return { ok: false, error: `Invalid activation mode '${m}': must be one of ${ACTIVATION_MODES.join(", ")}` };
71
+ }
72
+ }
73
+ if (!ACTIVATION_MODES.includes(a.default)) {
74
+ return { ok: false, error: `Invalid 'activation.default': must be one of ${ACTIVATION_MODES.join(", ")}` };
75
+ }
76
+ if (!a.modes.includes(a.default)) {
77
+ return { ok: false, error: "'activation.default' must be one of 'activation.modes'" };
78
+ }
79
+ if (a.claudeHookDirective !== undefined && typeof a.claudeHookDirective !== "string") {
80
+ return { ok: false, error: "'activation.claudeHookDirective' must be a string" };
81
+ }
82
+ }
59
83
  return { ok: true, manifest: d };
60
84
  }
61
85
 
@@ -109,9 +133,56 @@ async function fetchAllSkillFiles(skillName, manifest) {
109
133
  // src/cli/adapters/claude.ts
110
134
  import { existsSync as existsSync2, mkdirSync, rmSync, unlinkSync } from "node:fs";
111
135
  import { dirname, join as join2 } from "node:path";
136
+ function matchesSkillDirective(command, skillName) {
137
+ return command.includes(`Activate ${skillName} skill`);
138
+ }
139
+ async function wireSessionStartHook(settingsPath, skillName, directive) {
140
+ let settings = {};
141
+ if (existsSync2(settingsPath)) {
142
+ settings = await Bun.file(settingsPath).json();
143
+ }
144
+ if (!settings.hooks)
145
+ settings.hooks = {};
146
+ if (!settings.hooks.SessionStart)
147
+ settings.hooks.SessionStart = [];
148
+ const command = `echo '${directive}'`;
149
+ for (const group of settings.hooks.SessionStart) {
150
+ for (const hook of group.hooks ?? []) {
151
+ if (matchesSkillDirective(hook.command, skillName))
152
+ return;
153
+ }
154
+ }
155
+ settings.hooks.SessionStart.push({
156
+ hooks: [{ type: "command", command }]
157
+ });
158
+ mkdirSync(dirname(settingsPath), { recursive: true });
159
+ await Bun.write(settingsPath, JSON.stringify(settings, null, 2) + `
160
+ `);
161
+ }
162
+ async function unwireSessionStartHook(settingsPath, skillName) {
163
+ if (!existsSync2(settingsPath))
164
+ return;
165
+ const settings = await Bun.file(settingsPath).json();
166
+ const sessionStart = settings.hooks?.SessionStart;
167
+ if (!sessionStart)
168
+ return;
169
+ const filteredGroups = sessionStart.map((group) => ({
170
+ hooks: (group.hooks ?? []).filter((h) => !matchesSkillDirective(h.command, skillName))
171
+ })).filter((group) => group.hooks.length > 0);
172
+ if (filteredGroups.length === 0) {
173
+ delete settings.hooks.SessionStart;
174
+ } else {
175
+ settings.hooks.SessionStart = filteredGroups;
176
+ }
177
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) {
178
+ delete settings.hooks;
179
+ }
180
+ await Bun.write(settingsPath, JSON.stringify(settings, null, 2) + `
181
+ `);
182
+ }
112
183
  var claudeAdapter = {
113
184
  name: "claude",
114
- async install(cwd, manifest, files) {
185
+ async install(cwd, manifest, files, activation) {
115
186
  const installed = [];
116
187
  const config = manifest.install.claude;
117
188
  if (!config)
@@ -147,6 +218,13 @@ var claudeAdapter = {
147
218
  `);
148
219
  installed.push(".claude/settings.json");
149
220
  }
221
+ if (activation === "global" && manifest.activation?.claudeHookDirective) {
222
+ const settingsPath = join2(cwd, ".claude/settings.json");
223
+ await wireSessionStartHook(settingsPath, manifest.name, manifest.activation.claudeHookDirective);
224
+ if (!installed.includes(".claude/settings.json")) {
225
+ installed.push(".claude/settings.json");
226
+ }
227
+ }
150
228
  return installed;
151
229
  },
152
230
  async remove(cwd, manifest, installedFiles) {
@@ -178,6 +256,10 @@ var claudeAdapter = {
178
256
  }
179
257
  }
180
258
  }
259
+ if (manifest.activation?.claudeHookDirective) {
260
+ const settingsPath = join2(cwd, ".claude/settings.json");
261
+ await unwireSessionStartHook(settingsPath, manifest.name);
262
+ }
181
263
  }
182
264
  };
183
265
 
@@ -186,7 +268,7 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync as unlin
186
268
  import { dirname as dirname2, join as join3 } from "node:path";
187
269
  var cursorAdapter = {
188
270
  name: "cursor",
189
- async install(cwd, manifest, files) {
271
+ async install(cwd, manifest, files, _activation) {
190
272
  const installed = [];
191
273
  const config = manifest.install.cursor;
192
274
  if (!config)
@@ -260,7 +342,7 @@ import { existsSync as existsSync4 } from "node:fs";
260
342
  import { join as join4 } from "node:path";
261
343
  var codexAdapter = {
262
344
  name: "codex",
263
- async install(cwd, manifest, files) {
345
+ async install(cwd, manifest, files, _activation) {
264
346
  const installed = [];
265
347
  const config = manifest.install.codex;
266
348
  if (!config)
@@ -316,7 +398,7 @@ import { existsSync as existsSync5, mkdirSync as mkdirSync3, unlinkSync as unlin
316
398
  import { dirname as dirname3, join as join5 } from "node:path";
317
399
  var geminiAdapter = {
318
400
  name: "gemini",
319
- async install(cwd, manifest, files) {
401
+ async install(cwd, manifest, files, _activation) {
320
402
  const installed = [];
321
403
  const config = manifest.install.gemini;
322
404
  if (!config)
@@ -424,7 +506,7 @@ async function removeSkillFromLockfile(cwd, skillName) {
424
506
  }
425
507
 
426
508
  // src/cli/commands/install.ts
427
- async function installSkill(cwd, manifest, files, targetTools) {
509
+ async function installSkill(cwd, manifest, files, targetTools, activation) {
428
510
  const compatibleTools = targetTools.filter((t) => manifest.tools.includes(t));
429
511
  const skipped = targetTools.filter((t) => !manifest.tools.includes(t));
430
512
  if (compatibleTools.length === 0) {
@@ -437,7 +519,7 @@ async function installSkill(cwd, manifest, files, targetTools) {
437
519
  const allFiles = [];
438
520
  for (const tool of compatibleTools) {
439
521
  const adapter = getAdapter(tool);
440
- const toolFiles = await adapter.install(cwd, manifest, files);
522
+ const toolFiles = await adapter.install(cwd, manifest, files, activation);
441
523
  installed[tool] = toolFiles;
442
524
  allFiles.push(...toolFiles);
443
525
  }
@@ -445,7 +527,8 @@ async function installSkill(cwd, manifest, files, targetTools) {
445
527
  version: manifest.version,
446
528
  tools: compatibleTools,
447
529
  installedAt: new Date().toISOString(),
448
- files: allFiles
530
+ files: allFiles,
531
+ ...activation !== undefined ? { activation } : {}
449
532
  });
450
533
  return { ok: true, installed, skipped };
451
534
  }
@@ -594,7 +677,7 @@ async function removeSkill2(cwd, skillName) {
594
677
  }
595
678
 
596
679
  // src/cli/commands/install.ts
597
- async function installSkill2(cwd, manifest, files, targetTools) {
680
+ async function installSkill2(cwd, manifest, files, targetTools, activation) {
598
681
  const compatibleTools = targetTools.filter((t) => manifest.tools.includes(t));
599
682
  const skipped = targetTools.filter((t) => !manifest.tools.includes(t));
600
683
  if (compatibleTools.length === 0) {
@@ -607,7 +690,7 @@ async function installSkill2(cwd, manifest, files, targetTools) {
607
690
  const allFiles = [];
608
691
  for (const tool of compatibleTools) {
609
692
  const adapter = getAdapter(tool);
610
- const toolFiles = await adapter.install(cwd, manifest, files);
693
+ const toolFiles = await adapter.install(cwd, manifest, files, activation);
611
694
  installed[tool] = toolFiles;
612
695
  allFiles.push(...toolFiles);
613
696
  }
@@ -615,7 +698,8 @@ async function installSkill2(cwd, manifest, files, targetTools) {
615
698
  version: manifest.version,
616
699
  tools: compatibleTools,
617
700
  installedAt: new Date().toISOString(),
618
- files: allFiles
701
+ files: allFiles,
702
+ ...activation !== undefined ? { activation } : {}
619
703
  });
620
704
  return { ok: true, installed, skipped };
621
705
  }
@@ -629,6 +713,7 @@ async function updateSkill(cwd, skillName) {
629
713
  }
630
714
  const oldVersion = entry.version;
631
715
  const tools = entry.tools;
716
+ const activation = entry.activation;
632
717
  const manifestResult = await fetchSkillManifest2(skillName);
633
718
  if (!manifestResult.ok) {
634
719
  return { ok: false, error: manifestResult.error };
@@ -638,7 +723,7 @@ async function updateSkill(cwd, skillName) {
638
723
  return { ok: false, error: filesResult.error };
639
724
  }
640
725
  await removeSkill2(cwd, skillName);
641
- const installResult = await installSkill2(cwd, manifestResult.manifest, filesResult, tools);
726
+ const installResult = await installSkill2(cwd, manifestResult.manifest, filesResult, tools, activation);
642
727
  if (!installResult.ok) {
643
728
  return { ok: false, error: installResult.error };
644
729
  }
@@ -832,6 +917,26 @@ Select (comma-separated numbers, e.g. 1,3): `);
832
917
  }
833
918
  return selected;
834
919
  }
920
+ async function promptActivation(skillName, modes) {
921
+ console.log(`
922
+ How should ${skillName} activate in Claude Code?
923
+ `);
924
+ const labels = {
925
+ session: `Per-session — invoke manually with /${skillName}`,
926
+ global: "Global — auto-activate every session (adds SessionStart hook)"
927
+ };
928
+ for (let i = 0;i < modes.length; i++) {
929
+ console.log(` ${i + 1}) ${labels[modes[i]]}`);
930
+ }
931
+ const answer = await ask(`
932
+ Select: `);
933
+ const n = parseInt(answer, 10);
934
+ if (isNaN(n) || n < 1 || n > modes.length) {
935
+ console.error("Invalid selection. Exiting.");
936
+ process.exit(1);
937
+ }
938
+ return modes[n - 1];
939
+ }
835
940
 
836
941
  // src/cli/index.ts
837
942
  import { mkdirSync as mkdirSync4 } from "fs";
@@ -873,7 +978,7 @@ function printHelp() {
873
978
  @iceinvein/agent-skills \u2014 Install agent skills into AI coding tools
874
979
 
875
980
  Usage:
876
- agent-skills install <skill> [--tool <tool>] [-g] Install a skill
981
+ agent-skills install <skill> [--tool <tool>] [--activation <session|global>] [-g] Install a skill
877
982
  agent-skills remove <skill> [-g] Remove a skill
878
983
  agent-skills update <skill> [-g] Update a skill
879
984
  agent-skills update --all [-g] Update all installed skills
@@ -886,6 +991,7 @@ Flags:
886
991
  --tool <tool> Install for a specific tool (${TOOL_NAMES2.join(", ")})
887
992
  -g, --global Install to home directory (available in all projects)
888
993
  --dry-run With bump --all: check without writing (exit 1 if unbumped)
994
+ --activation <mode> Set activation mode for skills that support it (session or global)
889
995
  `);
890
996
  }
891
997
  function printInstalled(result, skipped) {
@@ -953,7 +1059,23 @@ async function main() {
953
1059
  console.log(`Detected tools: ${tools.join(", ")}`);
954
1060
  }
955
1061
  }
956
- const result = await installSkill(installDir, manifestResult.manifest, filesResult, tools);
1062
+ let activation;
1063
+ const manifest = manifestResult.manifest;
1064
+ if (manifest.activation && tools.includes("claude")) {
1065
+ const validModes = manifest.activation.modes;
1066
+ if (flags.activation) {
1067
+ if (!validModes.includes(flags.activation)) {
1068
+ console.error(`Error: activation mode '${flags.activation}' not supported. Must be one of: ${validModes.join(", ")}`);
1069
+ process.exit(1);
1070
+ }
1071
+ activation = flags.activation;
1072
+ } else if (process.stdin.isTTY) {
1073
+ activation = await promptActivation(manifest.name, validModes);
1074
+ } else {
1075
+ activation = manifest.activation.default;
1076
+ }
1077
+ }
1078
+ const result = await installSkill(installDir, manifestResult.manifest, filesResult, tools, activation);
957
1079
  if (!result.ok) {
958
1080
  console.error(`Error: ${result.error}`);
959
1081
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iceinvein/agent-skills",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Install agent skills into AI coding tools",
5
5
  "author": "iceinvein",
6
6
  "license": "MIT",
package/skills/index.json CHANGED
@@ -165,6 +165,6 @@
165
165
  "name": "terse",
166
166
  "description": "Professional output compression — proper grammar, no fluff, ~50-60% fewer tokens. Three levels: clean, tight, sharp.",
167
167
  "type": "prompt",
168
- "version": "1.0.0"
168
+ "version": "1.1.0"
169
169
  }
170
170
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terse",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Professional output compression — proper grammar, no fluff, ~50-60% fewer tokens. Three levels: clean, tight, sharp.",
5
5
  "author": "iceinvein",
6
6
  "type": "prompt",
@@ -27,5 +27,10 @@
27
27
  "gemini": {
28
28
  "prompt": ".gemini/skills/terse.md"
29
29
  }
30
+ },
31
+ "activation": {
32
+ "modes": ["session", "global"],
33
+ "default": "session",
34
+ "claudeHookDirective": "Activate terse skill at tight level for this session."
30
35
  }
31
36
  }