@maccesar/titools 2.3.0 → 2.4.1
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/bin/titools.js +8 -0
- package/lib/cache.js +49 -0
- package/lib/commands/auto-update.js +138 -0
- package/lib/commands/skills.js +11 -0
- package/lib/commands/uninstall.js +17 -1
- package/lib/config.js +2 -0
- package/lib/hooks.js +74 -0
- package/lib/utils.js +1 -0
- package/package.json +1 -1
package/bin/titools.js
CHANGED
|
@@ -11,6 +11,7 @@ import { skillsCommand } from '../lib/commands/skills.js';
|
|
|
11
11
|
import { agentsCommand } from '../lib/commands/agents.js';
|
|
12
12
|
import { updateCommand } from '../lib/commands/update.js';
|
|
13
13
|
import { uninstallCommand } from '../lib/commands/uninstall.js';
|
|
14
|
+
import { autoUpdateCommand } from '../lib/commands/auto-update.js';
|
|
14
15
|
|
|
15
16
|
const program = new Command();
|
|
16
17
|
|
|
@@ -51,6 +52,13 @@ program
|
|
|
51
52
|
.option('-l, --local', 'Remove local skills from the current project')
|
|
52
53
|
.action(uninstallCommand);
|
|
53
54
|
|
|
55
|
+
// Auto-update command
|
|
56
|
+
program
|
|
57
|
+
.command('auto-update')
|
|
58
|
+
.description('Check for updates and apply silently (used by hooks)')
|
|
59
|
+
.option('-s, --silent', 'Suppress all output except errors')
|
|
60
|
+
.action(autoUpdateCommand);
|
|
61
|
+
|
|
54
62
|
// Parse arguments
|
|
55
63
|
program.parse();
|
|
56
64
|
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache for update checks
|
|
3
|
+
* Stores last check timestamp to avoid hitting npm registry on every run
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
const CACHE_FILE = 'last-check.json';
|
|
10
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
11
|
+
|
|
12
|
+
function getTtl() {
|
|
13
|
+
const envTtl = process.env.TITOOLS_CACHE_TTL_MS;
|
|
14
|
+
return envTtl ? parseInt(envTtl, 10) : DEFAULT_TTL_MS;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readLastCheck(cacheDir) {
|
|
18
|
+
const filePath = join(cacheDir, CACHE_FILE);
|
|
19
|
+
if (!existsSync(filePath)) return null;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
23
|
+
if (typeof data.lastCheck !== 'number' || typeof data.latestVersion !== 'string') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return data;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function writeLastCheck(cacheDir, latestVersion) {
|
|
33
|
+
if (!existsSync(cacheDir)) {
|
|
34
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
writeFileSync(
|
|
37
|
+
join(cacheDir, CACHE_FILE),
|
|
38
|
+
JSON.stringify({ lastCheck: Date.now(), latestVersion }),
|
|
39
|
+
'utf8'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function shouldCheckForUpdate(cacheDir) {
|
|
44
|
+
const data = readLastCheck(cacheDir);
|
|
45
|
+
if (!data) return true;
|
|
46
|
+
return (Date.now() - data.lastCheck) >= getTtl();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default { readLastCheck, writeLastCheck, shouldCheckForUpdate };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-update command
|
|
3
|
+
* Full pipeline: check npm -> update CLI -> sync skills -> sync MDs
|
|
4
|
+
* Designed to run silently from hooks or manually with progress
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import { execFileSync } from 'child_process';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { join, resolve } from 'path';
|
|
12
|
+
import {
|
|
13
|
+
PACKAGE_VERSION,
|
|
14
|
+
SKILLS,
|
|
15
|
+
getConfigDir,
|
|
16
|
+
} from '../config.js';
|
|
17
|
+
import {
|
|
18
|
+
shouldCheckForUpdate as shouldCheck,
|
|
19
|
+
writeLastCheck,
|
|
20
|
+
} from '../cache.js';
|
|
21
|
+
import {
|
|
22
|
+
checkForUpdate,
|
|
23
|
+
fetchLatestNpmVersion,
|
|
24
|
+
} from '../downloader.js';
|
|
25
|
+
import {
|
|
26
|
+
installSkills,
|
|
27
|
+
getLocalRepoDir,
|
|
28
|
+
} from '../installer.js';
|
|
29
|
+
import { createSkillSymlinks } from '../symlink.js';
|
|
30
|
+
import { detectPlatforms } from '../platform.js';
|
|
31
|
+
import { cleanupLegacyArtifacts, getSkillList } from '../cleanup.js';
|
|
32
|
+
import { isTitaniumProject } from '../utils.js';
|
|
33
|
+
|
|
34
|
+
function isDevMode() {
|
|
35
|
+
const repoDir = getLocalRepoDir();
|
|
36
|
+
if (!repoDir) return false;
|
|
37
|
+
return existsSync(join(repoDir, '.git'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function noopSpinner() {
|
|
41
|
+
const noop = () => noopSpinner;
|
|
42
|
+
return { start: noop, succeed: noop, fail: noop, warn: noop, info: noop };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function autoUpdateCommand(options) {
|
|
46
|
+
const silent = !!options.silent;
|
|
47
|
+
const log = silent ? () => {} : console.log;
|
|
48
|
+
const spinner = silent ? noopSpinner() : ora();
|
|
49
|
+
|
|
50
|
+
const cacheDir = getConfigDir();
|
|
51
|
+
|
|
52
|
+
// Step 1: Check cache
|
|
53
|
+
if (!shouldCheck(cacheDir)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Step 2: Check npm for latest version
|
|
58
|
+
spinner.start('Checking for updates...');
|
|
59
|
+
|
|
60
|
+
let latestVersion;
|
|
61
|
+
try {
|
|
62
|
+
latestVersion = await fetchLatestNpmVersion();
|
|
63
|
+
} catch {
|
|
64
|
+
if (!silent) spinner.fail('Could not reach npm registry');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 3: Write cache
|
|
69
|
+
try {
|
|
70
|
+
writeLastCheck(cacheDir, latestVersion);
|
|
71
|
+
} catch {
|
|
72
|
+
// Cache write failed — continue anyway
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Step 4: Compare versions
|
|
76
|
+
const hasUpdate = await checkForUpdate(PACKAGE_VERSION);
|
|
77
|
+
|
|
78
|
+
if (!hasUpdate) {
|
|
79
|
+
spinner.succeed(`Up to date (v${PACKAGE_VERSION})`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Step 5: Update CLI (skip in dev mode)
|
|
84
|
+
if (isDevMode()) {
|
|
85
|
+
spinner.info(`Dev mode — skipping npm update (latest: ${latestVersion})`);
|
|
86
|
+
} else {
|
|
87
|
+
spinner.start(`Updating titools v${PACKAGE_VERSION} → ${latestVersion}...`);
|
|
88
|
+
try {
|
|
89
|
+
execFileSync('npm', ['update', '-g', '@maccesar/titools'], {
|
|
90
|
+
stdio: 'pipe',
|
|
91
|
+
timeout: 60000,
|
|
92
|
+
});
|
|
93
|
+
spinner.succeed(`Updated to ${latestVersion}`);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
spinner.fail(`npm update failed: ${error.message}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Step 6: Sync skills and refresh symlinks
|
|
101
|
+
const repoDir = getLocalRepoDir();
|
|
102
|
+
if (repoDir) {
|
|
103
|
+
spinner.start('Syncing skills...');
|
|
104
|
+
try {
|
|
105
|
+
await installSkills(repoDir);
|
|
106
|
+
cleanupLegacyArtifacts();
|
|
107
|
+
|
|
108
|
+
const detectedPlatforms = detectPlatforms();
|
|
109
|
+
const skillList = getSkillList({ includeLegacy: false });
|
|
110
|
+
for (const platform of detectedPlatforms) {
|
|
111
|
+
const hasSymlinks = skillList.some((skill) =>
|
|
112
|
+
existsSync(join(platform.skillsDir, skill))
|
|
113
|
+
);
|
|
114
|
+
if (hasSymlinks) {
|
|
115
|
+
await createSkillSymlinks(platform.skillsDir, SKILLS);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
spinner.succeed('Skills synced');
|
|
119
|
+
} catch (error) {
|
|
120
|
+
spinner.fail(`Skill sync failed: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Step 7: If in a Titanium project, update existing MD files
|
|
125
|
+
const projectDir = resolve(process.cwd());
|
|
126
|
+
if (isTitaniumProject(projectDir)) {
|
|
127
|
+
try {
|
|
128
|
+
const { agentsCommand } = await import('./agents.js');
|
|
129
|
+
await agentsCommand(projectDir, { onlyExisting: true, force: true });
|
|
130
|
+
} catch {
|
|
131
|
+
// MD sync failed — not critical
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
log('');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default autoUpdateCommand;
|
package/lib/commands/skills.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { agentsCommand } from './agents.js';
|
|
32
32
|
import { downloadRepoArchive } from '../downloader.js';
|
|
33
33
|
import { createSkillSymlinks } from '../symlink.js';
|
|
34
|
+
import { installHook } from '../hooks.js';
|
|
34
35
|
import { formatList, isTitaniumProject } from '../utils.js';
|
|
35
36
|
import { mkdtemp } from 'fs/promises';
|
|
36
37
|
import { existsSync } from 'fs';
|
|
@@ -263,6 +264,16 @@ export async function skillsCommand(options) {
|
|
|
263
264
|
);
|
|
264
265
|
}
|
|
265
266
|
}
|
|
267
|
+
|
|
268
|
+
// Install Claude Code auto-update hook
|
|
269
|
+
if (claudeSelected) {
|
|
270
|
+
const claudePlatform = selectedPlatforms.find((p) => p.name === 'claude');
|
|
271
|
+
if (claudePlatform) {
|
|
272
|
+
const claudeConfigDir = join(claudePlatform.skillsDir, '..');
|
|
273
|
+
installHook(claudeConfigDir);
|
|
274
|
+
spinner.succeed('Claude Code: Auto-update hook installed');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
266
277
|
} else {
|
|
267
278
|
const agentsResult = removeAgents(baseDir);
|
|
268
279
|
const skillsResult = removeSkills(baseDir);
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import ora from 'ora';
|
|
8
|
-
import
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import { rm } from 'fs/promises';
|
|
10
|
+
import { SKILLS, getConfigDir } from '../config.js';
|
|
11
|
+
import { removeHook } from '../hooks.js';
|
|
9
12
|
import {
|
|
10
13
|
detectPlatforms,
|
|
11
14
|
} from '../platform.js';
|
|
@@ -231,6 +234,19 @@ export async function uninstallCommand(options) {
|
|
|
231
234
|
}
|
|
232
235
|
}
|
|
233
236
|
|
|
237
|
+
// Remove Claude Code hook and cache
|
|
238
|
+
const claudeConfigDir = join(os.homedir(), '.claude');
|
|
239
|
+
removeHook(claudeConfigDir);
|
|
240
|
+
|
|
241
|
+
const cacheDir = getConfigDir();
|
|
242
|
+
if (existsSync(cacheDir)) {
|
|
243
|
+
try {
|
|
244
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
245
|
+
} catch {
|
|
246
|
+
// Not critical
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
234
250
|
if (actionTaken) {
|
|
235
251
|
console.log('');
|
|
236
252
|
console.log(chalk.green('✓ Uninstallation complete!'));
|
package/lib/config.js
CHANGED
|
@@ -60,6 +60,7 @@ export const getClaudeAgentsDir = (baseDir = os.homedir()) => path.join(baseDir,
|
|
|
60
60
|
export const getClaudeSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.claude', 'skills');
|
|
61
61
|
export const getGeminiSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.gemini', 'skills');
|
|
62
62
|
export const getCodexSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.codex', 'skills');
|
|
63
|
+
export const getConfigDir = () => path.join(os.homedir(), '.titools');
|
|
63
64
|
|
|
64
65
|
// AI platform detection
|
|
65
66
|
export const getPlatforms = (baseDir = os.homedir()) => [
|
|
@@ -119,6 +120,7 @@ export default {
|
|
|
119
120
|
getClaudeSkillsDir,
|
|
120
121
|
getGeminiSkillsDir,
|
|
121
122
|
getCodexSkillsDir,
|
|
123
|
+
getConfigDir,
|
|
122
124
|
getPlatforms,
|
|
123
125
|
AI_FILE_PRIORITIES,
|
|
124
126
|
TITANIUM_PROJECT_FILE,
|
package/lib/hooks.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code hook management
|
|
3
|
+
* Installs/removes the SessionStart hook for auto-update
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
const HOOK_COMMAND = 'titools auto-update --silent';
|
|
10
|
+
const SETTINGS_FILE = 'settings.json';
|
|
11
|
+
|
|
12
|
+
function readSettings(claudeDir) {
|
|
13
|
+
const settingsPath = join(claudeDir, SETTINGS_FILE);
|
|
14
|
+
if (!existsSync(settingsPath)) return {};
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeSettings(claudeDir, settings) {
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join(claudeDir, SETTINGS_FILE),
|
|
25
|
+
JSON.stringify(settings, null, 2) + '\n',
|
|
26
|
+
'utf8'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Find the hook entry that contains our command
|
|
32
|
+
* Claude Code hook format: { hooks: [{ type: "command", command: "..." }] }
|
|
33
|
+
*/
|
|
34
|
+
function findHookEntry(sessionStartHooks) {
|
|
35
|
+
if (!Array.isArray(sessionStartHooks)) return -1;
|
|
36
|
+
return sessionStartHooks.findIndex((entry) =>
|
|
37
|
+
Array.isArray(entry.hooks) &&
|
|
38
|
+
entry.hooks.some((h) => h.type === 'command' && h.command === HOOK_COMMAND)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hasHook(claudeDir) {
|
|
43
|
+
const settings = readSettings(claudeDir);
|
|
44
|
+
const hooks = settings.hooks?.SessionStart;
|
|
45
|
+
return findHookEntry(hooks) !== -1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function installHook(claudeDir) {
|
|
49
|
+
if (hasHook(claudeDir)) return;
|
|
50
|
+
const settings = readSettings(claudeDir);
|
|
51
|
+
if (!settings.hooks) settings.hooks = {};
|
|
52
|
+
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
53
|
+
settings.hooks.SessionStart.push({
|
|
54
|
+
hooks: [
|
|
55
|
+
{
|
|
56
|
+
type: 'command',
|
|
57
|
+
command: HOOK_COMMAND,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
writeSettings(claudeDir, settings);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function removeHook(claudeDir) {
|
|
65
|
+
if (!hasHook(claudeDir)) return;
|
|
66
|
+
const settings = readSettings(claudeDir);
|
|
67
|
+
const idx = findHookEntry(settings.hooks.SessionStart);
|
|
68
|
+
if (idx !== -1) {
|
|
69
|
+
settings.hooks.SessionStart.splice(idx, 1);
|
|
70
|
+
}
|
|
71
|
+
writeSettings(claudeDir, settings);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default { installHook, removeHook, hasHook };
|
package/lib/utils.js
CHANGED
|
@@ -84,6 +84,7 @@ export function buildKnowledgeIndex(rootPath = '~/.agents/skills') {
|
|
|
84
84
|
// Build the compressed index format (no code blocks — direct content per Vercel's research)
|
|
85
85
|
return `[Titanium SDK Docs Index]|root: ${rootPath}\n` +
|
|
86
86
|
'|MANDATORY: Your training data for Titanium SDK, Alloy, and PurgeTSS is OUTDATED and INCOMPLETE. These reference files are the SINGLE SOURCE OF TRUTH. Prefer retrieval-led reasoning over pre-training-led reasoning. ALWAYS read the relevant files below BEFORE writing any code.\n' +
|
|
87
|
+
'|AUTO-UPDATE: At the start of a session in a Titanium project, run `titools auto-update --silent` to check for skill updates (runs at most once per day).\n' +
|
|
87
88
|
'|' + indexEntries.join('\n|');
|
|
88
89
|
}
|
|
89
90
|
|