talking-stick 0.4.11 → 0.4.13
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 +15 -12
- package/dist/cli/install-commands.js +108 -19
- package/dist/cli/output.js +2 -2
- package/dist/cli/parser.js +1 -0
- package/dist/cli/registry.js +1 -1
- package/dist/harness-model.js +44 -0
- package/dist/identity.js +11 -8
- package/dist/index.js +3 -2
- package/dist/install-migration.js +7 -1
- package/dist/install.js +20 -1
- package/dist/instructions.js +14 -5
- package/dist/skill-install.js +200 -25
- package/dist/update-migration.js +11 -1
- package/docs/plans/2026-06-13-antigravity-agents-skill-install.md +793 -0
- package/docs/releases/0.4.12.md +17 -0
- package/docs/releases/0.4.13.md +25 -0
- package/package.json +1 -1
- package/skills/talking-stick/SKILL.md +2 -2
package/dist/skill-install.js
CHANGED
|
@@ -2,12 +2,55 @@ import fs from "node:fs";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { FILE_SKILL_HARNESSES, HARNESS_SKILL_MODELS, SUPPORTED_HARNESSES } from "./harness-model.js";
|
|
5
6
|
import { MissingHarnessError, resolveHarnessConfigDir, skipAction } from "./install.js";
|
|
6
7
|
export const DEFAULT_SKILL_NAME = "talking-stick";
|
|
7
|
-
|
|
8
|
+
export { FILE_SKILL_HARNESSES } from "./harness-model.js";
|
|
8
9
|
export function resolveBundledSkillPath(options = {}) {
|
|
9
10
|
return options.sourcePath ?? path.resolve(currentPackageDir(), "skills", DEFAULT_SKILL_NAME);
|
|
10
11
|
}
|
|
12
|
+
export function resolveSharedAgentsSkillsDir(options = {}) {
|
|
13
|
+
const homeDir = options.homeDir ?? process.env.HOME ?? "";
|
|
14
|
+
return path.join(homeDir, ".agents", "skills");
|
|
15
|
+
}
|
|
16
|
+
export function resolveSharedSkillTargetPath(options = {}) {
|
|
17
|
+
return path.join(resolveSharedAgentsSkillsDir(options), options.skillName ?? DEFAULT_SKILL_NAME);
|
|
18
|
+
}
|
|
19
|
+
export function skillLoadingModel(harness) {
|
|
20
|
+
return HARNESS_SKILL_MODELS[harness].skillLoadingModel;
|
|
21
|
+
}
|
|
22
|
+
export function resolvePrimarySkillTargetPath(harness, options = {}) {
|
|
23
|
+
const model = skillLoadingModel(harness);
|
|
24
|
+
if (model === "shared" || model === "shared+proprietary") {
|
|
25
|
+
return resolveSharedSkillTargetPath(options);
|
|
26
|
+
}
|
|
27
|
+
return resolveSkillTargetPath(harness, options);
|
|
28
|
+
}
|
|
29
|
+
export function resolveLegacyOpencodeSkillTargetPath(options = {}) {
|
|
30
|
+
const homeDir = options.homeDir ?? process.env.HOME ?? "";
|
|
31
|
+
return path.join(homeDir, ".opencode", "skills", options.skillName ?? DEFAULT_SKILL_NAME);
|
|
32
|
+
}
|
|
33
|
+
export function resolveDuplicateSkillTargetPaths(harness, options = {}) {
|
|
34
|
+
const skillName = options.skillName ?? DEFAULT_SKILL_NAME;
|
|
35
|
+
switch (harness) {
|
|
36
|
+
case "claude-code":
|
|
37
|
+
case "antigravity":
|
|
38
|
+
return [];
|
|
39
|
+
case "codex":
|
|
40
|
+
return [resolveSkillTargetPath("codex", options)];
|
|
41
|
+
case "grok":
|
|
42
|
+
return [resolveSkillTargetPath("grok", options)];
|
|
43
|
+
case "opencode":
|
|
44
|
+
return [
|
|
45
|
+
resolveSkillTargetPath("opencode", options),
|
|
46
|
+
resolveLegacyOpencodeSkillTargetPath(options)
|
|
47
|
+
];
|
|
48
|
+
case "gemini":
|
|
49
|
+
return [path.join(resolveHarnessConfigDir("gemini", options), "skills", skillName)];
|
|
50
|
+
default:
|
|
51
|
+
throw new Error(`Unknown duplicate skill cleanup harness: ${harness}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
11
54
|
export function resolveSkillTargetPath(harness, options = {}) {
|
|
12
55
|
const homeDir = options.homeDir ?? process.env.HOME ?? "";
|
|
13
56
|
switch (harness) {
|
|
@@ -15,6 +58,8 @@ export function resolveSkillTargetPath(harness, options = {}) {
|
|
|
15
58
|
return path.join(homeDir, ".claude", "skills", options.skillName ?? DEFAULT_SKILL_NAME);
|
|
16
59
|
case "codex":
|
|
17
60
|
return path.join(homeDir, ".codex", "skills", options.skillName ?? DEFAULT_SKILL_NAME);
|
|
61
|
+
case "antigravity":
|
|
62
|
+
return resolveSharedSkillTargetPath(options);
|
|
18
63
|
case "grok":
|
|
19
64
|
return path.join(resolveHarnessConfigDir("grok", options), "skills", options.skillName ?? DEFAULT_SKILL_NAME);
|
|
20
65
|
case "opencode":
|
|
@@ -29,28 +74,15 @@ export function planSkillInstall(harness, options = {}) {
|
|
|
29
74
|
const shouldLink = options.link ?? true;
|
|
30
75
|
ensureSkillSourceExists(sourcePath);
|
|
31
76
|
if (harness === "gemini") {
|
|
32
|
-
return
|
|
33
|
-
? {
|
|
34
|
-
kind: "exec",
|
|
35
|
-
harness,
|
|
36
|
-
command: "gemini",
|
|
37
|
-
args: ["skills", "link", sourcePath, "--scope", "user", "--consent"],
|
|
38
|
-
description: `gemini skills link ${sourcePath} --scope user --consent`,
|
|
39
|
-
operation: "install"
|
|
40
|
-
}
|
|
41
|
-
: {
|
|
42
|
-
kind: "exec",
|
|
43
|
-
harness,
|
|
44
|
-
command: "gemini",
|
|
45
|
-
args: ["skills", "install", sourcePath, "--scope", "user", "--consent"],
|
|
46
|
-
description: `gemini skills install ${sourcePath} --scope user --consent`,
|
|
47
|
-
operation: "install"
|
|
48
|
-
};
|
|
77
|
+
return skipAction(harness, `Gemini CLI skill install is deprecated; use tt install antigravity. Cleanup will remove ${path.join(resolveHarnessConfigDir("gemini", options), "skills", skillName)} when it is a Talking Stick-managed symlink.`);
|
|
49
78
|
}
|
|
50
|
-
const targetPath =
|
|
51
|
-
const harnessRootPath =
|
|
79
|
+
const targetPath = resolvePrimarySkillTargetPath(harness, options);
|
|
80
|
+
const harnessRootPath = skillLoadingModel(harness) === "shared" ||
|
|
81
|
+
skillLoadingModel(harness) === "shared+proprietary"
|
|
82
|
+
? undefined
|
|
83
|
+
: resolveHarnessConfigDir(harness, options);
|
|
52
84
|
const pathExists = options.pathExists ?? fs.existsSync;
|
|
53
|
-
if (options.skipMissing && !pathExists(harnessRootPath)) {
|
|
85
|
+
if (options.skipMissing && harnessRootPath && !pathExists(harnessRootPath)) {
|
|
54
86
|
return skipAction(harness, `harness config directory not found: ${harnessRootPath}`);
|
|
55
87
|
}
|
|
56
88
|
return {
|
|
@@ -67,6 +99,9 @@ export function planSkillInstall(harness, options = {}) {
|
|
|
67
99
|
}
|
|
68
100
|
export function planSkillUninstall(harness, options = {}) {
|
|
69
101
|
const skillName = options.skillName ?? DEFAULT_SKILL_NAME;
|
|
102
|
+
if (harness === "antigravity") {
|
|
103
|
+
return skipAction(harness, `shared skill left installed: ${resolveSharedSkillTargetPath(options)}`);
|
|
104
|
+
}
|
|
70
105
|
if (harness === "gemini") {
|
|
71
106
|
return {
|
|
72
107
|
kind: "exec",
|
|
@@ -90,16 +125,141 @@ export function planSkillUninstall(harness, options = {}) {
|
|
|
90
125
|
apply: () => removeInstalledSkill(targetPath, harnessRootPath, options)
|
|
91
126
|
};
|
|
92
127
|
}
|
|
128
|
+
export function planSharedSkillUninstall(options = {}) {
|
|
129
|
+
const targetPath = resolveSharedSkillTargetPath(options);
|
|
130
|
+
return {
|
|
131
|
+
kind: "file-patch",
|
|
132
|
+
harness: "antigravity",
|
|
133
|
+
filePath: targetPath,
|
|
134
|
+
description: `remove shared agents skill ${targetPath}`,
|
|
135
|
+
operation: "uninstall",
|
|
136
|
+
inspect: () => inspectInstalledPath(targetPath),
|
|
137
|
+
apply: () => removeInstalledSkill(targetPath, undefined, options)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
93
140
|
export function syncInstalledSkills(options = {}) {
|
|
94
141
|
const sourcePath = resolveBundledSkillPath(options);
|
|
95
142
|
ensureSkillSourceExists(sourcePath);
|
|
96
143
|
const sourceDigest = digestDirectory(sourcePath);
|
|
144
|
+
const syncTargets = dedupeSyncTargets(options);
|
|
97
145
|
return {
|
|
98
146
|
sourcePath,
|
|
99
147
|
sourceDigest,
|
|
100
|
-
targets:
|
|
148
|
+
targets: syncTargets.map((harness) => syncInstalledFileSkill(harness, sourcePath, sourceDigest, options))
|
|
101
149
|
};
|
|
102
150
|
}
|
|
151
|
+
export function removeDuplicateSkillInstalls(options) {
|
|
152
|
+
const sourcePath = resolveBundledSkillPath(options);
|
|
153
|
+
ensureSkillSourceExists(sourcePath);
|
|
154
|
+
const harnesses = options.harnesses === undefined || options.harnesses === "all"
|
|
155
|
+
? SUPPORTED_HARNESSES
|
|
156
|
+
: options.harnesses;
|
|
157
|
+
const results = [];
|
|
158
|
+
for (const harness of harnesses) {
|
|
159
|
+
const targets = dedupePaths(resolveDuplicateSkillTargetPaths(harness, options));
|
|
160
|
+
if (targets.length === 0) {
|
|
161
|
+
const result = {
|
|
162
|
+
harness,
|
|
163
|
+
action: "skipped",
|
|
164
|
+
message: `${harness}: no proprietary skill duplicate target`,
|
|
165
|
+
target_type: "skill"
|
|
166
|
+
};
|
|
167
|
+
results.push(result);
|
|
168
|
+
appendSkillCleanupAudit(options, result);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
for (const targetPath of targets) {
|
|
172
|
+
const result = cleanupDuplicateSkillTarget(harness, targetPath, sourcePath);
|
|
173
|
+
results.push(result);
|
|
174
|
+
appendSkillCleanupAudit(options, result, targetPath);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return results;
|
|
178
|
+
}
|
|
179
|
+
function dedupePaths(paths) {
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
const result = [];
|
|
182
|
+
for (const targetPath of paths) {
|
|
183
|
+
const key = path.resolve(targetPath);
|
|
184
|
+
if (seen.has(key))
|
|
185
|
+
continue;
|
|
186
|
+
seen.add(key);
|
|
187
|
+
result.push(targetPath);
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
function cleanupDuplicateSkillTarget(harness, targetPath, sourcePath) {
|
|
192
|
+
try {
|
|
193
|
+
const stat = fs.lstatSync(targetPath);
|
|
194
|
+
if (!stat.isSymbolicLink()) {
|
|
195
|
+
return {
|
|
196
|
+
harness,
|
|
197
|
+
action: "preserved",
|
|
198
|
+
message: `${targetPath} is not a Talking Stick-managed symlink; left in place.`,
|
|
199
|
+
target_type: "skill"
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const currentTarget = fs.readlinkSync(targetPath);
|
|
203
|
+
const resolvedCurrentTarget = path.resolve(path.dirname(targetPath), currentTarget);
|
|
204
|
+
if (!sameRealPath(resolvedCurrentTarget, sourcePath)) {
|
|
205
|
+
return {
|
|
206
|
+
harness,
|
|
207
|
+
action: "preserved",
|
|
208
|
+
message: `${targetPath} points at ${resolvedCurrentTarget}; left in place.`,
|
|
209
|
+
target_type: "skill"
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
fs.unlinkSync(targetPath);
|
|
213
|
+
return {
|
|
214
|
+
harness,
|
|
215
|
+
action: "removed",
|
|
216
|
+
message: `Removed duplicate skill symlink ${targetPath}.`,
|
|
217
|
+
target_type: "skill"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (error.code === "ENOENT") {
|
|
222
|
+
return {
|
|
223
|
+
harness,
|
|
224
|
+
action: "absent",
|
|
225
|
+
message: `${targetPath} is already absent.`,
|
|
226
|
+
target_type: "skill"
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
harness,
|
|
231
|
+
action: "failed",
|
|
232
|
+
message: error instanceof Error ? error.message : String(error),
|
|
233
|
+
target_type: "skill"
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function appendSkillCleanupAudit(options, result, targetPath) {
|
|
238
|
+
options.audit?.append({
|
|
239
|
+
reason: options.reason,
|
|
240
|
+
package_version_from: options.packageVersionFrom,
|
|
241
|
+
package_version_to: options.packageVersionTo,
|
|
242
|
+
harness: result.harness,
|
|
243
|
+
target_type: "skill",
|
|
244
|
+
config_path: targetPath,
|
|
245
|
+
action: result.action,
|
|
246
|
+
server_name: options.skillName ?? DEFAULT_SKILL_NAME,
|
|
247
|
+
detail: result.message
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function dedupeSyncTargets(options) {
|
|
251
|
+
const seen = new Set();
|
|
252
|
+
const targets = [];
|
|
253
|
+
for (const harness of FILE_SKILL_HARNESSES) {
|
|
254
|
+
const targetPath = resolvePrimarySkillTargetPath(harness, options);
|
|
255
|
+
const key = path.resolve(targetPath);
|
|
256
|
+
if (seen.has(key))
|
|
257
|
+
continue;
|
|
258
|
+
seen.add(key);
|
|
259
|
+
targets.push(harness);
|
|
260
|
+
}
|
|
261
|
+
return targets;
|
|
262
|
+
}
|
|
103
263
|
function currentPackageDir() {
|
|
104
264
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
105
265
|
}
|
|
@@ -111,7 +271,7 @@ function ensureSkillSourceExists(sourcePath) {
|
|
|
111
271
|
}
|
|
112
272
|
function installSkillDirectory(sourcePath, targetPath, harnessRootPath, link, options) {
|
|
113
273
|
const pathExists = options.pathExists ?? fs.existsSync;
|
|
114
|
-
if (options.skipMissing && !pathExists(harnessRootPath)) {
|
|
274
|
+
if (options.skipMissing && harnessRootPath && !pathExists(harnessRootPath)) {
|
|
115
275
|
throw new MissingHarnessError(`harness config directory not found: ${harnessRootPath}`);
|
|
116
276
|
}
|
|
117
277
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
@@ -123,8 +283,11 @@ function installSkillDirectory(sourcePath, targetPath, harnessRootPath, link, op
|
|
|
123
283
|
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
124
284
|
}
|
|
125
285
|
function syncInstalledFileSkill(harness, sourcePath, sourceDigest, options) {
|
|
126
|
-
const targetPath =
|
|
127
|
-
const
|
|
286
|
+
const targetPath = resolvePrimarySkillTargetPath(harness, options);
|
|
287
|
+
const model = skillLoadingModel(harness);
|
|
288
|
+
const harnessRootPath = model === "shared" || model === "shared+proprietary"
|
|
289
|
+
? path.dirname(resolveSharedAgentsSkillsDir(options))
|
|
290
|
+
: resolveHarnessConfigDir(harness, options);
|
|
128
291
|
try {
|
|
129
292
|
if (!fs.existsSync(harnessRootPath) || !fs.existsSync(targetPath)) {
|
|
130
293
|
return {
|
|
@@ -206,6 +369,18 @@ function inspectInstalledSkill(sourcePath, targetPath, link) {
|
|
|
206
369
|
throw error;
|
|
207
370
|
}
|
|
208
371
|
}
|
|
372
|
+
function inspectInstalledPath(targetPath) {
|
|
373
|
+
try {
|
|
374
|
+
fs.lstatSync(targetPath);
|
|
375
|
+
return "present";
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (error.code === "ENOENT") {
|
|
379
|
+
return "absent";
|
|
380
|
+
}
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
209
384
|
function removeInstalledSkill(targetPath, harnessRootPath, options = {}) {
|
|
210
385
|
const pathExists = options.pathExists ?? fs.existsSync;
|
|
211
386
|
if (options.skipMissing && harnessRootPath && !pathExists(harnessRootPath)) {
|
package/dist/update-migration.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { resolveDataDir } from "./config.js";
|
|
5
5
|
import { FileAuditLog, defaultAuditLogPath } from "./install-audit.js";
|
|
6
6
|
import { removeStaleMcpRegistrations } from "./install-migration.js";
|
|
7
|
+
import { removeDuplicateSkillInstalls } from "./skill-install.js";
|
|
7
8
|
export const UPDATE_MIGRATION_STATE_FILE = "update-migrations-state.json";
|
|
8
9
|
export async function runStaleMcpCleanup(options) {
|
|
9
10
|
const packageVersionTo = options.packageVersionTo ?? options.packageVersion ?? readPackageVersion();
|
|
@@ -11,7 +12,7 @@ export async function runStaleMcpCleanup(options) {
|
|
|
11
12
|
const statePath = resolveUpdateMigrationStatePath(dataDir);
|
|
12
13
|
const auditPath = defaultAuditLogPath(dataDir);
|
|
13
14
|
const audit = options.audit ?? new FileAuditLog(auditPath);
|
|
14
|
-
const
|
|
15
|
+
const mcpResults = await removeStaleMcpRegistrations({
|
|
15
16
|
harnesses: options.harnesses ?? "all",
|
|
16
17
|
reason: options.reason,
|
|
17
18
|
packageVersionFrom: options.packageVersionFrom,
|
|
@@ -19,6 +20,15 @@ export async function runStaleMcpCleanup(options) {
|
|
|
19
20
|
audit,
|
|
20
21
|
installOptions: options.installOptions
|
|
21
22
|
});
|
|
23
|
+
const skillResults = removeDuplicateSkillInstalls({
|
|
24
|
+
harnesses: options.harnesses ?? "all",
|
|
25
|
+
reason: options.reason,
|
|
26
|
+
packageVersionFrom: options.packageVersionFrom,
|
|
27
|
+
packageVersionTo,
|
|
28
|
+
audit,
|
|
29
|
+
...(options.installOptions ?? {})
|
|
30
|
+
});
|
|
31
|
+
const results = [...mcpResults, ...skillResults];
|
|
22
32
|
if (options.updateState !== false && !results.some((result) => result.action === "failed")) {
|
|
23
33
|
writeUpdateMigrationState(statePath, {
|
|
24
34
|
mcp_cleanup_version: packageVersionTo,
|