@maccesar/titools 2.3.0 → 2.4.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/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;
@@ -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 { SKILLS } from '../config.js';
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,59 @@
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 HOOK_TIMEOUT = 30000;
11
+ const SETTINGS_FILE = 'settings.json';
12
+
13
+ function readSettings(claudeDir) {
14
+ const settingsPath = join(claudeDir, SETTINGS_FILE);
15
+ if (!existsSync(settingsPath)) return {};
16
+ try {
17
+ return JSON.parse(readFileSync(settingsPath, 'utf8'));
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+
23
+ function writeSettings(claudeDir, settings) {
24
+ writeFileSync(
25
+ join(claudeDir, SETTINGS_FILE),
26
+ JSON.stringify(settings, null, 2) + '\n',
27
+ 'utf8'
28
+ );
29
+ }
30
+
31
+ export function hasHook(claudeDir) {
32
+ const settings = readSettings(claudeDir);
33
+ const hooks = settings.hooks?.SessionStart;
34
+ if (!Array.isArray(hooks)) return false;
35
+ return hooks.some((h) => h.command === HOOK_COMMAND);
36
+ }
37
+
38
+ export function installHook(claudeDir) {
39
+ if (hasHook(claudeDir)) return;
40
+ const settings = readSettings(claudeDir);
41
+ if (!settings.hooks) settings.hooks = {};
42
+ if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
43
+ settings.hooks.SessionStart.push({
44
+ command: HOOK_COMMAND,
45
+ timeout: HOOK_TIMEOUT,
46
+ });
47
+ writeSettings(claudeDir, settings);
48
+ }
49
+
50
+ export function removeHook(claudeDir) {
51
+ if (!hasHook(claudeDir)) return;
52
+ const settings = readSettings(claudeDir);
53
+ settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
54
+ (h) => h.command !== HOOK_COMMAND
55
+ );
56
+ writeSettings(claudeDir, settings);
57
+ }
58
+
59
+ 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Titanium SDK skills and agents for AI coding assistants (Claude Code, Gemini CLI, Codex CLI)",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",