talking-stick 0.4.12 → 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 +101 -16
- 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 +14 -1
- package/dist/instructions.js +14 -5
- package/dist/skill-install.js +200 -28
- 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.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,31 +74,15 @@ export function planSkillInstall(harness, options = {}) {
|
|
|
29
74
|
const shouldLink = options.link ?? true;
|
|
30
75
|
ensureSkillSourceExists(sourcePath);
|
|
31
76
|
if (harness === "gemini") {
|
|
32
|
-
|
|
33
|
-
return shouldLink
|
|
34
|
-
? {
|
|
35
|
-
kind: "exec",
|
|
36
|
-
harness,
|
|
37
|
-
command: "gemini",
|
|
38
|
-
args: ["skills", "link", sourcePath, "--scope", "user", "--consent"],
|
|
39
|
-
description: `gemini skills link ${sourcePath} --scope user --consent`,
|
|
40
|
-
operation: "install",
|
|
41
|
-
inspect: () => inspectInstalledSkill(sourcePath, geminiTargetPath, true)
|
|
42
|
-
}
|
|
43
|
-
: {
|
|
44
|
-
kind: "exec",
|
|
45
|
-
harness,
|
|
46
|
-
command: "gemini",
|
|
47
|
-
args: ["skills", "install", sourcePath, "--scope", "user", "--consent"],
|
|
48
|
-
description: `gemini skills install ${sourcePath} --scope user --consent`,
|
|
49
|
-
operation: "install",
|
|
50
|
-
inspect: () => inspectInstalledSkill(sourcePath, geminiTargetPath, false)
|
|
51
|
-
};
|
|
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.`);
|
|
52
78
|
}
|
|
53
|
-
const targetPath =
|
|
54
|
-
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);
|
|
55
84
|
const pathExists = options.pathExists ?? fs.existsSync;
|
|
56
|
-
if (options.skipMissing && !pathExists(harnessRootPath)) {
|
|
85
|
+
if (options.skipMissing && harnessRootPath && !pathExists(harnessRootPath)) {
|
|
57
86
|
return skipAction(harness, `harness config directory not found: ${harnessRootPath}`);
|
|
58
87
|
}
|
|
59
88
|
return {
|
|
@@ -70,6 +99,9 @@ export function planSkillInstall(harness, options = {}) {
|
|
|
70
99
|
}
|
|
71
100
|
export function planSkillUninstall(harness, options = {}) {
|
|
72
101
|
const skillName = options.skillName ?? DEFAULT_SKILL_NAME;
|
|
102
|
+
if (harness === "antigravity") {
|
|
103
|
+
return skipAction(harness, `shared skill left installed: ${resolveSharedSkillTargetPath(options)}`);
|
|
104
|
+
}
|
|
73
105
|
if (harness === "gemini") {
|
|
74
106
|
return {
|
|
75
107
|
kind: "exec",
|
|
@@ -93,16 +125,141 @@ export function planSkillUninstall(harness, options = {}) {
|
|
|
93
125
|
apply: () => removeInstalledSkill(targetPath, harnessRootPath, options)
|
|
94
126
|
};
|
|
95
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
|
+
}
|
|
96
140
|
export function syncInstalledSkills(options = {}) {
|
|
97
141
|
const sourcePath = resolveBundledSkillPath(options);
|
|
98
142
|
ensureSkillSourceExists(sourcePath);
|
|
99
143
|
const sourceDigest = digestDirectory(sourcePath);
|
|
144
|
+
const syncTargets = dedupeSyncTargets(options);
|
|
100
145
|
return {
|
|
101
146
|
sourcePath,
|
|
102
147
|
sourceDigest,
|
|
103
|
-
targets:
|
|
148
|
+
targets: syncTargets.map((harness) => syncInstalledFileSkill(harness, sourcePath, sourceDigest, options))
|
|
104
149
|
};
|
|
105
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
|
+
}
|
|
106
263
|
function currentPackageDir() {
|
|
107
264
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
108
265
|
}
|
|
@@ -114,7 +271,7 @@ function ensureSkillSourceExists(sourcePath) {
|
|
|
114
271
|
}
|
|
115
272
|
function installSkillDirectory(sourcePath, targetPath, harnessRootPath, link, options) {
|
|
116
273
|
const pathExists = options.pathExists ?? fs.existsSync;
|
|
117
|
-
if (options.skipMissing && !pathExists(harnessRootPath)) {
|
|
274
|
+
if (options.skipMissing && harnessRootPath && !pathExists(harnessRootPath)) {
|
|
118
275
|
throw new MissingHarnessError(`harness config directory not found: ${harnessRootPath}`);
|
|
119
276
|
}
|
|
120
277
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
@@ -126,8 +283,11 @@ function installSkillDirectory(sourcePath, targetPath, harnessRootPath, link, op
|
|
|
126
283
|
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
127
284
|
}
|
|
128
285
|
function syncInstalledFileSkill(harness, sourcePath, sourceDigest, options) {
|
|
129
|
-
const targetPath =
|
|
130
|
-
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);
|
|
131
291
|
try {
|
|
132
292
|
if (!fs.existsSync(harnessRootPath) || !fs.existsSync(targetPath)) {
|
|
133
293
|
return {
|
|
@@ -209,6 +369,18 @@ function inspectInstalledSkill(sourcePath, targetPath, link) {
|
|
|
209
369
|
throw error;
|
|
210
370
|
}
|
|
211
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
|
+
}
|
|
212
384
|
function removeInstalledSkill(targetPath, harnessRootPath, options = {}) {
|
|
213
385
|
const pathExists = options.pathExists ?? fs.existsSync;
|
|
214
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,
|