@opentil/cli 1.11.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.
- package/dist/index.js +620 -0
- package/package.json +44 -0
- package/templates/claude-md-section.md +5 -0
- package/templates/cursor-rule.md +4 -0
- package/templates/hooks.json +26 -0
- package/templates/skill/SKILL.md +635 -0
- package/templates/skill/references/api.md +465 -0
- package/templates/skill/references/auto-detection.md +145 -0
- package/templates/skill/references/local-drafts.md +142 -0
- package/templates/skill/references/management.md +779 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/install.ts
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/agents/registry.ts
|
|
8
|
+
import { existsSync as existsSync2 } from "fs";
|
|
9
|
+
import { join as join2 } from "path";
|
|
10
|
+
|
|
11
|
+
// src/utils.ts
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, rmSync } from "fs";
|
|
13
|
+
import { join, dirname } from "path";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
var home = homedir();
|
|
16
|
+
function ensureDir(dir) {
|
|
17
|
+
if (!existsSync(dir)) {
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function readJsonFile(path) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeJsonFile(path, data) {
|
|
29
|
+
ensureDir(dirname(path));
|
|
30
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
31
|
+
}
|
|
32
|
+
function readTextFile(path) {
|
|
33
|
+
try {
|
|
34
|
+
return readFileSync(path, "utf-8");
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function writeTextFile(path, content) {
|
|
40
|
+
ensureDir(dirname(path));
|
|
41
|
+
writeFileSync(path, content, "utf-8");
|
|
42
|
+
}
|
|
43
|
+
function removeFile(path) {
|
|
44
|
+
try {
|
|
45
|
+
rmSync(path, { force: true });
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function removeDir(path) {
|
|
50
|
+
try {
|
|
51
|
+
rmSync(path, { recursive: true, force: true });
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/agents/registry.ts
|
|
57
|
+
var agents = {
|
|
58
|
+
"claude-code": {
|
|
59
|
+
name: "claude-code",
|
|
60
|
+
displayName: "Claude Code",
|
|
61
|
+
detect: () => existsSync2(join2(home, ".claude")),
|
|
62
|
+
globalSkillDir: join2(home, ".claude", "skills"),
|
|
63
|
+
extras: ["hooks", "claude-md"]
|
|
64
|
+
},
|
|
65
|
+
cursor: {
|
|
66
|
+
name: "cursor",
|
|
67
|
+
displayName: "Cursor",
|
|
68
|
+
detect: () => existsSync2(join2(home, ".cursor")),
|
|
69
|
+
globalSkillDir: join2(home, ".cursor", "skills"),
|
|
70
|
+
extras: ["cursor-rules"]
|
|
71
|
+
},
|
|
72
|
+
codex: {
|
|
73
|
+
name: "codex",
|
|
74
|
+
displayName: "Codex",
|
|
75
|
+
detect: () => existsSync2(join2(home, ".codex")),
|
|
76
|
+
globalSkillDir: join2(home, ".agents", "skills"),
|
|
77
|
+
extras: []
|
|
78
|
+
},
|
|
79
|
+
opencode: {
|
|
80
|
+
name: "opencode",
|
|
81
|
+
displayName: "OpenCode",
|
|
82
|
+
detect: () => existsSync2(join2(home, ".config", "opencode")),
|
|
83
|
+
globalSkillDir: join2(home, ".agents", "skills"),
|
|
84
|
+
extras: []
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/agents/detect.ts
|
|
89
|
+
function detectAgents() {
|
|
90
|
+
return Object.entries(agents).map(([id, config]) => ({
|
|
91
|
+
id,
|
|
92
|
+
config,
|
|
93
|
+
installed: config.detect()
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/skill-content.ts
|
|
98
|
+
import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync3 } from "fs";
|
|
99
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
100
|
+
import { fileURLToPath } from "url";
|
|
101
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
102
|
+
function resolveSkillDir() {
|
|
103
|
+
const candidates = [
|
|
104
|
+
// npm package: dist/../templates/skill/ (prebuild copies skills/til/ here)
|
|
105
|
+
join3(__dirname, "..", "templates", "skill"),
|
|
106
|
+
// dev fallback: cli/src/../../skills/til/
|
|
107
|
+
join3(__dirname, "..", "..", "skills", "til")
|
|
108
|
+
];
|
|
109
|
+
for (const dir of candidates) {
|
|
110
|
+
if (existsSync3(dir)) return dir;
|
|
111
|
+
}
|
|
112
|
+
throw new Error("Could not find skill content. Please report this issue.");
|
|
113
|
+
}
|
|
114
|
+
function installSkillFiles(targetDir) {
|
|
115
|
+
const skillDir = resolveSkillDir();
|
|
116
|
+
const skillMd = readFileSync2(join3(skillDir, "SKILL.md"), "utf-8");
|
|
117
|
+
ensureDir(targetDir);
|
|
118
|
+
writeTextFile(join3(targetDir, "SKILL.md"), skillMd);
|
|
119
|
+
const refsDir = join3(skillDir, "references");
|
|
120
|
+
if (existsSync3(refsDir)) {
|
|
121
|
+
const targetRefsDir = join3(targetDir, "references");
|
|
122
|
+
ensureDir(targetRefsDir);
|
|
123
|
+
for (const file of readdirSync(refsDir)) {
|
|
124
|
+
const content = readFileSync2(join3(refsDir, file), "utf-8");
|
|
125
|
+
writeTextFile(join3(targetRefsDir, file), content);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/agents/claude-code.ts
|
|
131
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
132
|
+
import { join as join4, dirname as dirname3 } from "path";
|
|
133
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
134
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
135
|
+
function resolveTemplate(name) {
|
|
136
|
+
const candidates = [
|
|
137
|
+
join4(__dirname2, "..", "..", "templates", name),
|
|
138
|
+
// from cli/src or cli/dist
|
|
139
|
+
join4(__dirname2, "..", "templates", name)
|
|
140
|
+
// fallback
|
|
141
|
+
];
|
|
142
|
+
for (const p5 of candidates) {
|
|
143
|
+
if (existsSync4(p5)) return readFileSync3(p5, "utf-8");
|
|
144
|
+
}
|
|
145
|
+
throw new Error(`Template not found: ${name}`);
|
|
146
|
+
}
|
|
147
|
+
function installClaudeCodeExtras(extras) {
|
|
148
|
+
if (extras.includes("hooks")) {
|
|
149
|
+
installHooks();
|
|
150
|
+
}
|
|
151
|
+
if (extras.includes("claude-md")) {
|
|
152
|
+
installClaudeMdSection();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function uninstallClaudeCodeExtras() {
|
|
156
|
+
uninstallHooks();
|
|
157
|
+
uninstallClaudeMdSection();
|
|
158
|
+
}
|
|
159
|
+
function installHooks() {
|
|
160
|
+
const hooksPath = join4(home, ".claude", "hooks.json");
|
|
161
|
+
const templateContent = resolveTemplate("hooks.json");
|
|
162
|
+
const template = JSON.parse(templateContent);
|
|
163
|
+
let existing = readJsonFile(hooksPath);
|
|
164
|
+
if (!existing) {
|
|
165
|
+
existing = { hooks: {} };
|
|
166
|
+
}
|
|
167
|
+
if (!existing.hooks) {
|
|
168
|
+
existing.hooks = {};
|
|
169
|
+
}
|
|
170
|
+
for (const [event, entries] of Object.entries(template.hooks)) {
|
|
171
|
+
if (!existing.hooks[event]) {
|
|
172
|
+
existing.hooks[event] = [];
|
|
173
|
+
}
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
const alreadyExists = existing.hooks[event].some(
|
|
176
|
+
(e) => {
|
|
177
|
+
if (entry.matcher) return e.matcher === entry.matcher;
|
|
178
|
+
const entryCmd = entry.hooks?.map((h) => h.command).join("|");
|
|
179
|
+
const existCmd = e.hooks?.map((h) => h.command).join("|");
|
|
180
|
+
return entryCmd === existCmd;
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
if (!alreadyExists) {
|
|
184
|
+
existing.hooks[event].push(entry);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
writeJsonFile(hooksPath, existing);
|
|
189
|
+
}
|
|
190
|
+
function uninstallHooks() {
|
|
191
|
+
const hooksPath = join4(home, ".claude", "hooks.json");
|
|
192
|
+
const existing = readJsonFile(hooksPath);
|
|
193
|
+
if (!existing?.hooks) return;
|
|
194
|
+
const templateContent = resolveTemplate("hooks.json");
|
|
195
|
+
const template = JSON.parse(templateContent);
|
|
196
|
+
for (const [event, templateEntries] of Object.entries(template.hooks)) {
|
|
197
|
+
if (!existing.hooks[event]) continue;
|
|
198
|
+
const tEntries = templateEntries;
|
|
199
|
+
existing.hooks[event] = existing.hooks[event].filter(
|
|
200
|
+
(e) => !tEntries.some((t) => {
|
|
201
|
+
if (t.matcher) return e.matcher === t.matcher;
|
|
202
|
+
const tCmd = t.hooks?.map((h) => h.command).join("|");
|
|
203
|
+
const eCmd = e.hooks?.map((h) => h.command).join("|");
|
|
204
|
+
return tCmd === eCmd;
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
if (existing.hooks[event].length === 0) {
|
|
208
|
+
delete existing.hooks[event];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (Object.keys(existing.hooks).length === 0) {
|
|
212
|
+
removeFile(hooksPath);
|
|
213
|
+
} else {
|
|
214
|
+
writeJsonFile(hooksPath, existing);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
var OPENTIL_START = "<!-- opentil:start -->";
|
|
218
|
+
var OPENTIL_END = "<!-- opentil:end -->";
|
|
219
|
+
function installClaudeMdSection() {
|
|
220
|
+
const claudeMdPath = join4(home, ".claude", "CLAUDE.md");
|
|
221
|
+
const section = resolveTemplate("claude-md-section.md");
|
|
222
|
+
let content = readTextFile(claudeMdPath);
|
|
223
|
+
if (content === null) {
|
|
224
|
+
writeTextFile(claudeMdPath, section);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (content.includes(OPENTIL_START)) {
|
|
228
|
+
const startIdx = content.indexOf(OPENTIL_START);
|
|
229
|
+
const endIdx = content.indexOf(OPENTIL_END);
|
|
230
|
+
if (endIdx !== -1) {
|
|
231
|
+
content = content.slice(0, startIdx) + section + content.slice(endIdx + OPENTIL_END.length);
|
|
232
|
+
writeTextFile(claudeMdPath, content);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const separator = content.endsWith("\n") ? "\n" : "\n\n";
|
|
237
|
+
writeTextFile(claudeMdPath, content + separator + section);
|
|
238
|
+
}
|
|
239
|
+
function uninstallClaudeMdSection() {
|
|
240
|
+
const claudeMdPath = join4(home, ".claude", "CLAUDE.md");
|
|
241
|
+
const content = readTextFile(claudeMdPath);
|
|
242
|
+
if (!content || !content.includes(OPENTIL_START)) return;
|
|
243
|
+
const startIdx = content.indexOf(OPENTIL_START);
|
|
244
|
+
const endIdx = content.indexOf(OPENTIL_END);
|
|
245
|
+
if (endIdx === -1) return;
|
|
246
|
+
let before = content.slice(0, startIdx);
|
|
247
|
+
let after = content.slice(endIdx + OPENTIL_END.length);
|
|
248
|
+
before = before.replace(/\n\n$/, "\n");
|
|
249
|
+
after = after.replace(/^\n\n/, "\n");
|
|
250
|
+
const result = (before + after).trim();
|
|
251
|
+
if (result.length === 0) {
|
|
252
|
+
removeFile(claudeMdPath);
|
|
253
|
+
} else {
|
|
254
|
+
writeTextFile(claudeMdPath, result + "\n");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/agents/cursor.ts
|
|
259
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
260
|
+
import { join as join5, dirname as dirname4 } from "path";
|
|
261
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
262
|
+
var __dirname3 = dirname4(fileURLToPath3(import.meta.url));
|
|
263
|
+
function resolveTemplate2(name) {
|
|
264
|
+
const candidates = [
|
|
265
|
+
join5(__dirname3, "..", "..", "templates", name),
|
|
266
|
+
join5(__dirname3, "..", "templates", name)
|
|
267
|
+
];
|
|
268
|
+
for (const p5 of candidates) {
|
|
269
|
+
if (existsSync5(p5)) return readFileSync4(p5, "utf-8");
|
|
270
|
+
}
|
|
271
|
+
throw new Error(`Template not found: ${name}`);
|
|
272
|
+
}
|
|
273
|
+
function installCursorExtras(extras) {
|
|
274
|
+
if (extras.includes("cursor-rules")) {
|
|
275
|
+
installCursorRule();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function uninstallCursorExtras() {
|
|
279
|
+
uninstallCursorRule();
|
|
280
|
+
}
|
|
281
|
+
function installCursorRule() {
|
|
282
|
+
const rulesDir = join5(home, ".cursor", "rules");
|
|
283
|
+
ensureDir(rulesDir);
|
|
284
|
+
const rule = resolveTemplate2("cursor-rule.md");
|
|
285
|
+
writeTextFile(join5(rulesDir, "til.md"), rule);
|
|
286
|
+
}
|
|
287
|
+
function uninstallCursorRule() {
|
|
288
|
+
removeFile(join5(home, ".cursor", "rules", "til.md"));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/manifest.ts
|
|
292
|
+
import { join as join6 } from "path";
|
|
293
|
+
var MANIFEST_PATH = join6(home, ".opentil", "manifest.json");
|
|
294
|
+
function readManifest() {
|
|
295
|
+
return readJsonFile(MANIFEST_PATH);
|
|
296
|
+
}
|
|
297
|
+
function writeManifest(manifest) {
|
|
298
|
+
writeJsonFile(MANIFEST_PATH, manifest);
|
|
299
|
+
}
|
|
300
|
+
function removeManifest() {
|
|
301
|
+
removeFile(MANIFEST_PATH);
|
|
302
|
+
}
|
|
303
|
+
function createManifest(version) {
|
|
304
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
305
|
+
return {
|
|
306
|
+
version,
|
|
307
|
+
installedAt: now,
|
|
308
|
+
updatedAt: now,
|
|
309
|
+
agents: {}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function updateManifest(manifest, version) {
|
|
313
|
+
return {
|
|
314
|
+
...manifest,
|
|
315
|
+
version,
|
|
316
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/commands/install.ts
|
|
321
|
+
import { join as join8 } from "path";
|
|
322
|
+
|
|
323
|
+
// src/version.ts
|
|
324
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
325
|
+
import { join as join7, dirname as dirname5 } from "path";
|
|
326
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
327
|
+
var __dirname4 = dirname5(fileURLToPath4(import.meta.url));
|
|
328
|
+
function getVersion() {
|
|
329
|
+
const candidates = [
|
|
330
|
+
join7(__dirname4, "..", "..", "package.json"),
|
|
331
|
+
// from cli/src or cli/dist
|
|
332
|
+
join7(__dirname4, "..", "package.json")
|
|
333
|
+
// fallback
|
|
334
|
+
];
|
|
335
|
+
for (const p5 of candidates) {
|
|
336
|
+
try {
|
|
337
|
+
const pkg = JSON.parse(readFileSync5(p5, "utf-8"));
|
|
338
|
+
if (pkg.version) return pkg.version;
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return "0.0.0";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/commands/install.ts
|
|
346
|
+
var EXTRA_LABELS = {
|
|
347
|
+
hooks: "Hooks (auto-detection reminders)",
|
|
348
|
+
"claude-md": "CLAUDE.md (TIL auto-detection section)",
|
|
349
|
+
"cursor-rules": "Cursor Rules (TIL suggestion rule)"
|
|
350
|
+
};
|
|
351
|
+
async function install() {
|
|
352
|
+
const version = getVersion();
|
|
353
|
+
p.intro(`${pc.bgCyan(pc.black(" OpenTIL "))} v${version}`);
|
|
354
|
+
const detected = detectAgents();
|
|
355
|
+
const installedAgents = detected.filter((a) => a.installed);
|
|
356
|
+
if (installedAgents.length === 0) {
|
|
357
|
+
p.log.warn("No supported AI agents detected.");
|
|
358
|
+
p.log.info("Supported agents: " + Object.values(agents).map((a) => a.displayName).join(", "));
|
|
359
|
+
p.log.info("Install an agent first, then re-run this installer.");
|
|
360
|
+
p.outro("Done");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
p.log.info(
|
|
364
|
+
`Detected: ${installedAgents.map((a) => pc.green(a.config.displayName)).join(", ")}`
|
|
365
|
+
);
|
|
366
|
+
const existingManifest = readManifest();
|
|
367
|
+
const agentSelection = await p.multiselect({
|
|
368
|
+
message: "Which agents should have the TIL skill?",
|
|
369
|
+
options: installedAgents.map((a) => ({
|
|
370
|
+
value: a.id,
|
|
371
|
+
label: a.config.displayName,
|
|
372
|
+
hint: a.config.extras.length > 0 ? `+ ${a.config.extras.map((e) => EXTRA_LABELS[e].split(" (")[0]).join(", ")}` : void 0
|
|
373
|
+
})),
|
|
374
|
+
initialValues: existingManifest ? Object.keys(existingManifest.agents) : installedAgents.map((a) => a.id),
|
|
375
|
+
required: true
|
|
376
|
+
});
|
|
377
|
+
if (p.isCancel(agentSelection)) {
|
|
378
|
+
p.cancel("Installation cancelled.");
|
|
379
|
+
process.exit(0);
|
|
380
|
+
}
|
|
381
|
+
const selectedAgentIds = agentSelection;
|
|
382
|
+
const agentExtras = {};
|
|
383
|
+
for (const agentId of selectedAgentIds) {
|
|
384
|
+
const config = agents[agentId];
|
|
385
|
+
if (config.extras.length === 0) {
|
|
386
|
+
agentExtras[agentId] = [];
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const existingExtras = existingManifest?.agents[agentId]?.extras ?? config.extras;
|
|
390
|
+
const extras = await p.multiselect({
|
|
391
|
+
message: `${config.displayName} extras:`,
|
|
392
|
+
options: config.extras.map((e) => ({
|
|
393
|
+
value: e,
|
|
394
|
+
label: EXTRA_LABELS[e]
|
|
395
|
+
})),
|
|
396
|
+
initialValues: existingExtras,
|
|
397
|
+
required: false
|
|
398
|
+
});
|
|
399
|
+
if (p.isCancel(extras)) {
|
|
400
|
+
p.cancel("Installation cancelled.");
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
agentExtras[agentId] = extras;
|
|
404
|
+
}
|
|
405
|
+
const s = p.spinner();
|
|
406
|
+
const manifest = existingManifest ? updateManifest(existingManifest, version) : createManifest(version);
|
|
407
|
+
manifest.agents = {};
|
|
408
|
+
const agentsToRemove = existingManifest ? Object.keys(existingManifest.agents).filter((id) => !selectedAgentIds.includes(id)) : [];
|
|
409
|
+
for (const agentId of agentsToRemove) {
|
|
410
|
+
const config = agents[agentId];
|
|
411
|
+
if (!config) continue;
|
|
412
|
+
s.start(`Removing TIL from ${config.displayName}...`);
|
|
413
|
+
removeAgentSkill(agentId, config.globalSkillDir);
|
|
414
|
+
s.stop(`${config.displayName} removed`);
|
|
415
|
+
}
|
|
416
|
+
for (const agentId of selectedAgentIds) {
|
|
417
|
+
const config = agents[agentId];
|
|
418
|
+
const extras = agentExtras[agentId];
|
|
419
|
+
s.start(`Installing TIL skill for ${config.displayName}...`);
|
|
420
|
+
const skillDir = join8(config.globalSkillDir, "til");
|
|
421
|
+
installSkillFiles(skillDir);
|
|
422
|
+
if (agentId === "claude-code") {
|
|
423
|
+
installClaudeCodeExtras(extras);
|
|
424
|
+
} else if (agentId === "cursor") {
|
|
425
|
+
installCursorExtras(extras);
|
|
426
|
+
}
|
|
427
|
+
manifest.agents[agentId] = {
|
|
428
|
+
skill: true,
|
|
429
|
+
extras
|
|
430
|
+
};
|
|
431
|
+
const extrasLabel = extras.length > 0 ? ` + ${extras.map((e) => EXTRA_LABELS[e].split(" (")[0]).join(", ")}` : "";
|
|
432
|
+
s.stop(`${config.displayName}: skill${extrasLabel}`);
|
|
433
|
+
}
|
|
434
|
+
writeManifest(manifest);
|
|
435
|
+
p.note(
|
|
436
|
+
[
|
|
437
|
+
`Skill installed for: ${selectedAgentIds.map((id) => agents[id].displayName).join(", ")}`,
|
|
438
|
+
"",
|
|
439
|
+
"Next steps:",
|
|
440
|
+
" 1. Get a token at https://opentil.ai/dashboard/settings/tokens",
|
|
441
|
+
' 2. Set it: export OPENTIL_TOKEN="til_xxx"',
|
|
442
|
+
" 3. Use /til in your agent to capture insights!",
|
|
443
|
+
"",
|
|
444
|
+
`Re-run ${pc.cyan("npx @opentil/cli")} to modify your setup.`
|
|
445
|
+
].join("\n"),
|
|
446
|
+
"Setup complete"
|
|
447
|
+
);
|
|
448
|
+
p.outro("Happy TIL-ing!");
|
|
449
|
+
}
|
|
450
|
+
function removeAgentSkill(agentId, globalSkillDir) {
|
|
451
|
+
const skillDir = join8(globalSkillDir, "til");
|
|
452
|
+
removeDir(skillDir);
|
|
453
|
+
if (agentId === "claude-code") {
|
|
454
|
+
uninstallClaudeCodeExtras();
|
|
455
|
+
} else if (agentId === "cursor") {
|
|
456
|
+
uninstallCursorExtras();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/commands/update.ts
|
|
461
|
+
import * as p2 from "@clack/prompts";
|
|
462
|
+
import pc2 from "picocolors";
|
|
463
|
+
async function update() {
|
|
464
|
+
p2.intro(`${pc2.bgCyan(pc2.black(" OpenTIL "))} update`);
|
|
465
|
+
p2.log.info(`Current version: v${getVersion()}`);
|
|
466
|
+
p2.log.info("To update, re-run the installer:");
|
|
467
|
+
p2.log.info(pc2.cyan(" curl -fsSL til.so/i | bash"));
|
|
468
|
+
p2.log.info("");
|
|
469
|
+
p2.log.info("Or with npx:");
|
|
470
|
+
p2.log.info(pc2.cyan(" npx @opentil/cli@latest"));
|
|
471
|
+
p2.outro("The installer always uses the latest skill content.");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/commands/uninstall.ts
|
|
475
|
+
import * as p3 from "@clack/prompts";
|
|
476
|
+
import pc3 from "picocolors";
|
|
477
|
+
import { join as join9 } from "path";
|
|
478
|
+
async function uninstall() {
|
|
479
|
+
p3.intro(`${pc3.bgCyan(pc3.black(" OpenTIL "))} uninstall`);
|
|
480
|
+
const manifest = readManifest();
|
|
481
|
+
if (!manifest) {
|
|
482
|
+
p3.log.warn("No OpenTIL installation found.");
|
|
483
|
+
p3.outro("Nothing to do");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const agentIds = Object.keys(manifest.agents);
|
|
487
|
+
p3.log.info(
|
|
488
|
+
`Currently installed for: ${agentIds.map((id) => agents[id]?.displayName ?? id).join(", ")}`
|
|
489
|
+
);
|
|
490
|
+
const confirm2 = await p3.confirm({
|
|
491
|
+
message: "Remove TIL skill from all agents?"
|
|
492
|
+
});
|
|
493
|
+
if (p3.isCancel(confirm2) || !confirm2) {
|
|
494
|
+
p3.cancel("Uninstall cancelled.");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const s = p3.spinner();
|
|
498
|
+
for (const agentId of agentIds) {
|
|
499
|
+
const config = agents[agentId];
|
|
500
|
+
if (!config) continue;
|
|
501
|
+
s.start(`Removing TIL from ${config.displayName}...`);
|
|
502
|
+
removeDir(join9(config.globalSkillDir, "til"));
|
|
503
|
+
if (agentId === "claude-code") {
|
|
504
|
+
uninstallClaudeCodeExtras();
|
|
505
|
+
} else if (agentId === "cursor") {
|
|
506
|
+
uninstallCursorExtras();
|
|
507
|
+
}
|
|
508
|
+
s.stop(`${config.displayName}: removed`);
|
|
509
|
+
}
|
|
510
|
+
removeManifest();
|
|
511
|
+
p3.outro("OpenTIL has been uninstalled. See you next time!");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/commands/doctor.ts
|
|
515
|
+
import * as p4 from "@clack/prompts";
|
|
516
|
+
import pc4 from "picocolors";
|
|
517
|
+
import { existsSync as existsSync6 } from "fs";
|
|
518
|
+
import { join as join10 } from "path";
|
|
519
|
+
async function doctor() {
|
|
520
|
+
p4.intro(`${pc4.bgCyan(pc4.black(" OpenTIL "))} doctor`);
|
|
521
|
+
const manifest = readManifest();
|
|
522
|
+
if (!manifest) {
|
|
523
|
+
p4.log.warn("No OpenTIL installation found.");
|
|
524
|
+
p4.log.info(`Run ${pc4.cyan("npx @opentil/cli")} to install.`);
|
|
525
|
+
p4.outro("Done");
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
p4.log.info(`Installed version: v${manifest.version}`);
|
|
529
|
+
p4.log.info(`Current CLI version: v${getVersion()}`);
|
|
530
|
+
const checks = [];
|
|
531
|
+
for (const [agentId, agentManifest] of Object.entries(manifest.agents)) {
|
|
532
|
+
const config = agents[agentId];
|
|
533
|
+
if (!config) {
|
|
534
|
+
checks.push({ label: `${agentId}: agent config`, ok: false, detail: "Unknown agent" });
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
checks.push({
|
|
538
|
+
label: `${config.displayName}: detected`,
|
|
539
|
+
ok: config.detect(),
|
|
540
|
+
detail: config.detect() ? void 0 : "Agent not found on system"
|
|
541
|
+
});
|
|
542
|
+
const skillMdPath = join10(config.globalSkillDir, "til", "SKILL.md");
|
|
543
|
+
checks.push({
|
|
544
|
+
label: `${config.displayName}: SKILL.md`,
|
|
545
|
+
ok: existsSync6(skillMdPath),
|
|
546
|
+
detail: existsSync6(skillMdPath) ? skillMdPath : "Missing"
|
|
547
|
+
});
|
|
548
|
+
for (const extra of agentManifest.extras) {
|
|
549
|
+
const result = checkExtra(agentId, extra);
|
|
550
|
+
checks.push(result);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const hasToken = !!process.env.OPENTIL_TOKEN;
|
|
554
|
+
const hasCredentials = existsSync6(join10(home, ".til", "credentials"));
|
|
555
|
+
checks.push({
|
|
556
|
+
label: "Token / credentials",
|
|
557
|
+
ok: hasToken || hasCredentials,
|
|
558
|
+
detail: hasToken ? "OPENTIL_TOKEN set" : hasCredentials ? "~/.til/credentials found" : "No token found. Set OPENTIL_TOKEN or run /til auth"
|
|
559
|
+
});
|
|
560
|
+
let allOk = true;
|
|
561
|
+
for (const check of checks) {
|
|
562
|
+
const icon = check.ok ? pc4.green("\u2713") : pc4.red("\u2717");
|
|
563
|
+
const detail = check.detail ? pc4.dim(` (${check.detail})`) : "";
|
|
564
|
+
p4.log.info(` ${icon} ${check.label}${detail}`);
|
|
565
|
+
if (!check.ok) allOk = false;
|
|
566
|
+
}
|
|
567
|
+
if (allOk) {
|
|
568
|
+
p4.outro(pc4.green("All checks passed!"));
|
|
569
|
+
} else {
|
|
570
|
+
p4.outro(pc4.yellow("Some checks failed. Run npx @opentil/cli to fix."));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function checkExtra(agentId, extra) {
|
|
574
|
+
const config = agents[agentId];
|
|
575
|
+
const label = `${config.displayName}: ${extra}`;
|
|
576
|
+
switch (extra) {
|
|
577
|
+
case "hooks": {
|
|
578
|
+
const hooksPath = join10(home, ".claude", "hooks.json");
|
|
579
|
+
const hooks = readJsonFile(hooksPath);
|
|
580
|
+
const hasOpentilHook = hooks?.hooks?.PostToolUse?.some(
|
|
581
|
+
(h) => h.matcher?.includes("ExitPlanMode")
|
|
582
|
+
) || hooks?.hooks?.Stop?.some(
|
|
583
|
+
(h) => {
|
|
584
|
+
const entry = h;
|
|
585
|
+
return entry.hooks?.some((hook) => hook.command?.includes("OpenTIL"));
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
return { label, ok: !!hasOpentilHook, detail: hasOpentilHook ? void 0 : "Hook not found in hooks.json" };
|
|
589
|
+
}
|
|
590
|
+
case "claude-md": {
|
|
591
|
+
const claudeMdPath = join10(home, ".claude", "CLAUDE.md");
|
|
592
|
+
const content = readTextFile(claudeMdPath);
|
|
593
|
+
const hasSection = content?.includes("<!-- opentil:start -->");
|
|
594
|
+
return { label, ok: !!hasSection, detail: hasSection ? void 0 : "Section not found in CLAUDE.md" };
|
|
595
|
+
}
|
|
596
|
+
case "cursor-rules": {
|
|
597
|
+
const rulePath = join10(home, ".cursor", "rules", "til.md");
|
|
598
|
+
return { label, ok: existsSync6(rulePath), detail: existsSync6(rulePath) ? void 0 : "Rule file missing" };
|
|
599
|
+
}
|
|
600
|
+
default:
|
|
601
|
+
return { label, ok: false, detail: `Unknown extra: ${extra}` };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/index.ts
|
|
606
|
+
var command = process.argv[2];
|
|
607
|
+
switch (command) {
|
|
608
|
+
case "update":
|
|
609
|
+
update();
|
|
610
|
+
break;
|
|
611
|
+
case "uninstall":
|
|
612
|
+
uninstall();
|
|
613
|
+
break;
|
|
614
|
+
case "doctor":
|
|
615
|
+
doctor();
|
|
616
|
+
break;
|
|
617
|
+
default:
|
|
618
|
+
install();
|
|
619
|
+
break;
|
|
620
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opentil/cli",
|
|
3
|
+
"version": "1.11.0",
|
|
4
|
+
"description": "Universal skill installer for OpenTIL",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"opentil": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"prebuild": "node scripts/copy-skill.js",
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"dev": "tsup --watch"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@clack/prompts": "^0.10.0",
|
|
20
|
+
"picocolors": "^1.1.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.7.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/opentil/opentil-skills.git",
|
|
33
|
+
"directory": "cli"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"opentil",
|
|
37
|
+
"til",
|
|
38
|
+
"skill",
|
|
39
|
+
"installer",
|
|
40
|
+
"claude-code",
|
|
41
|
+
"cursor",
|
|
42
|
+
"ai-agent"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!-- opentil:start -->
|
|
2
|
+
## TIL Auto-Detection
|
|
3
|
+
|
|
4
|
+
When you notice a genuine insight during development (surprising behavior, non-obvious root cause, clever pattern), proactively suggest capturing it with `/til`. Only suggest once per session unless the user asks.
|
|
5
|
+
<!-- opentil:end -->
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "ExitPlanMode",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "echo '[OpenTIL] Consider capturing a TIL from this session with /til'"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"Stop": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "echo '[OpenTIL] If you learned something new, capture it with /til'",
|
|
20
|
+
"once": true
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|