thought-cabinet 0.0.3 → 0.1.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 CHANGED
@@ -4,10 +4,10 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/thoughts/init.ts
7
- import fs4 from "fs";
8
- import path6 from "path";
9
- import { execSync as execSync2 } from "child_process";
10
- import chalk2 from "chalk";
7
+ import fs7 from "fs";
8
+ import path10 from "path";
9
+ import { execSync as execSync4 } from "child_process";
10
+ import chalk5 from "chalk";
11
11
  import * as p from "@clack/prompts";
12
12
 
13
13
  // src/config.ts
@@ -80,42 +80,147 @@ function saveThoughtsConfig(thoughtsConfig, options = {}) {
80
80
  }
81
81
 
82
82
  // src/commands/thoughts/utils/paths.ts
83
- import path2 from "path";
83
+ import path3 from "path";
84
84
  import os from "os";
85
+
86
+ // src/git.ts
87
+ import { execFileSync } from "child_process";
88
+ import path2 from "path";
89
+ function runGitCommand(args, opts = {}) {
90
+ return execFileSync("git", args, {
91
+ cwd: opts.cwd,
92
+ encoding: "utf8",
93
+ stdio: ["ignore", "pipe", "pipe"]
94
+ }).trim();
95
+ }
96
+ function runGitCommandOrThrow(args, opts = {}) {
97
+ execFileSync("git", args, {
98
+ cwd: opts.cwd,
99
+ stdio: "inherit"
100
+ });
101
+ }
102
+ function isGitRepo(cwd) {
103
+ try {
104
+ runGitCommand(["rev-parse", "--git-dir"], { cwd });
105
+ return true;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+ function getMainRepoPath() {
111
+ try {
112
+ const gitCommonDir = runGitCommand(["rev-parse", "--git-common-dir"]);
113
+ const gitDir = runGitCommand(["rev-parse", "--git-dir"]);
114
+ if (gitCommonDir !== gitDir && gitCommonDir !== ".git") {
115
+ const mainRepoPath = path2.dirname(path2.resolve(gitCommonDir));
116
+ return mainRepoPath;
117
+ }
118
+ return null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+ function validateWorktreeHandle(name) {
124
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
125
+ throw new Error(
126
+ `Invalid worktree name '${name}'. Use only [A-Za-z0-9._-] and start with a letter/number.`
127
+ );
128
+ }
129
+ }
130
+ function parseWorktreeListPorcelain(output) {
131
+ const blocks = output.trim().length === 0 ? [] : output.trim().split(/\n\n+/);
132
+ const out = [];
133
+ for (const block of blocks) {
134
+ let worktreePath = "";
135
+ let branch = "";
136
+ let detached = false;
137
+ for (const line of block.split("\n")) {
138
+ if (line.startsWith("worktree ")) {
139
+ worktreePath = line.slice("worktree ".length).trim();
140
+ } else if (line.startsWith("branch refs/heads/")) {
141
+ branch = line.slice("branch refs/heads/".length).trim();
142
+ } else if (line.trim() === "detached") {
143
+ detached = true;
144
+ branch = "(detached)";
145
+ }
146
+ }
147
+ if (worktreePath && branch) {
148
+ out.push({ worktreePath, branch, detached });
149
+ }
150
+ }
151
+ return out;
152
+ }
153
+ function getMainWorktreeRoot(cwd) {
154
+ const list = runGitCommand(["worktree", "list", "--porcelain"], { cwd });
155
+ const entries = parseWorktreeListPorcelain(list);
156
+ if (entries.length === 0) {
157
+ throw new Error("No git worktrees found");
158
+ }
159
+ return entries[0].worktreePath;
160
+ }
161
+ function getWorktreesBaseDir(mainWorktreeRoot) {
162
+ const repoName = path2.basename(mainWorktreeRoot);
163
+ const parent = path2.dirname(mainWorktreeRoot);
164
+ return path2.join(parent, `${repoName}__worktrees`);
165
+ }
166
+ function findWorktree(nameOrBranch, cwd) {
167
+ const list = runGitCommand(["worktree", "list", "--porcelain"], { cwd });
168
+ const entries = parseWorktreeListPorcelain(list);
169
+ for (const e of entries) {
170
+ if (path2.basename(e.worktreePath) === nameOrBranch) {
171
+ return e;
172
+ }
173
+ }
174
+ for (const e of entries) {
175
+ if (e.branch === nameOrBranch) {
176
+ return e;
177
+ }
178
+ }
179
+ throw new Error(`Worktree not found: ${nameOrBranch}`);
180
+ }
181
+ function hasUncommittedChanges(repoPath) {
182
+ const status = runGitCommand(["status", "--porcelain"], { cwd: repoPath });
183
+ return status.trim().length > 0;
184
+ }
185
+ function setBranchBase(branch, base, cwd) {
186
+ runGitCommandOrThrow(["config", "--local", `branch.${branch}.thc-base`, base], { cwd });
187
+ }
188
+
189
+ // src/commands/thoughts/utils/paths.ts
85
190
  function getDefaultThoughtsRepo() {
86
- return path2.join(os.homedir(), "thoughts");
191
+ return path3.join(os.homedir(), "thoughts");
87
192
  }
88
193
  function expandPath(filePath) {
89
194
  if (filePath.startsWith("~/")) {
90
- return path2.join(os.homedir(), filePath.slice(2));
195
+ return path3.join(os.homedir(), filePath.slice(2));
91
196
  }
92
- return path2.resolve(filePath);
197
+ return path3.resolve(filePath);
93
198
  }
94
199
  function getCurrentRepoPath() {
95
200
  return process.cwd();
96
201
  }
97
202
  function getRepoNameFromPath(repoPath) {
98
- const parts = repoPath.split(path2.sep);
203
+ const parts = repoPath.split(path3.sep);
99
204
  return parts[parts.length - 1] || "unnamed_repo";
100
205
  }
101
206
  function getRepoThoughtsPath(thoughtsRepoOrConfig, reposDirOrRepoName, repoName) {
102
207
  if (typeof thoughtsRepoOrConfig === "string") {
103
- return path2.join(expandPath(thoughtsRepoOrConfig), reposDirOrRepoName, repoName);
208
+ return path3.join(expandPath(thoughtsRepoOrConfig), reposDirOrRepoName, repoName);
104
209
  }
105
210
  const config = thoughtsRepoOrConfig;
106
- return path2.join(expandPath(config.thoughtsRepo), config.reposDir, reposDirOrRepoName);
211
+ return path3.join(expandPath(config.thoughtsRepo), config.reposDir, reposDirOrRepoName);
107
212
  }
108
213
  function getGlobalThoughtsPath(thoughtsRepoOrConfig, globalDir) {
109
214
  if (typeof thoughtsRepoOrConfig === "string") {
110
- return path2.join(expandPath(thoughtsRepoOrConfig), globalDir);
215
+ return path3.join(expandPath(thoughtsRepoOrConfig), globalDir);
111
216
  }
112
217
  const config = thoughtsRepoOrConfig;
113
- return path2.join(expandPath(config.thoughtsRepo), config.globalDir);
218
+ return path3.join(expandPath(config.thoughtsRepo), config.globalDir);
114
219
  }
115
220
 
116
221
  // src/commands/thoughts/utils/repository.ts
117
222
  import fs2 from "fs";
118
- import path4 from "path";
223
+ import path5 from "path";
119
224
  import { execSync } from "child_process";
120
225
 
121
226
  // src/templates/gitignore.ts
@@ -158,7 +263,7 @@ This directory contains thoughts and notes that apply across all repositories.
158
263
  }
159
264
 
160
265
  // src/templates/agentMd.ts
161
- import path3 from "path";
266
+ import path4 from "path";
162
267
  import os2 from "os";
163
268
  function generateAgentMd({
164
269
  thoughtsRepo,
@@ -167,8 +272,8 @@ function generateAgentMd({
167
272
  user,
168
273
  productName
169
274
  }) {
170
- const reposPath = path3.join(thoughtsRepo, reposDir, repoName).replace(os2.homedir(), "~");
171
- const globalPath = path3.join(thoughtsRepo, "global").replace(os2.homedir(), "~");
275
+ const reposPath = path4.join(thoughtsRepo, reposDir, repoName).replace(os2.homedir(), "~");
276
+ const globalPath = path4.join(thoughtsRepo, "global").replace(os2.homedir(), "~");
172
277
  return `# Thoughts Directory Structure
173
278
 
174
279
  This directory contains developer thoughts and notes for the ${repoName} repository.
@@ -288,20 +393,20 @@ function ensureThoughtsRepoExists(configOrThoughtsRepo, reposDir, globalDir) {
288
393
  if (!fs2.existsSync(expandedRepo)) {
289
394
  fs2.mkdirSync(expandedRepo, { recursive: true });
290
395
  }
291
- const expandedRepos = path4.join(expandedRepo, effectiveReposDir);
292
- const expandedGlobal = path4.join(expandedRepo, effectiveGlobalDir);
396
+ const expandedRepos = path5.join(expandedRepo, effectiveReposDir);
397
+ const expandedGlobal = path5.join(expandedRepo, effectiveGlobalDir);
293
398
  if (!fs2.existsSync(expandedRepos)) {
294
399
  fs2.mkdirSync(expandedRepos, { recursive: true });
295
400
  }
296
401
  if (!fs2.existsSync(expandedGlobal)) {
297
402
  fs2.mkdirSync(expandedGlobal, { recursive: true });
298
403
  }
299
- const gitPath = path4.join(expandedRepo, ".git");
300
- const isGitRepo = fs2.existsSync(gitPath) && (fs2.statSync(gitPath).isDirectory() || fs2.statSync(gitPath).isFile());
301
- if (!isGitRepo) {
404
+ const gitPath = path5.join(expandedRepo, ".git");
405
+ const isGitRepo2 = fs2.existsSync(gitPath) && (fs2.statSync(gitPath).isDirectory() || fs2.statSync(gitPath).isFile());
406
+ if (!isGitRepo2) {
302
407
  execSync("git init", { cwd: expandedRepo });
303
408
  const gitignore = generateGitignore();
304
- fs2.writeFileSync(path4.join(expandedRepo, ".gitignore"), gitignore);
409
+ fs2.writeFileSync(path5.join(expandedRepo, ".gitignore"), gitignore);
305
410
  execSync("git add .gitignore", { cwd: expandedRepo });
306
411
  execSync('git commit -m "Initial thoughts repository setup"', { cwd: expandedRepo });
307
412
  }
@@ -328,11 +433,11 @@ function createThoughtsDirectoryStructure(configOrThoughtsRepo, reposDirOrRepoNa
328
433
  resolvedConfig.reposDir,
329
434
  effectiveRepoName
330
435
  );
331
- const repoUserPath = path4.join(repoThoughtsPath, effectiveUser);
332
- const repoSharedPath = path4.join(repoThoughtsPath, "shared");
436
+ const repoUserPath = path5.join(repoThoughtsPath, effectiveUser);
437
+ const repoSharedPath = path5.join(repoThoughtsPath, "shared");
333
438
  const globalPath = getGlobalThoughtsPath(resolvedConfig.thoughtsRepo, resolvedConfig.globalDir);
334
- const globalUserPath = path4.join(globalPath, effectiveUser);
335
- const globalSharedPath = path4.join(globalPath, "shared");
439
+ const globalUserPath = path5.join(globalPath, effectiveUser);
440
+ const globalSharedPath = path5.join(globalPath, "shared");
336
441
  for (const dir of [repoUserPath, repoSharedPath, globalUserPath, globalSharedPath]) {
337
442
  if (!fs2.existsSync(dir)) {
338
443
  fs2.mkdirSync(dir, { recursive: true });
@@ -345,17 +450,17 @@ function createThoughtsDirectoryStructure(configOrThoughtsRepo, reposDirOrRepoNa
345
450
  const globalReadme = generateGlobalReadme({
346
451
  user: effectiveUser
347
452
  });
348
- if (!fs2.existsSync(path4.join(repoThoughtsPath, "README.md"))) {
349
- fs2.writeFileSync(path4.join(repoThoughtsPath, "README.md"), repoReadme);
453
+ if (!fs2.existsSync(path5.join(repoThoughtsPath, "README.md"))) {
454
+ fs2.writeFileSync(path5.join(repoThoughtsPath, "README.md"), repoReadme);
350
455
  }
351
- if (!fs2.existsSync(path4.join(globalPath, "README.md"))) {
352
- fs2.writeFileSync(path4.join(globalPath, "README.md"), globalReadme);
456
+ if (!fs2.existsSync(path5.join(globalPath, "README.md"))) {
457
+ fs2.writeFileSync(path5.join(globalPath, "README.md"), globalReadme);
353
458
  }
354
459
  }
355
460
 
356
461
  // src/commands/thoughts/utils/symlinks.ts
357
462
  import fs3 from "fs";
358
- import path5 from "path";
463
+ import path6 from "path";
359
464
  function updateSymlinksForNewUsers(currentRepoPath, configOrThoughtsRepo, reposDirOrRepoName, repoNameOrCurrentUser, currentUser) {
360
465
  let resolvedConfig;
361
466
  let effectiveRepoName;
@@ -372,7 +477,7 @@ function updateSymlinksForNewUsers(currentRepoPath, configOrThoughtsRepo, reposD
372
477
  effectiveRepoName = reposDirOrRepoName;
373
478
  effectiveUser = repoNameOrCurrentUser;
374
479
  }
375
- const thoughtsDir = path5.join(currentRepoPath, "thoughts");
480
+ const thoughtsDir = path6.join(currentRepoPath, "thoughts");
376
481
  const repoThoughtsPath = getRepoThoughtsPath(
377
482
  resolvedConfig.thoughtsRepo,
378
483
  resolvedConfig.reposDir,
@@ -385,8 +490,8 @@ function updateSymlinksForNewUsers(currentRepoPath, configOrThoughtsRepo, reposD
385
490
  const entries = fs3.readdirSync(repoThoughtsPath, { withFileTypes: true });
386
491
  const userDirs = entries.filter((entry) => entry.isDirectory() && entry.name !== "shared" && !entry.name.startsWith(".")).map((entry) => entry.name);
387
492
  for (const userName of userDirs) {
388
- const symlinkPath = path5.join(thoughtsDir, userName);
389
- const targetPath = path5.join(repoThoughtsPath, userName);
493
+ const symlinkPath = path6.join(thoughtsDir, userName);
494
+ const targetPath = path6.join(repoThoughtsPath, userName);
390
495
  if (!fs3.existsSync(symlinkPath) && userName !== effectiveUser) {
391
496
  try {
392
497
  fs3.symlinkSync(targetPath, symlinkPath, "dir");
@@ -398,6 +503,12 @@ function updateSymlinksForNewUsers(currentRepoPath, configOrThoughtsRepo, reposD
398
503
  return addedSymlinks;
399
504
  }
400
505
 
506
+ // src/commands/thoughts/utils/cleanup.ts
507
+ import fs4 from "fs";
508
+ import path7 from "path";
509
+ import { execSync as execSync2 } from "child_process";
510
+ import chalk2 from "chalk";
511
+
401
512
  // src/commands/thoughts/profile/utils.ts
402
513
  function resolveProfileForRepo(config, repoPath) {
403
514
  const mapping = config.repoMappings[repoPath];
@@ -451,104 +562,436 @@ function sanitizeProfileName(name) {
451
562
  return name.replace(/[^a-zA-Z0-9_-]/g, "_");
452
563
  }
453
564
 
454
- // src/commands/thoughts/init.ts
455
- function sanitizeDirectoryName(name) {
456
- return name.replace(/[^a-zA-Z0-9_-]/g, "_");
457
- }
458
- function checkExistingSetup(config) {
459
- const thoughtsDir = path6.join(process.cwd(), "thoughts");
565
+ // src/commands/thoughts/utils/cleanup.ts
566
+ function cleanupThoughtsDirectory({
567
+ repoPath,
568
+ config,
569
+ force = false,
570
+ verbose = true
571
+ }) {
572
+ const thoughtsDir = path7.join(repoPath, "thoughts");
573
+ const result = {
574
+ thoughtsRemoved: false,
575
+ configRemoved: false
576
+ };
460
577
  if (!fs4.existsSync(thoughtsDir)) {
461
- return { exists: false, isValid: false };
578
+ return result;
462
579
  }
463
- if (!fs4.lstatSync(thoughtsDir).isDirectory()) {
464
- return { exists: true, isValid: false, message: "thoughts exists but is not a directory" };
580
+ const mapping = config.repoMappings[repoPath];
581
+ const mappedName = getRepoNameFromMapping(mapping);
582
+ const profileName = getProfileNameFromMapping(mapping);
583
+ result.mappedName = mappedName;
584
+ result.profileName = profileName;
585
+ if (!mappedName && !force) {
586
+ return result;
465
587
  }
466
- if (!config) {
467
- return {
468
- exists: true,
469
- isValid: false,
470
- message: "thoughts directory exists but configuration is missing"
471
- };
588
+ const searchableDir = path7.join(thoughtsDir, "searchable");
589
+ if (fs4.existsSync(searchableDir)) {
590
+ if (verbose) {
591
+ console.log(chalk2.gray("Removing searchable directory..."));
592
+ }
593
+ try {
594
+ execSync2(`chmod -R 755 "${searchableDir}"`, { stdio: "pipe" });
595
+ } catch {
596
+ }
597
+ fs4.rmSync(searchableDir, { recursive: true, force: true });
472
598
  }
473
- const userPath = path6.join(thoughtsDir, config.user);
474
- const sharedPath = path6.join(thoughtsDir, "shared");
475
- const globalPath = path6.join(thoughtsDir, "global");
476
- const hasUser = fs4.existsSync(userPath) && fs4.lstatSync(userPath).isSymbolicLink();
477
- const hasShared = fs4.existsSync(sharedPath) && fs4.lstatSync(sharedPath).isSymbolicLink();
478
- const hasGlobal = fs4.existsSync(globalPath) && fs4.lstatSync(globalPath).isSymbolicLink();
479
- if (!hasUser || !hasShared || !hasGlobal) {
480
- return {
481
- exists: true,
482
- isValid: false,
483
- message: "thoughts directory exists but symlinks are missing or broken"
484
- };
599
+ if (verbose) {
600
+ console.log(chalk2.gray("Removing thoughts directory..."));
485
601
  }
486
- return { exists: true, isValid: true };
602
+ try {
603
+ fs4.rmSync(thoughtsDir, { recursive: true, force: true });
604
+ result.thoughtsRemoved = true;
605
+ } catch (error) {
606
+ if (verbose) {
607
+ console.error(chalk2.red(`Error removing thoughts directory: ${error}`));
608
+ }
609
+ throw error;
610
+ }
611
+ if (mappedName) {
612
+ if (verbose) {
613
+ console.log(chalk2.gray("Removing repository from thoughts configuration..."));
614
+ }
615
+ delete config.repoMappings[repoPath];
616
+ result.configRemoved = true;
617
+ }
618
+ return result;
619
+ }
620
+
621
+ // src/commands/thoughts/init-core.ts
622
+ import fs5 from "fs";
623
+ import path8 from "path";
624
+ import { execSync as execSync3 } from "child_process";
625
+ function setupThoughtsDirectory(options) {
626
+ const { repoPath, profileConfig, mappedName, user, createSearchable = false, setupHooks = false } = options;
627
+ const thoughtsDir = path8.join(repoPath, "thoughts");
628
+ if (fs5.existsSync(thoughtsDir)) {
629
+ const searchableDir = path8.join(thoughtsDir, "searchable");
630
+ if (fs5.existsSync(searchableDir)) {
631
+ try {
632
+ execSync3(`chmod -R 755 "${searchableDir}"`, { stdio: "pipe" });
633
+ } catch {
634
+ }
635
+ }
636
+ fs5.rmSync(thoughtsDir, { recursive: true, force: true });
637
+ }
638
+ fs5.mkdirSync(thoughtsDir);
639
+ const repoTarget = getRepoThoughtsPath(profileConfig, mappedName);
640
+ const globalTarget = getGlobalThoughtsPath(profileConfig);
641
+ fs5.symlinkSync(path8.join(repoTarget, user), path8.join(thoughtsDir, user), "dir");
642
+ fs5.symlinkSync(path8.join(repoTarget, "shared"), path8.join(thoughtsDir, "shared"), "dir");
643
+ fs5.symlinkSync(globalTarget, path8.join(thoughtsDir, "global"), "dir");
644
+ const otherUsers = updateSymlinksForNewUsers(
645
+ repoPath,
646
+ profileConfig,
647
+ mappedName,
648
+ user
649
+ );
650
+ const claudeMd = generateClaudeMd({
651
+ thoughtsRepo: profileConfig.thoughtsRepo,
652
+ reposDir: profileConfig.reposDir,
653
+ repoName: mappedName,
654
+ user
655
+ });
656
+ fs5.writeFileSync(path8.join(thoughtsDir, "CLAUDE.md"), claudeMd);
657
+ let hooksUpdated = [];
658
+ if (setupHooks) {
659
+ const hookResult = setupGitHooks(repoPath);
660
+ hooksUpdated = hookResult.updated;
661
+ }
662
+ if (createSearchable) {
663
+ createSearchableIndex(thoughtsDir);
664
+ }
665
+ return {
666
+ thoughtsDir,
667
+ otherUsers,
668
+ hooksUpdated
669
+ };
487
670
  }
488
671
  function setupGitHooks(repoPath) {
489
672
  const updated = [];
490
673
  let gitCommonDir;
491
674
  try {
492
- gitCommonDir = execSync2("git rev-parse --git-common-dir", {
675
+ gitCommonDir = execSync3("git rev-parse --git-common-dir", {
493
676
  cwd: repoPath,
494
677
  encoding: "utf8",
495
678
  stdio: "pipe"
496
679
  }).trim();
497
- if (!path6.isAbsolute(gitCommonDir)) {
498
- gitCommonDir = path6.join(repoPath, gitCommonDir);
680
+ if (!path8.isAbsolute(gitCommonDir)) {
681
+ gitCommonDir = path8.join(repoPath, gitCommonDir);
499
682
  }
500
683
  } catch (error) {
501
684
  throw new Error(`Failed to find git common directory: ${error}`);
502
685
  }
503
- const hooksDir = path6.join(gitCommonDir, "hooks");
504
- if (!fs4.existsSync(hooksDir)) {
505
- fs4.mkdirSync(hooksDir, { recursive: true });
686
+ const hooksDir = path8.join(gitCommonDir, "hooks");
687
+ if (!fs5.existsSync(hooksDir)) {
688
+ fs5.mkdirSync(hooksDir, { recursive: true });
506
689
  }
507
- const preCommitPath = path6.join(hooksDir, "pre-commit");
690
+ const preCommitPath = path8.join(hooksDir, "pre-commit");
508
691
  const preCommitContent = generatePreCommitHook({ hookPath: preCommitPath });
509
- const postCommitPath = path6.join(hooksDir, "post-commit");
692
+ const postCommitPath = path8.join(hooksDir, "post-commit");
510
693
  const postCommitContent = generatePostCommitHook({ hookPath: postCommitPath });
511
694
  const hookNeedsUpdate = (hookPath) => {
512
- if (!fs4.existsSync(hookPath)) return true;
513
- const content = fs4.readFileSync(hookPath, "utf8");
695
+ if (!fs5.existsSync(hookPath)) return true;
696
+ const content = fs5.readFileSync(hookPath, "utf8");
514
697
  if (!content.includes("ThoughtCabinet thoughts")) return false;
515
698
  const versionMatch = content.match(/# Version: (\d+)/);
516
699
  if (!versionMatch) return true;
517
700
  const currentVersion = parseInt(versionMatch[1]);
518
701
  return currentVersion < parseInt(HOOK_VERSION);
519
702
  };
520
- if (fs4.existsSync(preCommitPath)) {
521
- const content = fs4.readFileSync(preCommitPath, "utf8");
703
+ if (fs5.existsSync(preCommitPath)) {
704
+ const content = fs5.readFileSync(preCommitPath, "utf8");
522
705
  if (!content.includes("ThoughtCabinet thoughts") || hookNeedsUpdate(preCommitPath)) {
523
706
  if (!content.includes("ThoughtCabinet thoughts")) {
524
- fs4.renameSync(preCommitPath, `${preCommitPath}.old`);
707
+ fs5.renameSync(preCommitPath, `${preCommitPath}.old`);
525
708
  } else {
526
- fs4.unlinkSync(preCommitPath);
709
+ fs5.unlinkSync(preCommitPath);
527
710
  }
528
711
  }
529
712
  }
530
- if (fs4.existsSync(postCommitPath)) {
531
- const content = fs4.readFileSync(postCommitPath, "utf8");
713
+ if (fs5.existsSync(postCommitPath)) {
714
+ const content = fs5.readFileSync(postCommitPath, "utf8");
532
715
  if (!content.includes("ThoughtCabinet thoughts") || hookNeedsUpdate(postCommitPath)) {
533
716
  if (!content.includes("ThoughtCabinet thoughts")) {
534
- fs4.renameSync(postCommitPath, `${postCommitPath}.old`);
717
+ fs5.renameSync(postCommitPath, `${postCommitPath}.old`);
535
718
  } else {
536
- fs4.unlinkSync(postCommitPath);
719
+ fs5.unlinkSync(postCommitPath);
537
720
  }
538
721
  }
539
722
  }
540
- if (!fs4.existsSync(preCommitPath) || hookNeedsUpdate(preCommitPath)) {
541
- fs4.writeFileSync(preCommitPath, preCommitContent);
542
- fs4.chmodSync(preCommitPath, "755");
723
+ if (!fs5.existsSync(preCommitPath) || hookNeedsUpdate(preCommitPath)) {
724
+ fs5.writeFileSync(preCommitPath, preCommitContent);
725
+ fs5.chmodSync(preCommitPath, "755");
543
726
  updated.push("pre-commit");
544
727
  }
545
- if (!fs4.existsSync(postCommitPath) || hookNeedsUpdate(postCommitPath)) {
546
- fs4.writeFileSync(postCommitPath, postCommitContent);
547
- fs4.chmodSync(postCommitPath, "755");
728
+ if (!fs5.existsSync(postCommitPath) || hookNeedsUpdate(postCommitPath)) {
729
+ fs5.writeFileSync(postCommitPath, postCommitContent);
730
+ fs5.chmodSync(postCommitPath, "755");
548
731
  updated.push("post-commit");
549
732
  }
550
733
  return { updated };
551
734
  }
735
+ function createSearchableIndex(thoughtsDir) {
736
+ const searchDir = path8.join(thoughtsDir, "searchable");
737
+ if (fs5.existsSync(searchDir)) {
738
+ try {
739
+ execSync3(`chmod -R 755 "${searchDir}"`, { stdio: "pipe" });
740
+ } catch {
741
+ }
742
+ fs5.rmSync(searchDir, { recursive: true, force: true });
743
+ }
744
+ fs5.mkdirSync(searchDir, { recursive: true });
745
+ function findFilesFollowingSymlinks(dir, baseDir = dir, visited = /* @__PURE__ */ new Set()) {
746
+ const files = [];
747
+ const realPath = fs5.realpathSync(dir);
748
+ if (visited.has(realPath)) {
749
+ return files;
750
+ }
751
+ visited.add(realPath);
752
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
753
+ for (const entry of entries) {
754
+ const fullPath = path8.join(dir, entry.name);
755
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
756
+ files.push(...findFilesFollowingSymlinks(fullPath, baseDir, visited));
757
+ } else if (entry.isSymbolicLink() && !entry.name.startsWith(".")) {
758
+ try {
759
+ const stat = fs5.statSync(fullPath);
760
+ if (stat.isDirectory()) {
761
+ files.push(...findFilesFollowingSymlinks(fullPath, baseDir, visited));
762
+ } else if (stat.isFile() && path8.basename(fullPath) !== "CLAUDE.md") {
763
+ files.push(path8.relative(baseDir, fullPath));
764
+ }
765
+ } catch {
766
+ }
767
+ } else if (entry.isFile() && !entry.name.startsWith(".") && entry.name !== "CLAUDE.md") {
768
+ files.push(path8.relative(baseDir, fullPath));
769
+ }
770
+ }
771
+ return files;
772
+ }
773
+ const allFiles = findFilesFollowingSymlinks(thoughtsDir);
774
+ let linkedCount = 0;
775
+ for (const relPath of allFiles) {
776
+ const sourcePath = path8.join(thoughtsDir, relPath);
777
+ const targetPath = path8.join(searchDir, relPath);
778
+ const targetDir = path8.dirname(targetPath);
779
+ if (!fs5.existsSync(targetDir)) {
780
+ fs5.mkdirSync(targetDir, { recursive: true });
781
+ }
782
+ try {
783
+ const realSourcePath = fs5.realpathSync(sourcePath);
784
+ fs5.linkSync(realSourcePath, targetPath);
785
+ linkedCount++;
786
+ } catch {
787
+ }
788
+ }
789
+ return linkedCount;
790
+ }
791
+ function pullThoughtsFromRemote(thoughtsRepo) {
792
+ const expandedRepo = expandPath(thoughtsRepo);
793
+ try {
794
+ execSync3("git remote get-url origin", { cwd: expandedRepo, stdio: "pipe" });
795
+ try {
796
+ execSync3("git pull --rebase", {
797
+ stdio: "pipe",
798
+ cwd: expandedRepo
799
+ });
800
+ return true;
801
+ } catch {
802
+ return false;
803
+ }
804
+ } catch {
805
+ return false;
806
+ }
807
+ }
808
+
809
+ // src/hooks/loader.ts
810
+ import fs6 from "fs";
811
+ import path9 from "path";
812
+ import chalk3 from "chalk";
813
+ var HOOKS_CONFIG_DIR = ".thought-cabinet";
814
+ var HOOKS_CONFIG_FILE = `${HOOKS_CONFIG_DIR}/hooks.json`;
815
+ function loadHooksConfig(repoPath) {
816
+ const configPath = path9.join(repoPath, HOOKS_CONFIG_FILE);
817
+ if (!fs6.existsSync(configPath)) {
818
+ return null;
819
+ }
820
+ try {
821
+ const content = fs6.readFileSync(configPath, "utf8");
822
+ const config = JSON.parse(content);
823
+ if (!config.hooks || typeof config.hooks !== "object") {
824
+ console.error(
825
+ chalk3.yellow(`Warning: Invalid hooks config at ${configPath}: missing 'hooks' object`)
826
+ );
827
+ return null;
828
+ }
829
+ return config;
830
+ } catch (error) {
831
+ console.error(
832
+ chalk3.yellow(
833
+ `Warning: Could not parse hooks config at ${configPath}: ${error.message}`
834
+ )
835
+ );
836
+ return null;
837
+ }
838
+ }
839
+ function getHooksForEvent(config, event) {
840
+ const hookGroups = config?.hooks[event];
841
+ if (!hookGroups) {
842
+ return [];
843
+ }
844
+ return hookGroups.flatMap((group) => group.hooks ?? []);
845
+ }
846
+
847
+ // src/hooks/executor.ts
848
+ import { spawn } from "child_process";
849
+ import chalk4 from "chalk";
850
+ var DEFAULT_TIMEOUT_SECONDS = 60;
851
+ function resolveExitCode(exitCode, timedOut) {
852
+ if (exitCode !== null) {
853
+ return exitCode;
854
+ }
855
+ if (timedOut) {
856
+ return 124;
857
+ }
858
+ return 1;
859
+ }
860
+ async function executeHook(hook, input, env = {}) {
861
+ const startTime = Date.now();
862
+ const timeoutMs = (hook.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3;
863
+ return new Promise((resolve) => {
864
+ let stdout = "";
865
+ let stderr = "";
866
+ let timedOut = false;
867
+ const child = spawn("bash", ["-c", hook.command], {
868
+ cwd: input.cwd,
869
+ env: { ...process.env, ...env },
870
+ stdio: ["pipe", "pipe", "pipe"]
871
+ });
872
+ const timer = setTimeout(() => {
873
+ timedOut = true;
874
+ child.kill("SIGTERM");
875
+ setTimeout(() => {
876
+ if (!child.killed) {
877
+ child.kill("SIGKILL");
878
+ }
879
+ }, 5e3);
880
+ }, timeoutMs);
881
+ child.stdin.write(JSON.stringify(input, null, 2));
882
+ child.stdin.end();
883
+ child.stdout.on("data", (data) => {
884
+ stdout += data.toString();
885
+ });
886
+ child.stderr.on("data", (data) => {
887
+ stderr += data.toString();
888
+ });
889
+ child.on("close", (exitCode) => {
890
+ clearTimeout(timer);
891
+ const duration = Date.now() - startTime;
892
+ resolve({
893
+ success: exitCode === 0 && !timedOut,
894
+ exitCode: resolveExitCode(exitCode, timedOut),
895
+ stdout: stdout.trim(),
896
+ stderr: stderr.trim(),
897
+ timedOut,
898
+ duration
899
+ });
900
+ });
901
+ child.on("error", (error) => {
902
+ clearTimeout(timer);
903
+ const duration = Date.now() - startTime;
904
+ resolve({
905
+ success: false,
906
+ exitCode: 1,
907
+ stdout: stdout.trim(),
908
+ stderr: `Failed to execute hook: ${error.message}`,
909
+ timedOut: false,
910
+ duration
911
+ });
912
+ });
913
+ });
914
+ }
915
+ function displayHookResult(hook, result, verbose) {
916
+ const timeoutSeconds = hook.timeout ?? DEFAULT_TIMEOUT_SECONDS;
917
+ if (result.timedOut) {
918
+ console.log(chalk4.yellow(`Warning: Hook timed out after ${timeoutSeconds}s: ${hook.command}`));
919
+ if (result.stderr) {
920
+ console.log(chalk4.yellow(result.stderr));
921
+ }
922
+ return;
923
+ }
924
+ switch (result.exitCode) {
925
+ case 0:
926
+ console.log(chalk4.gray(`Hook completed (${result.duration}ms): ${hook.command}`));
927
+ if (verbose && result.stdout) {
928
+ console.log(chalk4.gray(result.stdout));
929
+ }
930
+ break;
931
+ case 2:
932
+ console.log(chalk4.red(`Hook failed with exit code 2: ${hook.command}`));
933
+ if (result.stderr) {
934
+ console.log(chalk4.red(result.stderr));
935
+ }
936
+ break;
937
+ default:
938
+ console.log(
939
+ chalk4.yellow(`Warning: Hook failed with exit code ${result.exitCode}: ${hook.command}`)
940
+ );
941
+ if (verbose && result.stderr) {
942
+ console.log(chalk4.yellow(result.stderr));
943
+ }
944
+ }
945
+ }
946
+ async function executeHooks(hooks, input, env = {}, verbose = false) {
947
+ if (hooks.length === 0) {
948
+ return [];
949
+ }
950
+ if (verbose) {
951
+ console.log(chalk4.gray(`
952
+ Executing ${hooks.length} hook(s) for ${input.hook_event_name}...`));
953
+ }
954
+ const results = await Promise.all(hooks.map((hook) => executeHook(hook, input, env)));
955
+ for (let i = 0; i < results.length; i++) {
956
+ displayHookResult(hooks[i], results[i], verbose);
957
+ }
958
+ return results;
959
+ }
960
+
961
+ // src/commands/thoughts/init.ts
962
+ function sanitizeDirectoryName(name) {
963
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
964
+ }
965
+ function checkExistingSetup(config) {
966
+ const thoughtsDir = path10.join(process.cwd(), "thoughts");
967
+ if (!fs7.existsSync(thoughtsDir)) {
968
+ return { exists: false, isValid: false };
969
+ }
970
+ if (!fs7.lstatSync(thoughtsDir).isDirectory()) {
971
+ return { exists: true, isValid: false, message: "thoughts exists but is not a directory" };
972
+ }
973
+ if (!config) {
974
+ return {
975
+ exists: true,
976
+ isValid: false,
977
+ message: "thoughts directory exists but configuration is missing"
978
+ };
979
+ }
980
+ const userPath = path10.join(thoughtsDir, config.user);
981
+ const sharedPath = path10.join(thoughtsDir, "shared");
982
+ const globalPath = path10.join(thoughtsDir, "global");
983
+ const hasUser = fs7.existsSync(userPath) && fs7.lstatSync(userPath).isSymbolicLink();
984
+ const hasShared = fs7.existsSync(sharedPath) && fs7.lstatSync(sharedPath).isSymbolicLink();
985
+ const hasGlobal = fs7.existsSync(globalPath) && fs7.lstatSync(globalPath).isSymbolicLink();
986
+ if (!hasUser || !hasShared || !hasGlobal) {
987
+ return {
988
+ exists: true,
989
+ isValid: false,
990
+ message: "thoughts directory exists but symlinks are missing or broken"
991
+ };
992
+ }
993
+ return { exists: true, isValid: true };
994
+ }
552
995
  async function thoughtsInitCommand(options) {
553
996
  try {
554
997
  if (!options.directory && !process.stdin.isTTY) {
@@ -558,18 +1001,18 @@ async function thoughtsInitCommand(options) {
558
1001
  }
559
1002
  const currentRepo = getCurrentRepoPath();
560
1003
  try {
561
- execSync2("git rev-parse --git-dir", { stdio: "pipe" });
1004
+ execSync4("git rev-parse --git-dir", { stdio: "pipe" });
562
1005
  } catch {
563
1006
  p.log.error("Not in a git repository");
564
1007
  process.exit(1);
565
1008
  }
566
1009
  let config = loadThoughtsConfig(options);
567
1010
  if (!config) {
568
- p.intro(chalk2.blue("Initial Thoughts Setup"));
1011
+ p.intro(chalk5.blue("Initial Thoughts Setup"));
569
1012
  p.log.info("First, let's configure your global thoughts system.");
570
1013
  const defaultRepo = getDefaultThoughtsRepo();
571
1014
  p.log.message(
572
- chalk2.gray("This is where all your thoughts across all projects will be stored.")
1015
+ chalk5.gray("This is where all your thoughts across all projects will be stored.")
573
1016
  );
574
1017
  const thoughtsRepoInput = await p.text({
575
1018
  message: "Thoughts repository location:",
@@ -581,9 +1024,9 @@ async function thoughtsInitCommand(options) {
581
1024
  process.exit(0);
582
1025
  }
583
1026
  const thoughtsRepo = thoughtsRepoInput || defaultRepo;
584
- p.log.message(chalk2.gray("Your thoughts will be organized into two main directories:"));
585
- p.log.message(chalk2.gray("- Repository-specific thoughts (one subdirectory per project)"));
586
- p.log.message(chalk2.gray("- Global thoughts (shared across all projects)"));
1027
+ p.log.message(chalk5.gray("Your thoughts will be organized into two main directories:"));
1028
+ p.log.message(chalk5.gray("- Repository-specific thoughts (one subdirectory per project)"));
1029
+ p.log.message(chalk5.gray("- Global thoughts (shared across all projects)"));
587
1030
  const reposDirInput = await p.text({
588
1031
  message: "Directory name for repository-specific thoughts:",
589
1032
  initialValue: "repos",
@@ -632,9 +1075,9 @@ async function thoughtsInitCommand(options) {
632
1075
  repoMappings: {}
633
1076
  };
634
1077
  p.note(
635
- `${chalk2.cyan(thoughtsRepo)}/
636
- \u251C\u2500\u2500 ${chalk2.cyan(reposDir2)}/ ${chalk2.gray("(project-specific thoughts)")}
637
- \u2514\u2500\u2500 ${chalk2.cyan(globalDir)}/ ${chalk2.gray("(cross-project thoughts)")}`,
1078
+ `${chalk5.cyan(thoughtsRepo)}/
1079
+ \u251C\u2500\u2500 ${chalk5.cyan(reposDir2)}/ ${chalk5.gray("(project-specific thoughts)")}
1080
+ \u2514\u2500\u2500 ${chalk5.cyan(globalDir)}/ ${chalk5.gray("(cross-project thoughts)")}`,
638
1081
  "Creating thoughts structure"
639
1082
  );
640
1083
  ensureThoughtsRepoExists(thoughtsRepo, reposDir2, globalDir);
@@ -644,16 +1087,16 @@ async function thoughtsInitCommand(options) {
644
1087
  if (options.profile) {
645
1088
  if (!validateProfile(config, options.profile)) {
646
1089
  p.log.error(`Profile "${options.profile}" does not exist.`);
647
- p.log.message(chalk2.gray("Available profiles:"));
1090
+ p.log.message(chalk5.gray("Available profiles:"));
648
1091
  if (config.profiles) {
649
1092
  Object.keys(config.profiles).forEach((name) => {
650
- p.log.message(chalk2.gray(` - ${name}`));
1093
+ p.log.message(chalk5.gray(` - ${name}`));
651
1094
  });
652
1095
  } else {
653
- p.log.message(chalk2.gray(" (none)"));
1096
+ p.log.message(chalk5.gray(" (none)"));
654
1097
  }
655
1098
  p.log.warn("Create a profile first:");
656
- p.log.message(chalk2.gray(` thoughtcabinet profile create ${options.profile}`));
1099
+ p.log.message(chalk5.gray(` thoughtcabinet profile create ${options.profile}`));
657
1100
  process.exit(1);
658
1101
  }
659
1102
  }
@@ -692,8 +1135,8 @@ async function thoughtsInitCommand(options) {
692
1135
  }
693
1136
  }
694
1137
  }
695
- const expandedRepo = expandPath(tempProfileConfig.thoughtsRepo);
696
- if (!fs4.existsSync(expandedRepo)) {
1138
+ let expandedRepo = expandPath(tempProfileConfig.thoughtsRepo);
1139
+ if (!fs7.existsSync(expandedRepo)) {
697
1140
  p.log.error(`Thoughts repository not found at ${tempProfileConfig.thoughtsRepo}`);
698
1141
  p.log.warn("The thoughts repository may have been moved or deleted.");
699
1142
  const recreate = await p.confirm({
@@ -710,16 +1153,24 @@ async function thoughtsInitCommand(options) {
710
1153
  tempProfileConfig.globalDir
711
1154
  );
712
1155
  }
713
- const reposDir = path6.join(expandedRepo, tempProfileConfig.reposDir);
714
- if (!fs4.existsSync(reposDir)) {
715
- fs4.mkdirSync(reposDir, { recursive: true });
1156
+ const reposDir = path10.join(expandedRepo, tempProfileConfig.reposDir);
1157
+ if (!fs7.existsSync(reposDir)) {
1158
+ fs7.mkdirSync(reposDir, { recursive: true });
716
1159
  }
717
- const existingRepos = fs4.readdirSync(reposDir).filter((name) => {
718
- const fullPath = path6.join(reposDir, name);
719
- return fs4.statSync(fullPath).isDirectory() && !name.startsWith(".");
1160
+ const existingRepos = fs7.readdirSync(reposDir).filter((name) => {
1161
+ const fullPath = path10.join(reposDir, name);
1162
+ return fs7.statSync(fullPath).isDirectory() && !name.startsWith(".");
720
1163
  });
721
1164
  const existingMapping = config.repoMappings[currentRepo];
722
1165
  let mappedName = getRepoNameFromMapping(existingMapping);
1166
+ let mainRepoMapping;
1167
+ let mainRepoPath = null;
1168
+ if (!mappedName) {
1169
+ mainRepoPath = getMainRepoPath();
1170
+ if (mainRepoPath && config.repoMappings[mainRepoPath]) {
1171
+ mainRepoMapping = config.repoMappings[mainRepoPath];
1172
+ }
1173
+ }
723
1174
  if (!mappedName) {
724
1175
  if (options.directory) {
725
1176
  const sanitizedDir = sanitizeDirectoryName(options.directory);
@@ -728,7 +1179,7 @@ async function thoughtsInitCommand(options) {
728
1179
  p.log.error("In non-interactive mode (--directory), you must specify a directory");
729
1180
  p.log.error("name that already exists in the thoughts repository.");
730
1181
  p.log.warn("Available directories:");
731
- existingRepos.forEach((repo) => p.log.message(chalk2.gray(` - ${repo}`)));
1182
+ existingRepos.forEach((repo) => p.log.message(chalk5.gray(` - ${repo}`)));
732
1183
  process.exit(1);
733
1184
  }
734
1185
  mappedName = sanitizedDir;
@@ -736,22 +1187,36 @@ async function thoughtsInitCommand(options) {
736
1187
  `Using existing: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/${mappedName}`
737
1188
  );
738
1189
  } else {
739
- p.intro(chalk2.blue("Repository Setup"));
740
- p.log.info(`Setting up thoughts for: ${chalk2.cyan(currentRepo)}`);
1190
+ p.intro(chalk5.blue("Repository Setup"));
1191
+ p.log.info(`Setting up thoughts for: ${chalk5.cyan(currentRepo)}`);
741
1192
  p.log.message(
742
- chalk2.gray(
1193
+ chalk5.gray(
743
1194
  `This will create a subdirectory in ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/`
744
1195
  )
745
1196
  );
746
- p.log.message(chalk2.gray("to store thoughts specific to this repository."));
747
- if (existingRepos.length > 0) {
748
- const selectOptions = [
749
- ...existingRepos.map((repo) => ({ value: repo, label: `Use existing: ${repo}` })),
750
- { value: "__create_new__", label: "Create new directory" }
751
- ];
1197
+ p.log.message(chalk5.gray("to store thoughts specific to this repository."));
1198
+ if (existingRepos.length > 0 || mainRepoMapping) {
1199
+ const selectOptions = [];
1200
+ let initialValue;
1201
+ const mainRepoMappedName = getRepoNameFromMapping(mainRepoMapping);
1202
+ if (mainRepoMappedName && existingRepos.includes(mainRepoMappedName)) {
1203
+ selectOptions.push({
1204
+ value: mainRepoMappedName,
1205
+ label: `Use existing: ${mainRepoMappedName} (from main repository)`
1206
+ });
1207
+ initialValue = mainRepoMappedName;
1208
+ }
1209
+ existingRepos.filter((repo) => repo !== mainRepoMappedName).forEach((repo) => {
1210
+ selectOptions.push({ value: repo, label: `Use existing: ${repo}` });
1211
+ });
1212
+ selectOptions.push({ value: "__create_new__", label: "Create new directory" });
1213
+ if (!initialValue) {
1214
+ initialValue = "__create_new__";
1215
+ }
752
1216
  const selection = await p.select({
753
1217
  message: "Select or create a thoughts directory for this repository:",
754
- options: selectOptions
1218
+ options: selectOptions,
1219
+ initialValue
755
1220
  });
756
1221
  if (p.isCancel(selection)) {
757
1222
  p.cancel("Operation cancelled.");
@@ -760,7 +1225,7 @@ async function thoughtsInitCommand(options) {
760
1225
  if (selection === "__create_new__") {
761
1226
  const defaultName = getRepoNameFromPath(currentRepo);
762
1227
  p.log.message(
763
- chalk2.gray(
1228
+ chalk5.gray(
764
1229
  `This name will be used for the directory: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/[name]`
765
1230
  )
766
1231
  );
@@ -780,6 +1245,13 @@ async function thoughtsInitCommand(options) {
780
1245
  );
781
1246
  } else {
782
1247
  mappedName = selection;
1248
+ if (mainRepoMapping && mappedName === mainRepoMappedName) {
1249
+ const inheritedProfile2 = getProfileNameFromMapping(mainRepoMapping);
1250
+ if (inheritedProfile2 && !options.profile) {
1251
+ options.profile = inheritedProfile2;
1252
+ p.log.info(`Inheriting profile "${inheritedProfile2}" from main repository`);
1253
+ }
1254
+ }
783
1255
  p.log.success(
784
1256
  `Will use existing: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/${mappedName}`
785
1257
  );
@@ -787,7 +1259,7 @@ async function thoughtsInitCommand(options) {
787
1259
  } else {
788
1260
  const defaultName = getRepoNameFromPath(currentRepo);
789
1261
  p.log.message(
790
- chalk2.gray(
1262
+ chalk5.gray(
791
1263
  `This name will be used for the directory: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/[name]`
792
1264
  )
793
1265
  );
@@ -816,29 +1288,36 @@ async function thoughtsInitCommand(options) {
816
1288
  config.repoMappings[currentRepo] = mappedName;
817
1289
  }
818
1290
  saveThoughtsConfig(config, options);
1291
+ const inheritedProfile = getProfileNameFromMapping(mainRepoMapping);
1292
+ if (inheritedProfile && options.profile === inheritedProfile && tempProfileConfig.profileName !== inheritedProfile) {
1293
+ const profileSettings = config.profiles?.[inheritedProfile];
1294
+ if (profileSettings) {
1295
+ expandedRepo = expandPath(profileSettings.thoughtsRepo);
1296
+ }
1297
+ }
819
1298
  }
820
1299
  if (!mappedName) {
821
1300
  mappedName = getRepoNameFromMapping(config.repoMappings[currentRepo]);
822
1301
  }
823
1302
  const profileConfig = resolveProfileForRepo(config, currentRepo);
824
1303
  createThoughtsDirectoryStructure(profileConfig, mappedName, config.user);
825
- const thoughtsDir = path6.join(currentRepo, "thoughts");
826
- if (fs4.existsSync(thoughtsDir)) {
827
- const searchableDir = path6.join(thoughtsDir, "searchable");
828
- if (fs4.existsSync(searchableDir)) {
1304
+ const thoughtsDir = path10.join(currentRepo, "thoughts");
1305
+ if (fs7.existsSync(thoughtsDir)) {
1306
+ const searchableDir = path10.join(thoughtsDir, "searchable");
1307
+ if (fs7.existsSync(searchableDir)) {
829
1308
  try {
830
- execSync2(`chmod -R 755 "${searchableDir}"`, { stdio: "pipe" });
1309
+ execSync4(`chmod -R 755 "${searchableDir}"`, { stdio: "pipe" });
831
1310
  } catch {
832
1311
  }
833
1312
  }
834
- fs4.rmSync(thoughtsDir, { recursive: true, force: true });
1313
+ fs7.rmSync(thoughtsDir, { recursive: true, force: true });
835
1314
  }
836
- fs4.mkdirSync(thoughtsDir);
1315
+ fs7.mkdirSync(thoughtsDir);
837
1316
  const repoTarget = getRepoThoughtsPath(profileConfig, mappedName);
838
1317
  const globalTarget = getGlobalThoughtsPath(profileConfig);
839
- fs4.symlinkSync(path6.join(repoTarget, config.user), path6.join(thoughtsDir, config.user), "dir");
840
- fs4.symlinkSync(path6.join(repoTarget, "shared"), path6.join(thoughtsDir, "shared"), "dir");
841
- fs4.symlinkSync(globalTarget, path6.join(thoughtsDir, "global"), "dir");
1318
+ fs7.symlinkSync(path10.join(repoTarget, config.user), path10.join(thoughtsDir, config.user), "dir");
1319
+ fs7.symlinkSync(path10.join(repoTarget, "shared"), path10.join(thoughtsDir, "shared"), "dir");
1320
+ fs7.symlinkSync(globalTarget, path10.join(thoughtsDir, "global"), "dir");
842
1321
  const otherUsers = updateSymlinksForNewUsers(
843
1322
  currentRepo,
844
1323
  profileConfig,
@@ -848,18 +1327,8 @@ async function thoughtsInitCommand(options) {
848
1327
  if (otherUsers.length > 0) {
849
1328
  p.log.success(`Added symlinks for other users: ${otherUsers.join(", ")}`);
850
1329
  }
851
- try {
852
- execSync2("git remote get-url origin", { cwd: expandedRepo, stdio: "pipe" });
853
- try {
854
- execSync2("git pull --rebase", {
855
- stdio: "pipe",
856
- cwd: expandedRepo
857
- });
858
- p.log.success("Pulled latest thoughts from remote");
859
- } catch (error) {
860
- p.log.warn(`Could not pull latest thoughts: ${error.message}`);
861
- }
862
- } catch {
1330
+ if (pullThoughtsFromRemote(expandedRepo)) {
1331
+ p.log.success("Pulled latest thoughts from remote");
863
1332
  }
864
1333
  const claudeMd = generateClaudeMd({
865
1334
  thoughtsRepo: profileConfig.thoughtsRepo,
@@ -867,34 +1336,55 @@ async function thoughtsInitCommand(options) {
867
1336
  repoName: mappedName,
868
1337
  user: config.user
869
1338
  });
870
- fs4.writeFileSync(path6.join(thoughtsDir, "CLAUDE.md"), claudeMd);
1339
+ fs7.writeFileSync(path10.join(thoughtsDir, "CLAUDE.md"), claudeMd);
871
1340
  const hookResult = setupGitHooks(currentRepo);
872
1341
  if (hookResult.updated.length > 0) {
873
1342
  p.log.step(`Updated git hooks: ${hookResult.updated.join(", ")}`);
874
1343
  }
875
1344
  p.log.success("Thoughts setup complete!");
876
- const structureText = `${chalk2.cyan(currentRepo)}/
1345
+ const structureText = `${chalk5.cyan(currentRepo)}/
877
1346
  \u2514\u2500\u2500 thoughts/
878
- \u251C\u2500\u2500 ${config.user}/ ${chalk2.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.reposDir}/${mappedName}/${config.user}/`)}
879
- \u251C\u2500\u2500 shared/ ${chalk2.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.reposDir}/${mappedName}/shared/`)}
880
- \u2514\u2500\u2500 global/ ${chalk2.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.globalDir}/`)}
881
- \u251C\u2500\u2500 ${config.user}/ ${chalk2.gray("(your cross-repo notes)")}
882
- \u2514\u2500\u2500 shared/ ${chalk2.gray("(team cross-repo notes)")}`;
1347
+ \u251C\u2500\u2500 ${config.user}/ ${chalk5.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.reposDir}/${mappedName}/${config.user}/`)}
1348
+ \u251C\u2500\u2500 shared/ ${chalk5.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.reposDir}/${mappedName}/shared/`)}
1349
+ \u2514\u2500\u2500 global/ ${chalk5.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.globalDir}/`)}
1350
+ \u251C\u2500\u2500 ${config.user}/ ${chalk5.gray("(your cross-repo notes)")}
1351
+ \u2514\u2500\u2500 shared/ ${chalk5.gray("(team cross-repo notes)")}`;
883
1352
  p.note(structureText, "Repository structure created");
884
1353
  p.note(
885
- `${chalk2.green("\u2713")} Pre-commit hook: Prevents committing thoughts/
886
- ${chalk2.green("\u2713")} Post-commit hook: Auto-syncs thoughts after commits`,
1354
+ `${chalk5.green("\u2713")} Pre-commit hook: Prevents committing thoughts/
1355
+ ${chalk5.green("\u2713")} Post-commit hook: Auto-syncs thoughts after commits`,
887
1356
  "Protection enabled"
888
1357
  );
1358
+ const hooksConfig = loadHooksConfig(currentRepo);
1359
+ const postInitHooks = getHooksForEvent(hooksConfig, "PostThoughtsInit");
1360
+ if (postInitHooks.length > 0) {
1361
+ const hookInput = {
1362
+ hook_event_name: "PostThoughtsInit",
1363
+ cwd: currentRepo,
1364
+ thoughts_repo: profileConfig.thoughtsRepo,
1365
+ repos_dir: profileConfig.reposDir,
1366
+ global_dir: profileConfig.globalDir,
1367
+ mapped_name: mappedName,
1368
+ user: config.user
1369
+ };
1370
+ const hookEnv = {
1371
+ THC_THOUGHTS_REPO: profileConfig.thoughtsRepo,
1372
+ THC_REPOS_DIR: profileConfig.reposDir,
1373
+ THC_GLOBAL_DIR: profileConfig.globalDir,
1374
+ THC_MAPPED_NAME: mappedName,
1375
+ THC_USER: config.user
1376
+ };
1377
+ await executeHooks(postInitHooks, hookInput, hookEnv, true);
1378
+ }
889
1379
  p.outro(
890
- chalk2.gray("Next steps:\n") + chalk2.gray(
891
- ` 1. Run ${chalk2.cyan("thoughtcabinet sync")} to create the searchable index
1380
+ chalk5.gray("Next steps:\n") + chalk5.gray(
1381
+ ` 1. Run ${chalk5.cyan("thoughtcabinet sync")} to create the searchable index
892
1382
  `
893
- ) + chalk2.gray(
894
- ` 2. Create markdown files in ${chalk2.cyan(`thoughts/${config.user}/`)} for your notes
1383
+ ) + chalk5.gray(
1384
+ ` 2. Create markdown files in ${chalk5.cyan(`thoughts/${config.user}/`)} for your notes
895
1385
  `
896
- ) + chalk2.gray(` 3. Your thoughts will sync automatically when you commit code
897
- `) + chalk2.gray(` 4. Run ${chalk2.cyan("thoughtcabinet status")} to check sync status`)
1386
+ ) + chalk5.gray(` 3. Your thoughts will sync automatically when you commit code
1387
+ `) + chalk5.gray(` 4. Run ${chalk5.cyan("thoughtcabinet status")} to check sync status`)
898
1388
  );
899
1389
  } catch (error) {
900
1390
  p.log.error(`Error during thoughts init: ${error}`);
@@ -903,81 +1393,84 @@ ${chalk2.green("\u2713")} Post-commit hook: Auto-syncs thoughts after commits`,
903
1393
  }
904
1394
 
905
1395
  // src/commands/thoughts/destroy.ts
906
- import fs5 from "fs";
907
- import path7 from "path";
908
- import { execSync as execSync3 } from "child_process";
909
- import chalk3 from "chalk";
1396
+ import fs8 from "fs";
1397
+ import path11 from "path";
1398
+ import chalk6 from "chalk";
910
1399
  async function thoughtsDestoryCommand(options) {
911
1400
  try {
912
1401
  const currentRepo = getCurrentRepoPath();
913
- const thoughtsDir = path7.join(currentRepo, "thoughts");
914
- if (!fs5.existsSync(thoughtsDir)) {
915
- console.error(chalk3.red("Error: Thoughts not initialized for this repository."));
916
- process.exit(1);
917
- }
918
- const config = loadThoughtsConfig(options);
919
- if (!config) {
920
- console.error(chalk3.red("Error: Thoughts configuration not found."));
921
- process.exit(1);
922
- }
923
- const mapping = config.repoMappings[currentRepo];
924
- const mappedName = getRepoNameFromMapping(mapping);
925
- const profileName = getProfileNameFromMapping(mapping);
926
- if (!mappedName && !options.force) {
927
- console.error(chalk3.red("Error: This repository is not in the thoughts configuration."));
928
- console.error(chalk3.yellow("Use --force to remove the thoughts directory anyway."));
929
- process.exit(1);
930
- }
931
- console.log(chalk3.blue("Removing thoughts setup from current repository..."));
932
- const searchableDir = path7.join(thoughtsDir, "searchable");
933
- if (fs5.existsSync(searchableDir)) {
934
- console.log(chalk3.gray("Removing searchable directory..."));
935
- try {
936
- execSync3(`chmod -R 755 "${searchableDir}"`, { stdio: "pipe" });
937
- } catch {
938
- }
939
- fs5.rmSync(searchableDir, { recursive: true, force: true });
1402
+ const thoughtsDir = path11.join(currentRepo, "thoughts");
1403
+ if (!fs8.existsSync(thoughtsDir)) {
1404
+ console.error(chalk6.red("Error: Thoughts not initialized for this repository."));
1405
+ process.exit(1);
940
1406
  }
941
- console.log(chalk3.gray("Removing thoughts directory (symlinks only)..."));
942
- try {
943
- fs5.rmSync(thoughtsDir, { recursive: true, force: true });
944
- } catch (error) {
945
- console.error(chalk3.red(`Error removing thoughts directory: ${error}`));
946
- console.error(chalk3.yellow("You may need to manually remove: " + thoughtsDir));
1407
+ const config = loadThoughtsConfig(options);
1408
+ if (!config) {
1409
+ console.error(chalk6.red("Error: Thoughts configuration not found."));
947
1410
  process.exit(1);
948
1411
  }
949
- if (mappedName) {
950
- console.log(chalk3.gray("Removing repository from thoughts configuration..."));
951
- delete config.repoMappings[currentRepo];
1412
+ const mapping = config.repoMappings[currentRepo];
1413
+ if (!mapping && !options.force) {
1414
+ console.error(chalk6.red("Error: This repository is not in the thoughts configuration."));
1415
+ console.error(chalk6.yellow("Use --force to remove the thoughts directory anyway."));
1416
+ process.exit(1);
1417
+ }
1418
+ console.log(chalk6.blue("Removing thoughts setup from current repository..."));
1419
+ const result = cleanupThoughtsDirectory({
1420
+ repoPath: currentRepo,
1421
+ config,
1422
+ force: options.force,
1423
+ verbose: true
1424
+ });
1425
+ if (result.configRemoved) {
952
1426
  saveThoughtsConfig(config, options);
953
1427
  }
954
- console.log(chalk3.green("\u2705 Thoughts removed from repository"));
955
- if (mappedName) {
1428
+ console.log(chalk6.green("\u2705 Thoughts removed from repository"));
1429
+ if (result.mappedName) {
956
1430
  console.log("");
957
- console.log(chalk3.gray("Note: Your thoughts content remains safe in:"));
958
- if (profileName && config.profiles && config.profiles[profileName]) {
959
- const profile = config.profiles[profileName];
960
- console.log(chalk3.gray(` ${profile.thoughtsRepo}/${profile.reposDir}/${mappedName}`));
961
- console.log(chalk3.gray(` (profile: ${profileName})`));
1431
+ console.log(chalk6.gray("Note: Your thoughts content remains safe in:"));
1432
+ if (result.profileName && config.profiles && config.profiles[result.profileName]) {
1433
+ const profile = config.profiles[result.profileName];
1434
+ console.log(chalk6.gray(` ${profile.thoughtsRepo}/${profile.reposDir}/${result.mappedName}`));
1435
+ console.log(chalk6.gray(` (profile: ${result.profileName})`));
962
1436
  } else {
963
- console.log(chalk3.gray(` ${config.thoughtsRepo}/${config.reposDir}/${mappedName}`));
1437
+ console.log(chalk6.gray(` ${config.thoughtsRepo}/${config.reposDir}/${result.mappedName}`));
964
1438
  }
965
- console.log(chalk3.gray("Only the local symlinks and configuration were removed."));
1439
+ console.log(chalk6.gray("Only the local symlinks and configuration were removed."));
1440
+ }
1441
+ const hooksConfig = loadHooksConfig(currentRepo);
1442
+ const postDestroyHooks = getHooksForEvent(hooksConfig, "PostThoughtsDestroy");
1443
+ if (postDestroyHooks.length > 0) {
1444
+ const hookInput = {
1445
+ hook_event_name: "PostThoughtsDestroy",
1446
+ cwd: currentRepo,
1447
+ thoughts_removed: result.thoughtsRemoved,
1448
+ config_removed: result.configRemoved,
1449
+ mapped_name: result.mappedName,
1450
+ profile_name: result.profileName
1451
+ };
1452
+ const hookEnv = {
1453
+ THC_THOUGHTS_REMOVED: result.thoughtsRemoved ? "true" : "false",
1454
+ THC_CONFIG_REMOVED: result.configRemoved ? "true" : "false",
1455
+ THC_MAPPED_NAME: result.mappedName || "",
1456
+ THC_PROFILE_NAME: result.profileName || ""
1457
+ };
1458
+ await executeHooks(postDestroyHooks, hookInput, hookEnv, true);
966
1459
  }
967
1460
  } catch (error) {
968
- console.error(chalk3.red(`Error during thoughts destroy: ${error}`));
1461
+ console.error(chalk6.red(`Error during thoughts destroy: ${error}`));
969
1462
  process.exit(1);
970
1463
  }
971
1464
  }
972
1465
 
973
1466
  // src/commands/thoughts/sync.ts
974
- import fs6 from "fs";
975
- import path8 from "path";
976
- import { execSync as execSync4, execFileSync } from "child_process";
977
- import chalk4 from "chalk";
1467
+ import fs9 from "fs";
1468
+ import path12 from "path";
1469
+ import { execSync as execSync5, execFileSync as execFileSync2 } from "child_process";
1470
+ import chalk7 from "chalk";
978
1471
  function checkGitStatus(repoPath) {
979
1472
  try {
980
- const status = execSync4("git status --porcelain", {
1473
+ const status = execSync5("git status --porcelain", {
981
1474
  cwd: repoPath,
982
1475
  encoding: "utf8",
983
1476
  stdio: "pipe"
@@ -990,115 +1483,59 @@ function checkGitStatus(repoPath) {
990
1483
  function syncThoughts(thoughtsRepo, message) {
991
1484
  const expandedRepo = expandPath(thoughtsRepo);
992
1485
  try {
993
- execSync4("git add -A", { cwd: expandedRepo, stdio: "pipe" });
1486
+ execSync5("git add -A", { cwd: expandedRepo, stdio: "pipe" });
994
1487
  const hasChanges = checkGitStatus(expandedRepo);
995
1488
  if (hasChanges) {
996
1489
  const commitMessage = message || `Sync thoughts - ${(/* @__PURE__ */ new Date()).toISOString()}`;
997
- execFileSync("git", ["commit", "-m", commitMessage], { cwd: expandedRepo, stdio: "pipe" });
998
- console.log(chalk4.green("\u2705 Thoughts synchronized"));
1490
+ execFileSync2("git", ["commit", "-m", commitMessage], { cwd: expandedRepo, stdio: "pipe" });
1491
+ console.log(chalk7.green("\u2705 Thoughts synchronized"));
999
1492
  } else {
1000
- console.log(chalk4.gray("No changes to commit"));
1493
+ console.log(chalk7.gray("No changes to commit"));
1001
1494
  }
1002
1495
  try {
1003
- execSync4("git pull --rebase", {
1496
+ execSync5("git pull --rebase", {
1004
1497
  stdio: "pipe",
1005
1498
  cwd: expandedRepo
1006
1499
  });
1007
1500
  } catch (error) {
1008
1501
  const errorStr = error.toString();
1009
1502
  if (errorStr.includes("CONFLICT (") || errorStr.includes("Automatic merge failed") || errorStr.includes("Patch failed at") || errorStr.includes('When you have resolved this problem, run "git rebase --continue"')) {
1010
- console.error(chalk4.red("Error: Merge conflict detected in thoughts repository"));
1011
- console.error(chalk4.red("Please resolve conflicts manually in:"), expandedRepo);
1012
- console.error(chalk4.red('Then run "git rebase --continue" and "thoughtcabinet sync" again'));
1503
+ console.error(chalk7.red("Error: Merge conflict detected in thoughts repository"));
1504
+ console.error(chalk7.red("Please resolve conflicts manually in:"), expandedRepo);
1505
+ console.error(chalk7.red('Then run "git rebase --continue" and "thoughtcabinet sync" again'));
1013
1506
  process.exit(1);
1014
1507
  } else {
1015
- console.warn(chalk4.yellow("Warning: Could not pull latest changes:"), error.message);
1508
+ console.warn(chalk7.yellow("Warning: Could not pull latest changes:"), error.message);
1016
1509
  }
1017
1510
  }
1018
1511
  try {
1019
- execSync4("git remote get-url origin", { cwd: expandedRepo, stdio: "pipe" });
1020
- console.log(chalk4.gray("Pushing to remote..."));
1512
+ execSync5("git remote get-url origin", { cwd: expandedRepo, stdio: "pipe" });
1513
+ console.log(chalk7.gray("Pushing to remote..."));
1021
1514
  try {
1022
- execSync4("git push", { cwd: expandedRepo, stdio: "pipe" });
1023
- console.log(chalk4.green("\u2705 Pushed to remote"));
1515
+ execSync5("git push", { cwd: expandedRepo, stdio: "pipe" });
1516
+ console.log(chalk7.green("\u2705 Pushed to remote"));
1024
1517
  } catch {
1025
- console.log(chalk4.yellow("\u26A0\uFE0F Could not push to remote. You may need to push manually."));
1518
+ console.log(chalk7.yellow("\u26A0\uFE0F Could not push to remote. You may need to push manually."));
1026
1519
  }
1027
1520
  } catch {
1028
- console.log(chalk4.yellow("\u2139\uFE0F No remote configured for thoughts repository"));
1521
+ console.log(chalk7.yellow("\u2139\uFE0F No remote configured for thoughts repository"));
1029
1522
  }
1030
1523
  } catch (error) {
1031
- console.error(chalk4.red(`Error syncing thoughts: ${error}`));
1524
+ console.error(chalk7.red(`Error syncing thoughts: ${error}`));
1032
1525
  process.exit(1);
1033
1526
  }
1034
1527
  }
1035
- function createSearchDirectory(thoughtsDir) {
1036
- const searchDir = path8.join(thoughtsDir, "searchable");
1037
- if (fs6.existsSync(searchDir)) {
1038
- try {
1039
- execSync4(`chmod -R 755 "${searchDir}"`, { stdio: "pipe" });
1040
- } catch {
1041
- }
1042
- fs6.rmSync(searchDir, { recursive: true, force: true });
1043
- }
1044
- fs6.mkdirSync(searchDir, { recursive: true });
1045
- function findFilesFollowingSymlinks(dir, baseDir = dir, visited = /* @__PURE__ */ new Set()) {
1046
- const files = [];
1047
- const realPath = fs6.realpathSync(dir);
1048
- if (visited.has(realPath)) {
1049
- return files;
1050
- }
1051
- visited.add(realPath);
1052
- const entries = fs6.readdirSync(dir, { withFileTypes: true });
1053
- for (const entry of entries) {
1054
- const fullPath = path8.join(dir, entry.name);
1055
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
1056
- files.push(...findFilesFollowingSymlinks(fullPath, baseDir, visited));
1057
- } else if (entry.isSymbolicLink() && !entry.name.startsWith(".")) {
1058
- try {
1059
- const stat = fs6.statSync(fullPath);
1060
- if (stat.isDirectory()) {
1061
- files.push(...findFilesFollowingSymlinks(fullPath, baseDir, visited));
1062
- } else if (stat.isFile() && path8.basename(fullPath) !== "CLAUDE.md") {
1063
- files.push(path8.relative(baseDir, fullPath));
1064
- }
1065
- } catch {
1066
- }
1067
- } else if (entry.isFile() && !entry.name.startsWith(".") && entry.name !== "CLAUDE.md") {
1068
- files.push(path8.relative(baseDir, fullPath));
1069
- }
1070
- }
1071
- return files;
1072
- }
1073
- const allFiles = findFilesFollowingSymlinks(thoughtsDir);
1074
- let linkedCount = 0;
1075
- for (const relPath of allFiles) {
1076
- const sourcePath = path8.join(thoughtsDir, relPath);
1077
- const targetPath = path8.join(searchDir, relPath);
1078
- const targetDir = path8.dirname(targetPath);
1079
- if (!fs6.existsSync(targetDir)) {
1080
- fs6.mkdirSync(targetDir, { recursive: true });
1081
- }
1082
- try {
1083
- const realSourcePath = fs6.realpathSync(sourcePath);
1084
- fs6.linkSync(realSourcePath, targetPath);
1085
- linkedCount++;
1086
- } catch {
1087
- }
1088
- }
1089
- console.log(chalk4.gray(`Created ${linkedCount} hard links in searchable directory`));
1090
- }
1091
1528
  async function thoughtsSyncCommand(options) {
1092
1529
  try {
1093
1530
  const config = loadThoughtsConfig(options);
1094
1531
  if (!config) {
1095
- console.error(chalk4.red('Error: Thoughts not configured. Run "thoughtcabinet init" first.'));
1532
+ console.error(chalk7.red('Error: Thoughts not configured. Run "thoughtcabinet init" first.'));
1096
1533
  process.exit(1);
1097
1534
  }
1098
1535
  const currentRepo = getCurrentRepoPath();
1099
- const thoughtsDir = path8.join(currentRepo, "thoughts");
1100
- if (!fs6.existsSync(thoughtsDir)) {
1101
- console.error(chalk4.red("Error: Thoughts not initialized for this repository."));
1536
+ const thoughtsDir = path12.join(currentRepo, "thoughts");
1537
+ if (!fs9.existsSync(thoughtsDir)) {
1538
+ console.error(chalk7.red("Error: Thoughts not initialized for this repository."));
1102
1539
  console.error('Run "thoughtcabinet init" to set up thoughts.');
1103
1540
  process.exit(1);
1104
1541
  }
@@ -1113,27 +1550,45 @@ async function thoughtsSyncCommand(options) {
1113
1550
  config.user
1114
1551
  );
1115
1552
  if (newUsers.length > 0) {
1116
- console.log(chalk4.green(`\u2713 Added symlinks for new users: ${newUsers.join(", ")}`));
1553
+ console.log(chalk7.green(`\u2713 Added symlinks for new users: ${newUsers.join(", ")}`));
1117
1554
  }
1118
1555
  }
1119
- console.log(chalk4.blue("Creating searchable index..."));
1120
- createSearchDirectory(thoughtsDir);
1121
- console.log(chalk4.blue("Syncing thoughts..."));
1556
+ console.log(chalk7.blue("Creating searchable index..."));
1557
+ const linkedCount = createSearchableIndex(thoughtsDir);
1558
+ console.log(chalk7.gray(`Created ${linkedCount} hard links in searchable directory`));
1559
+ console.log(chalk7.blue("Syncing thoughts..."));
1122
1560
  syncThoughts(profileConfig.thoughtsRepo, options.message || "");
1561
+ const hooksConfig = loadHooksConfig(currentRepo);
1562
+ const postSyncHooks = getHooksForEvent(hooksConfig, "PostThoughtsSync");
1563
+ if (postSyncHooks.length > 0) {
1564
+ const hookInput = {
1565
+ hook_event_name: "PostThoughtsSync",
1566
+ cwd: currentRepo,
1567
+ thoughts_repo: profileConfig.thoughtsRepo,
1568
+ has_changes: true,
1569
+ searchable_created: true
1570
+ };
1571
+ const hookEnv = {
1572
+ THC_THOUGHTS_REPO: profileConfig.thoughtsRepo,
1573
+ THC_HAS_CHANGES: "true",
1574
+ THC_SEARCHABLE_CREATED: "true"
1575
+ };
1576
+ await executeHooks(postSyncHooks, hookInput, hookEnv, true);
1577
+ }
1123
1578
  } catch (error) {
1124
- console.error(chalk4.red(`Error during thoughts sync: ${error}`));
1579
+ console.error(chalk7.red(`Error during thoughts sync: ${error}`));
1125
1580
  process.exit(1);
1126
1581
  }
1127
1582
  }
1128
1583
 
1129
1584
  // src/commands/thoughts/status.ts
1130
- import fs7 from "fs";
1131
- import path9 from "path";
1132
- import { execSync as execSync5 } from "child_process";
1133
- import chalk5 from "chalk";
1585
+ import fs10 from "fs";
1586
+ import path13 from "path";
1587
+ import { execSync as execSync6 } from "child_process";
1588
+ import chalk8 from "chalk";
1134
1589
  function getGitStatus(repoPath) {
1135
1590
  try {
1136
- return execSync5("git status -sb", {
1591
+ return execSync6("git status -sb", {
1137
1592
  cwd: repoPath,
1138
1593
  encoding: "utf8",
1139
1594
  stdio: "pipe"
@@ -1144,7 +1599,7 @@ function getGitStatus(repoPath) {
1144
1599
  }
1145
1600
  function getUncommittedChanges(repoPath) {
1146
1601
  try {
1147
- const output = execSync5("git status --porcelain", {
1602
+ const output = execSync6("git status --porcelain", {
1148
1603
  cwd: repoPath,
1149
1604
  encoding: "utf8",
1150
1605
  stdio: "pipe"
@@ -1158,7 +1613,7 @@ function getUncommittedChanges(repoPath) {
1158
1613
  else if (status[0] === "D") statusText = "deleted";
1159
1614
  else if (status[0] === "?") statusText = "untracked";
1160
1615
  else if (status[0] === "R") statusText = "renamed";
1161
- return ` ${chalk5.yellow(statusText.padEnd(10))} ${file}`;
1616
+ return ` ${chalk8.yellow(statusText.padEnd(10))} ${file}`;
1162
1617
  });
1163
1618
  } catch {
1164
1619
  return [];
@@ -1166,7 +1621,7 @@ function getUncommittedChanges(repoPath) {
1166
1621
  }
1167
1622
  function getLastCommit(repoPath) {
1168
1623
  try {
1169
- return execSync5('git log -1 --pretty=format:"%h %s (%cr)"', {
1624
+ return execSync6('git log -1 --pretty=format:"%h %s (%cr)"', {
1170
1625
  cwd: repoPath,
1171
1626
  encoding: "utf8",
1172
1627
  stdio: "pipe"
@@ -1177,64 +1632,64 @@ function getLastCommit(repoPath) {
1177
1632
  }
1178
1633
  function getRemoteStatus(repoPath) {
1179
1634
  try {
1180
- execSync5("git remote get-url origin", { cwd: repoPath, stdio: "pipe" });
1635
+ execSync6("git remote get-url origin", { cwd: repoPath, stdio: "pipe" });
1181
1636
  try {
1182
- execSync5("git fetch", { cwd: repoPath, stdio: "pipe" });
1637
+ execSync6("git fetch", { cwd: repoPath, stdio: "pipe" });
1183
1638
  } catch {
1184
1639
  }
1185
- const status = execSync5("git status -sb", {
1640
+ const status = execSync6("git status -sb", {
1186
1641
  cwd: repoPath,
1187
1642
  encoding: "utf8",
1188
1643
  stdio: "pipe"
1189
1644
  });
1190
1645
  if (status.includes("ahead")) {
1191
1646
  const ahead = status.match(/ahead (\d+)/)?.[1] || "?";
1192
- return chalk5.yellow(`${ahead} commits ahead of remote`);
1647
+ return chalk8.yellow(`${ahead} commits ahead of remote`);
1193
1648
  } else if (status.includes("behind")) {
1194
1649
  const behind = status.match(/behind (\d+)/)?.[1] || "?";
1195
1650
  try {
1196
- execSync5("git pull --rebase", {
1651
+ execSync6("git pull --rebase", {
1197
1652
  stdio: "pipe",
1198
1653
  cwd: repoPath
1199
1654
  });
1200
- console.log(chalk5.green("\u2713 Automatically pulled latest changes"));
1201
- const newStatus = execSync5("git status -sb", {
1655
+ console.log(chalk8.green("\u2713 Automatically pulled latest changes"));
1656
+ const newStatus = execSync6("git status -sb", {
1202
1657
  encoding: "utf8",
1203
1658
  cwd: repoPath,
1204
1659
  stdio: "pipe"
1205
1660
  });
1206
1661
  if (newStatus.includes("behind")) {
1207
1662
  const newBehind = newStatus.match(/behind (\d+)/)?.[1] || "?";
1208
- return chalk5.yellow(`${newBehind} commits behind remote (after pull)`);
1663
+ return chalk8.yellow(`${newBehind} commits behind remote (after pull)`);
1209
1664
  } else {
1210
- return chalk5.green("Up to date with remote (after pull)");
1665
+ return chalk8.green("Up to date with remote (after pull)");
1211
1666
  }
1212
1667
  } catch {
1213
- return chalk5.yellow(`${behind} commits behind remote`);
1668
+ return chalk8.yellow(`${behind} commits behind remote`);
1214
1669
  }
1215
1670
  } else {
1216
- return chalk5.green("Up to date with remote");
1671
+ return chalk8.green("Up to date with remote");
1217
1672
  }
1218
1673
  } catch {
1219
- return chalk5.gray("No remote configured");
1674
+ return chalk8.gray("No remote configured");
1220
1675
  }
1221
1676
  }
1222
1677
  async function thoughtsStatusCommand(options) {
1223
1678
  try {
1224
1679
  const config = loadThoughtsConfig(options);
1225
1680
  if (!config) {
1226
- console.error(chalk5.red('Error: Thoughts not configured. Run "thoughtcabinet init" first.'));
1681
+ console.error(chalk8.red('Error: Thoughts not configured. Run "thoughtcabinet init" first.'));
1227
1682
  process.exit(1);
1228
1683
  }
1229
- console.log(chalk5.blue("Thoughts Repository Status"));
1230
- console.log(chalk5.gray("=".repeat(50)));
1684
+ console.log(chalk8.blue("Thoughts Repository Status"));
1685
+ console.log(chalk8.gray("=".repeat(50)));
1231
1686
  console.log("");
1232
- console.log(chalk5.yellow("Configuration:"));
1233
- console.log(` Repository: ${chalk5.cyan(config.thoughtsRepo)}`);
1234
- console.log(` Repos directory: ${chalk5.cyan(config.reposDir)}`);
1235
- console.log(` Global directory: ${chalk5.cyan(config.globalDir)}`);
1236
- console.log(` User: ${chalk5.cyan(config.user)}`);
1237
- console.log(` Mapped repos: ${chalk5.cyan(Object.keys(config.repoMappings).length)}`);
1687
+ console.log(chalk8.yellow("Configuration:"));
1688
+ console.log(` Repository: ${chalk8.cyan(config.thoughtsRepo)}`);
1689
+ console.log(` Repos directory: ${chalk8.cyan(config.reposDir)}`);
1690
+ console.log(` Global directory: ${chalk8.cyan(config.globalDir)}`);
1691
+ console.log(` User: ${chalk8.cyan(config.user)}`);
1692
+ console.log(` Mapped repos: ${chalk8.cyan(Object.keys(config.repoMappings).length)}`);
1238
1693
  console.log("");
1239
1694
  const currentRepo = getCurrentRepoPath();
1240
1695
  const currentMapping = config.repoMappings[currentRepo];
@@ -1242,28 +1697,28 @@ async function thoughtsStatusCommand(options) {
1242
1697
  const profileName = getProfileNameFromMapping(currentMapping);
1243
1698
  const profileConfig = resolveProfileForRepo(config, currentRepo);
1244
1699
  if (mappedName) {
1245
- console.log(chalk5.yellow("Current Repository:"));
1246
- console.log(` Path: ${chalk5.cyan(currentRepo)}`);
1247
- console.log(` Thoughts directory: ${chalk5.cyan(`${profileConfig.reposDir}/${mappedName}`)}`);
1700
+ console.log(chalk8.yellow("Current Repository:"));
1701
+ console.log(` Path: ${chalk8.cyan(currentRepo)}`);
1702
+ console.log(` Thoughts directory: ${chalk8.cyan(`${profileConfig.reposDir}/${mappedName}`)}`);
1248
1703
  if (profileName) {
1249
- console.log(` Profile: ${chalk5.cyan(profileName)}`);
1704
+ console.log(` Profile: ${chalk8.cyan(profileName)}`);
1250
1705
  } else {
1251
- console.log(` Profile: ${chalk5.gray("(default)")}`);
1706
+ console.log(` Profile: ${chalk8.gray("(default)")}`);
1252
1707
  }
1253
- const thoughtsDir = path9.join(currentRepo, "thoughts");
1254
- if (fs7.existsSync(thoughtsDir)) {
1255
- console.log(` Status: ${chalk5.green("\u2713 Initialized")}`);
1708
+ const thoughtsDir = path13.join(currentRepo, "thoughts");
1709
+ if (fs10.existsSync(thoughtsDir)) {
1710
+ console.log(` Status: ${chalk8.green("\u2713 Initialized")}`);
1256
1711
  } else {
1257
- console.log(` Status: ${chalk5.red("\u2717 Not initialized")}`);
1712
+ console.log(` Status: ${chalk8.red("\u2717 Not initialized")}`);
1258
1713
  }
1259
1714
  } else {
1260
- console.log(chalk5.yellow("Current repository not mapped to thoughts"));
1715
+ console.log(chalk8.yellow("Current repository not mapped to thoughts"));
1261
1716
  }
1262
1717
  console.log("");
1263
1718
  const expandedRepo = expandPath(profileConfig.thoughtsRepo);
1264
- console.log(chalk5.yellow("Thoughts Repository Git Status:"));
1719
+ console.log(chalk8.yellow("Thoughts Repository Git Status:"));
1265
1720
  if (profileName) {
1266
- console.log(chalk5.gray(` (using profile: ${profileName})`));
1721
+ console.log(chalk8.gray(` (using profile: ${profileName})`));
1267
1722
  }
1268
1723
  console.log(` ${getGitStatus(expandedRepo)}`);
1269
1724
  console.log(` Remote: ${getRemoteStatus(expandedRepo)}`);
@@ -1271,33 +1726,33 @@ async function thoughtsStatusCommand(options) {
1271
1726
  console.log("");
1272
1727
  const changes = getUncommittedChanges(expandedRepo);
1273
1728
  if (changes.length > 0) {
1274
- console.log(chalk5.yellow("Uncommitted changes:"));
1729
+ console.log(chalk8.yellow("Uncommitted changes:"));
1275
1730
  changes.forEach((change) => console.log(change));
1276
1731
  console.log("");
1277
- console.log(chalk5.gray('Run "thoughtcabinet sync" to commit these changes'));
1732
+ console.log(chalk8.gray('Run "thoughtcabinet sync" to commit these changes'));
1278
1733
  } else {
1279
- console.log(chalk5.green("\u2713 No uncommitted changes"));
1734
+ console.log(chalk8.green("\u2713 No uncommitted changes"));
1280
1735
  }
1281
1736
  } catch (error) {
1282
- console.error(chalk5.red(`Error checking thoughts status: ${error}`));
1737
+ console.error(chalk8.red(`Error checking thoughts status: ${error}`));
1283
1738
  process.exit(1);
1284
1739
  }
1285
1740
  }
1286
1741
 
1287
1742
  // src/commands/thoughts/config.ts
1288
- import { spawn } from "child_process";
1289
- import chalk6 from "chalk";
1743
+ import { spawn as spawn2 } from "child_process";
1744
+ import chalk9 from "chalk";
1290
1745
  async function thoughtsConfigCommand(options) {
1291
1746
  try {
1292
1747
  const configPath = options.configFile || getDefaultConfigPath();
1293
1748
  if (options.edit) {
1294
1749
  const editor = process.env.EDITOR || "vi";
1295
- spawn(editor, [configPath], { stdio: "inherit" });
1750
+ spawn2(editor, [configPath], { stdio: "inherit" });
1296
1751
  return;
1297
1752
  }
1298
1753
  const config = loadThoughtsConfig(options);
1299
1754
  if (!config) {
1300
- console.error(chalk6.red("No thoughts configuration found."));
1755
+ console.error(chalk9.red("No thoughts configuration found."));
1301
1756
  console.error('Run "thoughtcabinet init" to create one.');
1302
1757
  process.exit(1);
1303
1758
  }
@@ -1305,52 +1760,113 @@ async function thoughtsConfigCommand(options) {
1305
1760
  console.log(JSON.stringify(config, null, 2));
1306
1761
  return;
1307
1762
  }
1308
- console.log(chalk6.blue("Thoughts Configuration"));
1309
- console.log(chalk6.gray("=".repeat(50)));
1763
+ console.log(chalk9.blue("Thoughts Configuration"));
1764
+ console.log(chalk9.gray("=".repeat(50)));
1310
1765
  console.log("");
1311
- console.log(chalk6.yellow("Settings:"));
1312
- console.log(` Config file: ${chalk6.cyan(configPath)}`);
1313
- console.log(` Thoughts repository: ${chalk6.cyan(config.thoughtsRepo)}`);
1314
- console.log(` Repos directory: ${chalk6.cyan(config.reposDir)}`);
1315
- console.log(` Global directory: ${chalk6.cyan(config.globalDir)}`);
1316
- console.log(` User: ${chalk6.cyan(config.user)}`);
1766
+ console.log(chalk9.yellow("Settings:"));
1767
+ console.log(` Config file: ${chalk9.cyan(configPath)}`);
1768
+ console.log(` Thoughts repository: ${chalk9.cyan(config.thoughtsRepo)}`);
1769
+ console.log(` Repos directory: ${chalk9.cyan(config.reposDir)}`);
1770
+ console.log(` Global directory: ${chalk9.cyan(config.globalDir)}`);
1771
+ console.log(` User: ${chalk9.cyan(config.user)}`);
1317
1772
  console.log("");
1318
- console.log(chalk6.yellow("Repository Mappings:"));
1773
+ console.log(chalk9.yellow("Repository Mappings:"));
1319
1774
  const mappings = Object.entries(config.repoMappings);
1320
1775
  if (mappings.length === 0) {
1321
- console.log(chalk6.gray(" No repositories mapped yet"));
1776
+ console.log(chalk9.gray(" No repositories mapped yet"));
1322
1777
  } else {
1323
1778
  mappings.forEach(([repo, mapping]) => {
1324
1779
  const repoName = getRepoNameFromMapping(mapping);
1325
1780
  const profileName = getProfileNameFromMapping(mapping);
1326
- console.log(` ${chalk6.cyan(repo)}`);
1327
- console.log(` \u2192 ${chalk6.green(`${config.reposDir}/${repoName}`)}`);
1781
+ console.log(` ${chalk9.cyan(repo)}`);
1782
+ console.log(` \u2192 ${chalk9.green(`${config.reposDir}/${repoName}`)}`);
1328
1783
  if (profileName) {
1329
- console.log(` Profile: ${chalk6.yellow(profileName)}`);
1784
+ console.log(` Profile: ${chalk9.yellow(profileName)}`);
1330
1785
  } else {
1331
- console.log(` Profile: ${chalk6.gray("(default)")}`);
1786
+ console.log(` Profile: ${chalk9.gray("(default)")}`);
1332
1787
  }
1333
1788
  });
1334
1789
  }
1335
1790
  console.log("");
1336
- console.log(chalk6.yellow("Profiles:"));
1791
+ console.log(chalk9.yellow("Profiles:"));
1337
1792
  if (!config.profiles || Object.keys(config.profiles).length === 0) {
1338
- console.log(chalk6.gray(" No profiles configured"));
1793
+ console.log(chalk9.gray(" No profiles configured"));
1339
1794
  } else {
1340
1795
  Object.keys(config.profiles).forEach((name) => {
1341
- console.log(` ${chalk6.cyan(name)}`);
1796
+ console.log(` ${chalk9.cyan(name)}`);
1342
1797
  });
1343
1798
  }
1344
1799
  console.log("");
1345
- console.log(chalk6.gray("To edit configuration, run: thoughtcabinet config --edit"));
1800
+ console.log(chalk9.gray("To edit configuration, run: thoughtcabinet config --edit"));
1801
+ } catch (error) {
1802
+ console.error(chalk9.red(`Error showing thoughts config: ${error}`));
1803
+ process.exit(1);
1804
+ }
1805
+ }
1806
+
1807
+ // src/commands/thoughts/prune.ts
1808
+ import fs11 from "fs";
1809
+ import chalk10 from "chalk";
1810
+ async function thoughtsPruneCommand(options) {
1811
+ try {
1812
+ const config = loadThoughtsConfig(options);
1813
+ if (!config) {
1814
+ console.error(chalk10.red("Error: Thoughts configuration not found."));
1815
+ console.error('Run "thoughtcabinet init" to create one.');
1816
+ process.exit(1);
1817
+ }
1818
+ const mappings = Object.entries(config.repoMappings);
1819
+ if (mappings.length === 0) {
1820
+ console.log(chalk10.gray("No repository mappings configured."));
1821
+ return;
1822
+ }
1823
+ const staleEntries = [];
1824
+ for (const [repoPath, mapping] of mappings) {
1825
+ if (!fs11.existsSync(repoPath)) {
1826
+ staleEntries.push({
1827
+ repoPath,
1828
+ mappedName: getRepoNameFromMapping(mapping),
1829
+ profileName: getProfileNameFromMapping(mapping)
1830
+ });
1831
+ }
1832
+ }
1833
+ if (staleEntries.length === 0) {
1834
+ console.log(chalk10.green("No stale repository mappings found."));
1835
+ console.log(chalk10.gray(`All ${mappings.length} mapped repositories exist.`));
1836
+ return;
1837
+ }
1838
+ console.log(chalk10.yellow(`Found ${staleEntries.length} stale repository mapping(s):`));
1839
+ console.log("");
1840
+ for (const entry of staleEntries) {
1841
+ console.log(` ${chalk10.red("\u2717")} ${chalk10.cyan(entry.repoPath)}`);
1842
+ console.log(` \u2192 ${chalk10.gray(entry.mappedName || "(unknown)")}`);
1843
+ if (entry.profileName) {
1844
+ console.log(` Profile: ${chalk10.yellow(entry.profileName)}`);
1845
+ }
1846
+ }
1847
+ console.log("");
1848
+ if (options.apply) {
1849
+ console.log(chalk10.blue("Removing stale entries from configuration..."));
1850
+ for (const entry of staleEntries) {
1851
+ delete config.repoMappings[entry.repoPath];
1852
+ }
1853
+ saveThoughtsConfig(config, options);
1854
+ console.log(chalk10.green(`\u2705 Removed ${staleEntries.length} stale mapping(s).`));
1855
+ console.log("");
1856
+ console.log(chalk10.gray("Note: The thoughts content in your thoughts repository was not modified."));
1857
+ console.log(chalk10.gray("Only the configuration entries pointing to non-existent directories were removed."));
1858
+ } else {
1859
+ console.log(chalk10.gray("This is a dry run. No changes were made."));
1860
+ console.log(chalk10.gray("Run with --apply to remove these stale entries."));
1861
+ }
1346
1862
  } catch (error) {
1347
- console.error(chalk6.red(`Error showing thoughts config: ${error}`));
1863
+ console.error(chalk10.red(`Error during thoughts prune: ${error}`));
1348
1864
  process.exit(1);
1349
1865
  }
1350
1866
  }
1351
1867
 
1352
1868
  // src/commands/thoughts/profile/create.ts
1353
- import chalk7 from "chalk";
1869
+ import chalk11 from "chalk";
1354
1870
  import * as p2 from "@clack/prompts";
1355
1871
  async function profileCreateCommand(profileName, options) {
1356
1872
  try {
@@ -1371,7 +1887,7 @@ async function profileCreateCommand(profileName, options) {
1371
1887
  if (sanitizedName !== profileName) {
1372
1888
  p2.log.warn(`Profile name sanitized: "${profileName}" \u2192 "${sanitizedName}"`);
1373
1889
  }
1374
- p2.intro(chalk7.blue(`Creating Profile: ${sanitizedName}`));
1890
+ p2.intro(chalk11.blue(`Creating Profile: ${sanitizedName}`));
1375
1891
  if (validateProfile(config, sanitizedName)) {
1376
1892
  p2.log.error(`Profile "${sanitizedName}" already exists.`);
1377
1893
  p2.log.info("Use a different name or delete the existing profile first.");
@@ -1432,15 +1948,15 @@ async function profileCreateCommand(profileName, options) {
1432
1948
  ensureThoughtsRepoExists(profileConfig);
1433
1949
  p2.log.success(`Profile "${sanitizedName}" created successfully!`);
1434
1950
  p2.note(
1435
- `Name: ${chalk7.cyan(sanitizedName)}
1436
- Thoughts repository: ${chalk7.cyan(thoughtsRepo)}
1437
- Repos directory: ${chalk7.cyan(reposDir)}
1438
- Global directory: ${chalk7.cyan(globalDir)}`,
1951
+ `Name: ${chalk11.cyan(sanitizedName)}
1952
+ Thoughts repository: ${chalk11.cyan(thoughtsRepo)}
1953
+ Repos directory: ${chalk11.cyan(reposDir)}
1954
+ Global directory: ${chalk11.cyan(globalDir)}`,
1439
1955
  "Profile Configuration"
1440
1956
  );
1441
1957
  p2.outro(
1442
- chalk7.gray("Next steps:\n") + chalk7.gray(` 1. Run "thoughtcabinet init --profile ${sanitizedName}" in a repository
1443
- `) + chalk7.gray(` 2. Your thoughts will sync to the profile's repository`)
1958
+ chalk11.gray("Next steps:\n") + chalk11.gray(` 1. Run "thoughtcabinet init --profile ${sanitizedName}" in a repository
1959
+ `) + chalk11.gray(` 2. Your thoughts will sync to the profile's repository`)
1444
1960
  );
1445
1961
  } catch (error) {
1446
1962
  p2.log.error(`Error creating profile: ${error}`);
@@ -1449,35 +1965,35 @@ Global directory: ${chalk7.cyan(globalDir)}`,
1449
1965
  }
1450
1966
 
1451
1967
  // src/commands/thoughts/profile/list.ts
1452
- import chalk8 from "chalk";
1968
+ import chalk12 from "chalk";
1453
1969
  async function profileListCommand(options) {
1454
1970
  try {
1455
1971
  const config = loadThoughtsConfig(options);
1456
1972
  if (!config) {
1457
- console.error(chalk8.red("Error: Thoughts not configured."));
1973
+ console.error(chalk12.red("Error: Thoughts not configured."));
1458
1974
  process.exit(1);
1459
1975
  }
1460
1976
  if (options.json) {
1461
1977
  console.log(JSON.stringify(config.profiles || {}, null, 2));
1462
1978
  return;
1463
1979
  }
1464
- console.log(chalk8.blue("Thoughts Profiles"));
1465
- console.log(chalk8.gray("=".repeat(50)));
1980
+ console.log(chalk12.blue("Thoughts Profiles"));
1981
+ console.log(chalk12.gray("=".repeat(50)));
1466
1982
  console.log("");
1467
- console.log(chalk8.yellow("Default Configuration:"));
1468
- console.log(` Thoughts repository: ${chalk8.cyan(config.thoughtsRepo)}`);
1469
- console.log(` Repos directory: ${chalk8.cyan(config.reposDir)}`);
1470
- console.log(` Global directory: ${chalk8.cyan(config.globalDir)}`);
1983
+ console.log(chalk12.yellow("Default Configuration:"));
1984
+ console.log(` Thoughts repository: ${chalk12.cyan(config.thoughtsRepo)}`);
1985
+ console.log(` Repos directory: ${chalk12.cyan(config.reposDir)}`);
1986
+ console.log(` Global directory: ${chalk12.cyan(config.globalDir)}`);
1471
1987
  console.log("");
1472
1988
  if (!config.profiles || Object.keys(config.profiles).length === 0) {
1473
- console.log(chalk8.gray("No profiles configured."));
1989
+ console.log(chalk12.gray("No profiles configured."));
1474
1990
  console.log("");
1475
- console.log(chalk8.gray("Create a profile with: thoughtcabinet profile create <name>"));
1991
+ console.log(chalk12.gray("Create a profile with: thoughtcabinet profile create <name>"));
1476
1992
  } else {
1477
- console.log(chalk8.yellow(`Profiles (${Object.keys(config.profiles).length}):`));
1993
+ console.log(chalk12.yellow(`Profiles (${Object.keys(config.profiles).length}):`));
1478
1994
  console.log("");
1479
1995
  Object.entries(config.profiles).forEach(([name, profile]) => {
1480
- console.log(chalk8.cyan(` ${name}:`));
1996
+ console.log(chalk12.cyan(` ${name}:`));
1481
1997
  console.log(` Thoughts repository: ${profile.thoughtsRepo}`);
1482
1998
  console.log(` Repos directory: ${profile.reposDir}`);
1483
1999
  console.log(` Global directory: ${profile.globalDir}`);
@@ -1485,30 +2001,30 @@ async function profileListCommand(options) {
1485
2001
  });
1486
2002
  }
1487
2003
  } catch (error) {
1488
- console.error(chalk8.red(`Error listing profiles: ${error}`));
2004
+ console.error(chalk12.red(`Error listing profiles: ${error}`));
1489
2005
  process.exit(1);
1490
2006
  }
1491
2007
  }
1492
2008
 
1493
2009
  // src/commands/thoughts/profile/show.ts
1494
- import chalk9 from "chalk";
2010
+ import chalk13 from "chalk";
1495
2011
  async function profileShowCommand(profileName, options) {
1496
2012
  try {
1497
2013
  const config = loadThoughtsConfig(options);
1498
2014
  if (!config) {
1499
- console.error(chalk9.red("Error: Thoughts not configured."));
2015
+ console.error(chalk13.red("Error: Thoughts not configured."));
1500
2016
  process.exit(1);
1501
2017
  }
1502
2018
  if (!validateProfile(config, profileName)) {
1503
- console.error(chalk9.red(`Error: Profile "${profileName}" not found.`));
2019
+ console.error(chalk13.red(`Error: Profile "${profileName}" not found.`));
1504
2020
  console.error("");
1505
- console.error(chalk9.gray("Available profiles:"));
2021
+ console.error(chalk13.gray("Available profiles:"));
1506
2022
  if (config.profiles) {
1507
2023
  Object.keys(config.profiles).forEach((name) => {
1508
- console.error(chalk9.gray(` - ${name}`));
2024
+ console.error(chalk13.gray(` - ${name}`));
1509
2025
  });
1510
2026
  } else {
1511
- console.error(chalk9.gray(" (none)"));
2027
+ console.error(chalk13.gray(" (none)"));
1512
2028
  }
1513
2029
  process.exit(1);
1514
2030
  }
@@ -1517,13 +2033,13 @@ async function profileShowCommand(profileName, options) {
1517
2033
  console.log(JSON.stringify(profile, null, 2));
1518
2034
  return;
1519
2035
  }
1520
- console.log(chalk9.blue(`Profile: ${profileName}`));
1521
- console.log(chalk9.gray("=".repeat(50)));
2036
+ console.log(chalk13.blue(`Profile: ${profileName}`));
2037
+ console.log(chalk13.gray("=".repeat(50)));
1522
2038
  console.log("");
1523
- console.log(chalk9.yellow("Configuration:"));
1524
- console.log(` Thoughts repository: ${chalk9.cyan(profile.thoughtsRepo)}`);
1525
- console.log(` Repos directory: ${chalk9.cyan(profile.reposDir)}`);
1526
- console.log(` Global directory: ${chalk9.cyan(profile.globalDir)}`);
2039
+ console.log(chalk13.yellow("Configuration:"));
2040
+ console.log(` Thoughts repository: ${chalk13.cyan(profile.thoughtsRepo)}`);
2041
+ console.log(` Repos directory: ${chalk13.cyan(profile.reposDir)}`);
2042
+ console.log(` Global directory: ${chalk13.cyan(profile.globalDir)}`);
1527
2043
  console.log("");
1528
2044
  let repoCount = 0;
1529
2045
  Object.values(config.repoMappings).forEach((mapping) => {
@@ -1531,16 +2047,16 @@ async function profileShowCommand(profileName, options) {
1531
2047
  repoCount++;
1532
2048
  }
1533
2049
  });
1534
- console.log(chalk9.yellow("Usage:"));
1535
- console.log(` Repositories using this profile: ${chalk9.cyan(repoCount)}`);
2050
+ console.log(chalk13.yellow("Usage:"));
2051
+ console.log(` Repositories using this profile: ${chalk13.cyan(repoCount)}`);
1536
2052
  } catch (error) {
1537
- console.error(chalk9.red(`Error showing profile: ${error}`));
2053
+ console.error(chalk13.red(`Error showing profile: ${error}`));
1538
2054
  process.exit(1);
1539
2055
  }
1540
2056
  }
1541
2057
 
1542
2058
  // src/commands/thoughts/profile/delete.ts
1543
- import chalk10 from "chalk";
2059
+ import chalk14 from "chalk";
1544
2060
  import * as p3 from "@clack/prompts";
1545
2061
  async function profileDeleteCommand(profileName, options) {
1546
2062
  try {
@@ -1549,7 +2065,7 @@ async function profileDeleteCommand(profileName, options) {
1549
2065
  p3.log.info("Use --force flag to delete without confirmation.");
1550
2066
  process.exit(1);
1551
2067
  }
1552
- p3.intro(chalk10.blue(`Delete Profile: ${profileName}`));
2068
+ p3.intro(chalk14.blue(`Delete Profile: ${profileName}`));
1553
2069
  const config = loadThoughtsConfig(options);
1554
2070
  if (!config) {
1555
2071
  p3.log.error("Thoughts not configured.");
@@ -1568,19 +2084,19 @@ async function profileDeleteCommand(profileName, options) {
1568
2084
  if (usingRepos.length > 0 && !options.force) {
1569
2085
  p3.log.error(`Profile "${profileName}" is in use by ${usingRepos.length} repository(ies):`);
1570
2086
  usingRepos.forEach((repo) => {
1571
- p3.log.message(chalk10.gray(` - ${repo}`));
2087
+ p3.log.message(chalk14.gray(` - ${repo}`));
1572
2088
  });
1573
2089
  p3.log.warn("Options:");
1574
- p3.log.message(chalk10.gray(' 1. Run "thoughtcabinet destroy" in each repository'));
2090
+ p3.log.message(chalk14.gray(' 1. Run "thoughtcabinet destroy" in each repository'));
1575
2091
  p3.log.message(
1576
- chalk10.gray(" 2. Use --force to delete anyway (repos will fall back to default config)")
2092
+ chalk14.gray(" 2. Use --force to delete anyway (repos will fall back to default config)")
1577
2093
  );
1578
2094
  process.exit(1);
1579
2095
  }
1580
2096
  if (!options.force) {
1581
- p3.log.warn(`You are about to delete profile: ${chalk10.cyan(profileName)}`);
1582
- p3.log.message(chalk10.gray("This will remove the profile configuration."));
1583
- p3.log.message(chalk10.gray("The thoughts repository files will NOT be deleted."));
2097
+ p3.log.warn(`You are about to delete profile: ${chalk14.cyan(profileName)}`);
2098
+ p3.log.message(chalk14.gray("This will remove the profile configuration."));
2099
+ p3.log.message(chalk14.gray("The thoughts repository files will NOT be deleted."));
1584
2100
  const confirmDelete = await p3.confirm({
1585
2101
  message: `Delete profile "${profileName}"?`,
1586
2102
  initialValue: false
@@ -1599,7 +2115,7 @@ async function profileDeleteCommand(profileName, options) {
1599
2115
  if (usingRepos.length > 0) {
1600
2116
  p3.log.warn("Repositories using this profile will fall back to default config");
1601
2117
  }
1602
- p3.outro(chalk10.green("Done"));
2118
+ p3.outro(chalk14.green("Done"));
1603
2119
  } catch (error) {
1604
2120
  p3.log.error(`Error deleting profile: ${error}`);
1605
2121
  process.exit(1);
@@ -1617,6 +2133,7 @@ function thoughtsCommand(program2) {
1617
2133
  cmd.command("sync").description("Manually sync thoughts to thoughts repository").option("-m, --message <message>", "Commit message for sync").option("--config-file <path>", "Path to config file").action(thoughtsSyncCommand);
1618
2134
  cmd.command("status").description("Show status of thoughts repository").option("--config-file <path>", "Path to config file").action(thoughtsStatusCommand);
1619
2135
  cmd.command("config").description("View or edit thoughts configuration").option("--edit", "Open configuration in editor").option("--json", "Output configuration as JSON").option("--config-file <path>", "Path to config file").action(thoughtsConfigCommand);
2136
+ cmd.command("prune").description("Remove stale repository mappings (directories that no longer exist)").option("--apply", "Apply changes (default is dry-run)").option("--config-file <path>", "Path to config file").action(thoughtsPruneCommand);
1620
2137
  const profile = cmd.command("profile").description("Manage thoughts profiles");
1621
2138
  profile.command("create <name>").description("Create a new thoughts profile").option("--repo <path>", "Thoughts repository path").option("--repos-dir <name>", "Repos directory name").option("--global-dir <name>", "Global directory name").option("--config-file <path>", "Path to config file").action(profileCreateCommand);
1622
2139
  profile.command("list").description("List all thoughts profiles").option("--json", "Output as JSON").option("--config-file <path>", "Path to config file").action(profileListCommand);
@@ -1625,38 +2142,38 @@ function thoughtsCommand(program2) {
1625
2142
  }
1626
2143
 
1627
2144
  // src/commands/agent/init.ts
1628
- import fs8 from "fs";
1629
- import path10 from "path";
1630
- import chalk11 from "chalk";
2145
+ import fs12 from "fs";
2146
+ import path14 from "path";
2147
+ import chalk15 from "chalk";
1631
2148
  import * as p4 from "@clack/prompts";
1632
2149
  import { fileURLToPath } from "url";
1633
2150
  import { dirname } from "path";
1634
2151
  var __filename2 = fileURLToPath(import.meta.url);
1635
2152
  var __dirname2 = dirname(__filename2);
1636
2153
  function ensureGitignoreEntry(targetDir, entry, productName) {
1637
- const gitignorePath = path10.join(targetDir, ".gitignore");
2154
+ const gitignorePath = path14.join(targetDir, ".gitignore");
1638
2155
  let gitignoreContent = "";
1639
- if (fs8.existsSync(gitignorePath)) {
1640
- gitignoreContent = fs8.readFileSync(gitignorePath, "utf8");
2156
+ if (fs12.existsSync(gitignorePath)) {
2157
+ gitignoreContent = fs12.readFileSync(gitignorePath, "utf8");
1641
2158
  }
1642
2159
  const lines = gitignoreContent.split("\n");
1643
2160
  if (lines.some((line) => line.trim() === entry)) {
1644
2161
  return;
1645
2162
  }
1646
2163
  const newContent = gitignoreContent + (gitignoreContent && !gitignoreContent.endsWith("\n") ? "\n" : "") + "\n# " + productName + " local settings\n" + entry + "\n";
1647
- fs8.writeFileSync(gitignorePath, newContent);
2164
+ fs12.writeFileSync(gitignorePath, newContent);
1648
2165
  }
1649
2166
  function copyDirectoryRecursive(sourceDir, targetDir) {
1650
2167
  let filesCopied = 0;
1651
- fs8.mkdirSync(targetDir, { recursive: true });
1652
- const entries = fs8.readdirSync(sourceDir, { withFileTypes: true });
2168
+ fs12.mkdirSync(targetDir, { recursive: true });
2169
+ const entries = fs12.readdirSync(sourceDir, { withFileTypes: true });
1653
2170
  for (const entry of entries) {
1654
- const sourcePath = path10.join(sourceDir, entry.name);
1655
- const targetPath = path10.join(targetDir, entry.name);
2171
+ const sourcePath = path14.join(sourceDir, entry.name);
2172
+ const targetPath = path14.join(targetDir, entry.name);
1656
2173
  if (entry.isDirectory()) {
1657
2174
  filesCopied += copyDirectoryRecursive(sourcePath, targetPath);
1658
2175
  } else {
1659
- fs8.copyFileSync(sourcePath, targetPath);
2176
+ fs12.copyFileSync(sourcePath, targetPath);
1660
2177
  filesCopied++;
1661
2178
  }
1662
2179
  }
@@ -1665,23 +2182,23 @@ function copyDirectoryRecursive(sourceDir, targetDir) {
1665
2182
  async function agentInitCommand(options) {
1666
2183
  const { product } = options;
1667
2184
  try {
1668
- p4.intro(chalk11.blue(`Initialize ${product.name} Configuration`));
2185
+ p4.intro(chalk15.blue(`Initialize ${product.name} Configuration`));
1669
2186
  if (!process.stdin.isTTY && !options.all) {
1670
2187
  p4.log.error("Not running in interactive terminal.");
1671
2188
  p4.log.info("Use --all flag to copy all files without prompting.");
1672
2189
  process.exit(1);
1673
2190
  }
1674
2191
  const targetDir = process.cwd();
1675
- const agentTargetDir = path10.join(targetDir, product.dirName);
2192
+ const agentTargetDir = path14.join(targetDir, product.dirName);
1676
2193
  const possiblePaths = [
1677
2194
  // When installed via npm: package root is one level up from dist
1678
- path10.resolve(__dirname2, "..", product.sourceDirName),
2195
+ path14.resolve(__dirname2, "..", product.sourceDirName),
1679
2196
  // When running from repo: repo root is two levels up from dist
1680
- path10.resolve(__dirname2, "../..", product.sourceDirName)
2197
+ path14.resolve(__dirname2, "../..", product.sourceDirName)
1681
2198
  ];
1682
2199
  let sourceAgentDir = null;
1683
2200
  for (const candidatePath of possiblePaths) {
1684
- if (fs8.existsSync(candidatePath)) {
2201
+ if (fs12.existsSync(candidatePath)) {
1685
2202
  sourceAgentDir = candidatePath;
1686
2203
  break;
1687
2204
  }
@@ -1695,7 +2212,7 @@ async function agentInitCommand(options) {
1695
2212
  p4.log.info("Are you running from the thoughtcabinet repository or npm package?");
1696
2213
  process.exit(1);
1697
2214
  }
1698
- if (fs8.existsSync(agentTargetDir) && !options.force) {
2215
+ if (fs12.existsSync(agentTargetDir) && !options.force) {
1699
2216
  const overwrite = await p4.confirm({
1700
2217
  message: `${product.dirName} directory already exists. Overwrite?`,
1701
2218
  initialValue: false
@@ -1711,18 +2228,18 @@ async function agentInitCommand(options) {
1711
2228
  } else {
1712
2229
  let commandsCount = 0;
1713
2230
  let agentsCount = 0;
1714
- const commandsDir = path10.join(sourceAgentDir, "commands");
1715
- const agentsDir = path10.join(sourceAgentDir, "agents");
1716
- if (fs8.existsSync(commandsDir)) {
1717
- commandsCount = fs8.readdirSync(commandsDir).length;
2231
+ const commandsDir = path14.join(sourceAgentDir, "commands");
2232
+ const agentsDir = path14.join(sourceAgentDir, "agents");
2233
+ if (fs12.existsSync(commandsDir)) {
2234
+ commandsCount = fs12.readdirSync(commandsDir).length;
1718
2235
  }
1719
- if (fs8.existsSync(agentsDir)) {
1720
- agentsCount = fs8.readdirSync(agentsDir).length;
2236
+ if (fs12.existsSync(agentsDir)) {
2237
+ agentsCount = fs12.readdirSync(agentsDir).length;
1721
2238
  }
1722
2239
  let skillsCount = 0;
1723
- const skillsDir = path10.join(sourceAgentDir, "skills");
1724
- if (fs8.existsSync(skillsDir)) {
1725
- skillsCount = fs8.readdirSync(skillsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).length;
2240
+ const skillsDir = path14.join(sourceAgentDir, "skills");
2241
+ if (fs12.existsSync(skillsDir)) {
2242
+ skillsCount = fs12.readdirSync(skillsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).length;
1726
2243
  }
1727
2244
  p4.note(
1728
2245
  "Use \u2191/\u2193 to move, press Space to select/deselect, press A to select/deselect all, press Enter to confirm. (Subsequent multi-selects apply; Ctrl+C to exit)",
@@ -1765,15 +2282,15 @@ async function agentInitCommand(options) {
1765
2282
  process.exit(0);
1766
2283
  }
1767
2284
  }
1768
- fs8.mkdirSync(agentTargetDir, { recursive: true });
2285
+ fs12.mkdirSync(agentTargetDir, { recursive: true });
1769
2286
  let filesCopied = 0;
1770
2287
  let filesSkipped = 0;
1771
2288
  const filesToCopyByCategory = {};
1772
2289
  if (!options.all) {
1773
2290
  if (selectedCategories.includes("commands")) {
1774
- const sourceDir = path10.join(sourceAgentDir, "commands");
1775
- if (fs8.existsSync(sourceDir)) {
1776
- const allFiles = fs8.readdirSync(sourceDir);
2291
+ const sourceDir = path14.join(sourceAgentDir, "commands");
2292
+ if (fs12.existsSync(sourceDir)) {
2293
+ const allFiles = fs12.readdirSync(sourceDir);
1777
2294
  const fileSelection = await p4.multiselect({
1778
2295
  message: "Select command files to copy:",
1779
2296
  options: allFiles.map((file) => ({
@@ -1794,9 +2311,9 @@ async function agentInitCommand(options) {
1794
2311
  }
1795
2312
  }
1796
2313
  if (selectedCategories.includes("agents")) {
1797
- const sourceDir = path10.join(sourceAgentDir, "agents");
1798
- if (fs8.existsSync(sourceDir)) {
1799
- const allFiles = fs8.readdirSync(sourceDir);
2314
+ const sourceDir = path14.join(sourceAgentDir, "agents");
2315
+ if (fs12.existsSync(sourceDir)) {
2316
+ const allFiles = fs12.readdirSync(sourceDir);
1800
2317
  const fileSelection = await p4.multiselect({
1801
2318
  message: "Select agent files to copy:",
1802
2319
  options: allFiles.map((file) => ({
@@ -1817,9 +2334,9 @@ async function agentInitCommand(options) {
1817
2334
  }
1818
2335
  }
1819
2336
  if (selectedCategories.includes("skills")) {
1820
- const sourceDir = path10.join(sourceAgentDir, "skills");
1821
- if (fs8.existsSync(sourceDir)) {
1822
- const allSkills = fs8.readdirSync(sourceDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
2337
+ const sourceDir = path14.join(sourceAgentDir, "skills");
2338
+ if (fs12.existsSync(sourceDir)) {
2339
+ const allSkills = fs12.readdirSync(sourceDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
1823
2340
  if (allSkills.length > 0) {
1824
2341
  const skillSelection = await p4.multiselect({
1825
2342
  message: "Select skills to copy:",
@@ -1866,13 +2383,13 @@ async function agentInitCommand(options) {
1866
2383
  }
1867
2384
  for (const category of selectedCategories) {
1868
2385
  if (category === "commands" || category === "agents") {
1869
- const sourceDir = path10.join(sourceAgentDir, category);
1870
- const targetCategoryDir = path10.join(agentTargetDir, category);
1871
- if (!fs8.existsSync(sourceDir)) {
2386
+ const sourceDir = path14.join(sourceAgentDir, category);
2387
+ const targetCategoryDir = path14.join(agentTargetDir, category);
2388
+ if (!fs12.existsSync(sourceDir)) {
1872
2389
  p4.log.warn(`${category} directory not found in source, skipping`);
1873
2390
  continue;
1874
2391
  }
1875
- const allFiles = fs8.readdirSync(sourceDir);
2392
+ const allFiles = fs12.readdirSync(sourceDir);
1876
2393
  let filesToCopy = allFiles;
1877
2394
  if (!options.all && filesToCopyByCategory[category]) {
1878
2395
  filesToCopy = filesToCopyByCategory[category];
@@ -1880,20 +2397,20 @@ async function agentInitCommand(options) {
1880
2397
  if (filesToCopy.length === 0) {
1881
2398
  continue;
1882
2399
  }
1883
- fs8.mkdirSync(targetCategoryDir, { recursive: true });
2400
+ fs12.mkdirSync(targetCategoryDir, { recursive: true });
1884
2401
  for (const file of filesToCopy) {
1885
- const sourcePath = path10.join(sourceDir, file);
1886
- const targetPath = path10.join(targetCategoryDir, file);
1887
- fs8.copyFileSync(sourcePath, targetPath);
2402
+ const sourcePath = path14.join(sourceDir, file);
2403
+ const targetPath = path14.join(targetCategoryDir, file);
2404
+ fs12.copyFileSync(sourcePath, targetPath);
1888
2405
  filesCopied++;
1889
2406
  }
1890
2407
  filesSkipped += allFiles.length - filesToCopy.length;
1891
2408
  p4.log.success(`Copied ${filesToCopy.length} ${category} file(s)`);
1892
2409
  } else if (category === "settings") {
1893
- const settingsPath = path10.join(sourceAgentDir, "settings.template.json");
1894
- const targetSettingsPath = path10.join(agentTargetDir, "settings.json");
1895
- if (fs8.existsSync(settingsPath)) {
1896
- const settingsContent = fs8.readFileSync(settingsPath, "utf8");
2410
+ const settingsPath = path14.join(sourceAgentDir, "settings.template.json");
2411
+ const targetSettingsPath = path14.join(agentTargetDir, "settings.json");
2412
+ if (fs12.existsSync(settingsPath)) {
2413
+ const settingsContent = fs12.readFileSync(settingsPath, "utf8");
1897
2414
  const settings = JSON.parse(settingsContent);
1898
2415
  if (maxThinkingTokens !== void 0) {
1899
2416
  if (!settings.env) {
@@ -1907,20 +2424,20 @@ async function agentInitCommand(options) {
1907
2424
  for (const [key, value] of Object.entries(product.defaultEnvVars)) {
1908
2425
  settings.env[key] = value;
1909
2426
  }
1910
- fs8.writeFileSync(targetSettingsPath, JSON.stringify(settings, null, 2) + "\n");
2427
+ fs12.writeFileSync(targetSettingsPath, JSON.stringify(settings, null, 2) + "\n");
1911
2428
  filesCopied++;
1912
2429
  p4.log.success(`Copied settings.json (maxTokens: ${maxThinkingTokens})`);
1913
2430
  } else {
1914
2431
  p4.log.warn("settings.json not found in source, skipping");
1915
2432
  }
1916
2433
  } else if (category === "skills") {
1917
- const sourceDir = path10.join(sourceAgentDir, "skills");
1918
- const targetCategoryDir = path10.join(agentTargetDir, "skills");
1919
- if (!fs8.existsSync(sourceDir)) {
2434
+ const sourceDir = path14.join(sourceAgentDir, "skills");
2435
+ const targetCategoryDir = path14.join(agentTargetDir, "skills");
2436
+ if (!fs12.existsSync(sourceDir)) {
1920
2437
  p4.log.warn("skills directory not found in source, skipping");
1921
2438
  continue;
1922
2439
  }
1923
- const allSkills = fs8.readdirSync(sourceDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
2440
+ const allSkills = fs12.readdirSync(sourceDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
1924
2441
  let skillsToCopy = allSkills;
1925
2442
  if (!options.all && filesToCopyByCategory["skills"]) {
1926
2443
  skillsToCopy = filesToCopyByCategory["skills"];
@@ -1928,11 +2445,11 @@ async function agentInitCommand(options) {
1928
2445
  if (skillsToCopy.length === 0) {
1929
2446
  continue;
1930
2447
  }
1931
- fs8.mkdirSync(targetCategoryDir, { recursive: true });
2448
+ fs12.mkdirSync(targetCategoryDir, { recursive: true });
1932
2449
  let skillFilesCopied = 0;
1933
2450
  for (const skill of skillsToCopy) {
1934
- const sourceSkillPath = path10.join(sourceDir, skill);
1935
- const targetSkillPath = path10.join(targetCategoryDir, skill);
2451
+ const sourceSkillPath = path14.join(sourceDir, skill);
2452
+ const targetSkillPath = path14.join(targetCategoryDir, skill);
1936
2453
  skillFilesCopied += copyDirectoryRecursive(sourceSkillPath, targetSkillPath);
1937
2454
  }
1938
2455
  filesCopied += skillFilesCopied;
@@ -1946,10 +2463,10 @@ async function agentInitCommand(options) {
1946
2463
  }
1947
2464
  let message = `Successfully copied ${filesCopied} file(s) to ${agentTargetDir}`;
1948
2465
  if (filesSkipped > 0) {
1949
- message += chalk11.gray(`
2466
+ message += chalk15.gray(`
1950
2467
  Skipped ${filesSkipped} file(s)`);
1951
2468
  }
1952
- message += chalk11.gray(`
2469
+ message += chalk15.gray(`
1953
2470
  You can now use these commands in ${product.name}.`);
1954
2471
  p4.outro(message);
1955
2472
  } catch (error) {
@@ -2009,27 +2526,27 @@ function agentCommand(program2) {
2009
2526
  }
2010
2527
 
2011
2528
  // src/commands/metadata/metadata.ts
2012
- import { execSync as execSync6 } from "child_process";
2529
+ import { execSync as execSync7 } from "child_process";
2013
2530
  function getGitInfo() {
2014
2531
  try {
2015
- execSync6("git rev-parse --is-inside-work-tree", {
2532
+ execSync7("git rev-parse --is-inside-work-tree", {
2016
2533
  encoding: "utf8",
2017
2534
  stdio: "pipe"
2018
2535
  });
2019
- const repoRoot = execSync6("git rev-parse --show-toplevel", {
2536
+ const repoRoot = execSync7("git rev-parse --show-toplevel", {
2020
2537
  encoding: "utf8",
2021
2538
  stdio: "pipe"
2022
2539
  }).trim();
2023
2540
  const repoName = repoRoot.split("/").pop() || "";
2024
2541
  let branch = "";
2025
2542
  try {
2026
- branch = execSync6("git branch --show-current", {
2543
+ branch = execSync7("git branch --show-current", {
2027
2544
  encoding: "utf8",
2028
2545
  stdio: "pipe"
2029
2546
  }).trim();
2030
2547
  } catch {
2031
2548
  try {
2032
- branch = execSync6("git rev-parse --abbrev-ref HEAD", {
2549
+ branch = execSync7("git rev-parse --abbrev-ref HEAD", {
2033
2550
  encoding: "utf8",
2034
2551
  stdio: "pipe"
2035
2552
  }).trim();
@@ -2039,7 +2556,7 @@ function getGitInfo() {
2039
2556
  }
2040
2557
  let commit = "";
2041
2558
  try {
2042
- commit = execSync6("git rev-parse HEAD", {
2559
+ commit = execSync7("git rev-parse HEAD", {
2043
2560
  encoding: "utf8",
2044
2561
  stdio: "pipe"
2045
2562
  }).trim();
@@ -2095,6 +2612,460 @@ function metadataCommand(program2) {
2095
2612
  program2.command("metadata").description("Output metadata for current repository (branch, commit, timestamp, etc.)").action(specMetadataCommand);
2096
2613
  }
2097
2614
 
2615
+ // src/commands/worktree.ts
2616
+ import fs14 from "fs";
2617
+ import path16 from "path";
2618
+ import chalk16 from "chalk";
2619
+
2620
+ // src/tmux.ts
2621
+ import { execFileSync as execFileSync3 } from "child_process";
2622
+ function sessionNameForHandle(handle) {
2623
+ return `thc-${handle}`;
2624
+ }
2625
+ function legacySessionNameForHandle(handle) {
2626
+ return `thc:${handle}`;
2627
+ }
2628
+ function allSessionNamesForHandle(handle) {
2629
+ return [sessionNameForHandle(handle), legacySessionNameForHandle(handle)];
2630
+ }
2631
+ function listTmuxSessions() {
2632
+ try {
2633
+ const out = execFileSync3("tmux", ["list-sessions", "-F", "#{session_name}"], {
2634
+ encoding: "utf8",
2635
+ stdio: ["ignore", "pipe", "pipe"]
2636
+ }).trim();
2637
+ return out.split("\n").map((s) => s.trim()).filter(Boolean);
2638
+ } catch {
2639
+ return [];
2640
+ }
2641
+ }
2642
+ function tmuxHasSession(sessionName) {
2643
+ try {
2644
+ execFileSync3("tmux", ["has-session", "-t", sessionName], {
2645
+ stdio: "ignore"
2646
+ });
2647
+ return true;
2648
+ } catch {
2649
+ return false;
2650
+ }
2651
+ }
2652
+ function tmuxNewSession(sessionName, cwd) {
2653
+ execFileSync3("tmux", ["new-session", "-d", "-s", sessionName, "-c", cwd], {
2654
+ stdio: "inherit"
2655
+ });
2656
+ }
2657
+ function tmuxKillSession(sessionName) {
2658
+ try {
2659
+ execFileSync3("tmux", ["kill-session", "-t", sessionName], { stdio: "ignore" });
2660
+ } catch {
2661
+ }
2662
+ }
2663
+
2664
+ // src/agent-config.ts
2665
+ import fs13 from "fs";
2666
+ import path15 from "path";
2667
+ var AGENT_CONFIG_DIRS = [".claude", ".codebuddy"];
2668
+ function copyAgentConfigDirs(options) {
2669
+ const { sourceDir, targetDir, configDirs = AGENT_CONFIG_DIRS } = options;
2670
+ const copied = [];
2671
+ const skipped = [];
2672
+ for (const dirName of configDirs) {
2673
+ const sourcePath = path15.join(sourceDir, dirName);
2674
+ const targetPath = path15.join(targetDir, dirName);
2675
+ if (!fs13.existsSync(sourcePath)) {
2676
+ skipped.push(dirName);
2677
+ continue;
2678
+ }
2679
+ fs13.cpSync(sourcePath, targetPath, { recursive: true });
2680
+ copied.push(dirName);
2681
+ }
2682
+ return { copied, skipped };
2683
+ }
2684
+
2685
+ // src/commands/worktree.ts
2686
+ function worktreeCommand(program2) {
2687
+ const wt = program2.command("worktree").description("Manage git worktrees bound to tmux sessions");
2688
+ wt.command("add <name>").description("Create a git worktree and a tmux session for it").option("--branch <branch>", "Branch name (defaults to <name>)").option("--base <ref>", "Base ref/commit (default: HEAD)", "HEAD").option("--path <path>", "Worktree directory path (default: ../<repo>__worktrees/<name>)").option("--detached", "Create a detached worktree at <base> (no branch)").option("--no-thoughts", "Skip thoughts initialization").action(async (name, options) => {
2689
+ try {
2690
+ validateWorktreeHandle(name);
2691
+ if (!isGitRepo()) {
2692
+ console.error(chalk16.red("Error: not in a git repository"));
2693
+ process.exit(1);
2694
+ }
2695
+ const mainRoot = getMainWorktreeRoot();
2696
+ const baseDir = getWorktreesBaseDir(mainRoot);
2697
+ const worktreePath = options.path ? path16.resolve(options.path) : path16.join(baseDir, name);
2698
+ fs14.mkdirSync(path16.dirname(worktreePath), { recursive: true });
2699
+ const sessionName = sessionNameForHandle(name);
2700
+ const sessionCandidates = allSessionNamesForHandle(name);
2701
+ const existing = sessionCandidates.find((s) => tmuxHasSession(s));
2702
+ if (existing) {
2703
+ console.error(chalk16.red(`Error: tmux session already exists: ${existing}`));
2704
+ process.exit(1);
2705
+ }
2706
+ const hooksConfig = loadHooksConfig(mainRoot);
2707
+ const preAddHooks = getHooksForEvent(hooksConfig, "PreWorktreeAdd");
2708
+ if (preAddHooks.length > 0) {
2709
+ const hookInput = {
2710
+ hook_event_name: "PreWorktreeAdd",
2711
+ cwd: mainRoot,
2712
+ worktree_path: worktreePath,
2713
+ worktree_name: name,
2714
+ worktree_branch: options.detached ? "" : options.branch ?? name,
2715
+ main_root: mainRoot,
2716
+ session_name: sessionName,
2717
+ base_ref: options.base
2718
+ };
2719
+ const hookEnv = {
2720
+ THC_WORKTREE_PATH: worktreePath,
2721
+ THC_WORKTREE_NAME: name,
2722
+ THC_WORKTREE_BRANCH: hookInput.worktree_branch,
2723
+ THC_MAIN_ROOT: mainRoot,
2724
+ THC_SESSION_NAME: sessionName,
2725
+ THC_BASE_REF: options.base
2726
+ };
2727
+ await executeHooks(preAddHooks, hookInput, hookEnv, true);
2728
+ }
2729
+ if (options.detached) {
2730
+ runGitCommandOrThrow(["worktree", "add", "--detach", worktreePath, options.base], {
2731
+ cwd: mainRoot
2732
+ });
2733
+ } else {
2734
+ const branch = options.branch ?? name;
2735
+ runGitCommandOrThrow(["worktree", "add", "-b", branch, worktreePath, options.base], {
2736
+ cwd: mainRoot
2737
+ });
2738
+ setBranchBase(branch, options.base, worktreePath);
2739
+ }
2740
+ tmuxNewSession(sessionName, worktreePath);
2741
+ const configResult = copyAgentConfigDirs({
2742
+ sourceDir: mainRoot,
2743
+ targetDir: worktreePath
2744
+ });
2745
+ if (configResult.copied.length > 0) {
2746
+ console.log(chalk16.gray(`Copied config: ${configResult.copied.join(", ")}`));
2747
+ }
2748
+ if (options.thoughts !== false) {
2749
+ const config = loadThoughtsConfig({});
2750
+ if (config) {
2751
+ const mainRepoMapping = config.repoMappings[mainRoot];
2752
+ const mappedName = getRepoNameFromMapping(mainRepoMapping);
2753
+ if (mappedName) {
2754
+ config.repoMappings[worktreePath] = mainRepoMapping;
2755
+ saveThoughtsConfig(config, {});
2756
+ const profileConfig = resolveProfileForRepo(config, worktreePath);
2757
+ createThoughtsDirectoryStructure(profileConfig, mappedName, config.user);
2758
+ const result = setupThoughtsDirectory({
2759
+ repoPath: worktreePath,
2760
+ profileConfig,
2761
+ mappedName,
2762
+ user: config.user,
2763
+ createSearchable: true,
2764
+ setupHooks: true
2765
+ });
2766
+ console.log(chalk16.gray("Thoughts initialized"));
2767
+ if (result.hooksUpdated.length > 0) {
2768
+ console.log(chalk16.gray(`Updated git hooks: ${result.hooksUpdated.join(", ")}`));
2769
+ }
2770
+ if (pullThoughtsFromRemote(profileConfig.thoughtsRepo)) {
2771
+ console.log(chalk16.gray("Pulled latest thoughts from remote"));
2772
+ }
2773
+ } else {
2774
+ console.log(chalk16.yellow("Main repo not configured for thoughts, skipping"));
2775
+ }
2776
+ } else {
2777
+ console.log(chalk16.yellow("Thoughts not configured globally, skipping"));
2778
+ }
2779
+ }
2780
+ const postAddHooks = getHooksForEvent(hooksConfig, "PostWorktreeAdd");
2781
+ if (postAddHooks.length > 0) {
2782
+ const hookInput = {
2783
+ hook_event_name: "PostWorktreeAdd",
2784
+ cwd: worktreePath,
2785
+ worktree_path: worktreePath,
2786
+ worktree_name: name,
2787
+ worktree_branch: options.detached ? "" : options.branch ?? name,
2788
+ main_root: mainRoot,
2789
+ session_name: sessionName
2790
+ };
2791
+ const hookEnv = {
2792
+ THC_WORKTREE_PATH: worktreePath,
2793
+ THC_WORKTREE_NAME: name,
2794
+ THC_WORKTREE_BRANCH: hookInput.worktree_branch,
2795
+ THC_MAIN_ROOT: mainRoot,
2796
+ THC_SESSION_NAME: sessionName
2797
+ };
2798
+ await executeHooks(postAddHooks, hookInput, hookEnv, true);
2799
+ }
2800
+ console.log(chalk16.green("\n\u2713 Worktree created"));
2801
+ console.log(chalk16.gray(`Path: ${worktreePath}`));
2802
+ console.log(chalk16.gray(`Tmux session: ${sessionName}`));
2803
+ console.log(chalk16.gray(`Attach: tmux attach -t ${sessionName}`));
2804
+ } catch (error) {
2805
+ console.error(chalk16.red(`Error: ${error.message}`));
2806
+ process.exit(1);
2807
+ }
2808
+ });
2809
+ wt.command("list").description("List thc-managed worktrees and their tmux sessions").option("--all", "Show all git worktrees (not just ../<repo>__worktrees)").action(async (options) => {
2810
+ try {
2811
+ if (!isGitRepo()) {
2812
+ console.error(chalk16.red("Error: not in a git repository"));
2813
+ process.exit(1);
2814
+ }
2815
+ const mainRoot = getMainWorktreeRoot();
2816
+ const baseDir = getWorktreesBaseDir(mainRoot);
2817
+ const baseDirResolved = path16.resolve(baseDir);
2818
+ const entries = parseWorktreeListPorcelain(
2819
+ runGitCommand(["worktree", "list", "--porcelain"], { cwd: mainRoot })
2820
+ );
2821
+ const sessions = new Set(listTmuxSessions());
2822
+ const filtered = options.all ? entries : entries.filter((e) => {
2823
+ const p5 = path16.resolve(e.worktreePath);
2824
+ return p5 === path16.resolve(mainRoot) || p5.startsWith(baseDirResolved + path16.sep);
2825
+ });
2826
+ if (filtered.length === 0) {
2827
+ console.log(chalk16.gray("No worktrees found."));
2828
+ return;
2829
+ }
2830
+ const cwd = process.cwd();
2831
+ const rows = filtered.map((e) => {
2832
+ const name = path16.basename(e.worktreePath);
2833
+ const sessionName = allSessionNamesForHandle(name).find((s) => sessions.has(s)) ?? "-";
2834
+ const isCurrent = path16.resolve(e.worktreePath) === path16.resolve(cwd);
2835
+ return {
2836
+ name: isCurrent ? `* ${name}` : ` ${name}`,
2837
+ branch: e.branch,
2838
+ tmux: sessionName,
2839
+ path: e.worktreePath,
2840
+ isCurrent
2841
+ };
2842
+ });
2843
+ const colWidths = {
2844
+ name: Math.max("NAME".length + 2, ...rows.map((r) => r.name.length)),
2845
+ branch: Math.max("BRANCH".length, ...rows.map((r) => r.branch.length)),
2846
+ tmux: Math.max("TMUX".length, ...rows.map((r) => r.tmux.length))
2847
+ };
2848
+ console.log(
2849
+ chalk16.blue(
2850
+ `${" NAME".padEnd(colWidths.name)} ${"BRANCH".padEnd(colWidths.branch)} ${"TMUX".padEnd(colWidths.tmux)} PATH`
2851
+ )
2852
+ );
2853
+ for (const row of rows) {
2854
+ const line = `${row.name.padEnd(colWidths.name)} ${row.branch.padEnd(colWidths.branch)} ${row.tmux.padEnd(colWidths.tmux)} ${row.path}`;
2855
+ console.log(row.isCurrent ? chalk16.green(line) : line);
2856
+ }
2857
+ } catch (error) {
2858
+ console.error(chalk16.red(`Error: ${error.message}`));
2859
+ process.exit(1);
2860
+ }
2861
+ });
2862
+ wt.command("merge <name>").description(
2863
+ "Rebase worktree branch onto target, ff-merge, then clean up worktree + tmux session"
2864
+ ).option(
2865
+ "--into <branch>",
2866
+ "Target branch to merge into (default: current branch in main worktree)"
2867
+ ).option("--force", "Force cleanup even if uncommitted changes exist").option("--keep-session", "Do not kill the tmux session").option("--keep-worktree", "Do not remove the git worktree").option("--keep-branch", "Do not delete the source branch").action(async (name, options) => {
2868
+ try {
2869
+ if (!isGitRepo()) {
2870
+ console.error(chalk16.red("Error: not in a git repository"));
2871
+ process.exit(1);
2872
+ }
2873
+ const mainRoot = getMainWorktreeRoot();
2874
+ const mainRootResolved = path16.resolve(mainRoot);
2875
+ const wtEntry = findWorktree(name, mainRoot);
2876
+ const wtResolved = path16.resolve(wtEntry.worktreePath);
2877
+ if (wtResolved === mainRootResolved) {
2878
+ console.error(chalk16.red("Error: refusing to merge/remove the main worktree"));
2879
+ process.exit(1);
2880
+ }
2881
+ if (wtEntry.detached || wtEntry.branch === "(detached)") {
2882
+ console.error(chalk16.red("Error: cannot merge a detached worktree"));
2883
+ process.exit(1);
2884
+ }
2885
+ const targetBranch = options.into ?? runGitCommand(["branch", "--show-current"], { cwd: mainRoot });
2886
+ if (!targetBranch) {
2887
+ console.error(chalk16.red("Error: could not determine target branch. Use --into <branch>."));
2888
+ process.exit(1);
2889
+ }
2890
+ if (targetBranch === wtEntry.branch) {
2891
+ console.error(chalk16.red("Error: source and target branch are the same"));
2892
+ process.exit(1);
2893
+ }
2894
+ const mergeHooksConfig = loadHooksConfig(mainRoot);
2895
+ const preMergeHooks = getHooksForEvent(mergeHooksConfig, "PreWorktreeMerge");
2896
+ if (preMergeHooks.length > 0) {
2897
+ const hookInput = {
2898
+ hook_event_name: "PreWorktreeMerge",
2899
+ cwd: mainRoot,
2900
+ worktree_path: wtResolved,
2901
+ worktree_name: name,
2902
+ worktree_branch: wtEntry.branch,
2903
+ target_branch: targetBranch,
2904
+ main_root: mainRoot
2905
+ };
2906
+ const hookEnv = {
2907
+ THC_WORKTREE_PATH: wtResolved,
2908
+ THC_WORKTREE_NAME: name,
2909
+ THC_WORKTREE_BRANCH: wtEntry.branch,
2910
+ THC_TARGET_BRANCH: targetBranch,
2911
+ THC_MAIN_ROOT: mainRoot
2912
+ };
2913
+ await executeHooks(preMergeHooks, hookInput, hookEnv, true);
2914
+ }
2915
+ const config = loadThoughtsConfig({});
2916
+ if (config && config.repoMappings[wtResolved]) {
2917
+ try {
2918
+ console.log(chalk16.gray("Cleaning up thoughts directory..."));
2919
+ const result = cleanupThoughtsDirectory({
2920
+ repoPath: wtResolved,
2921
+ config,
2922
+ force: options.force,
2923
+ verbose: false
2924
+ // Suppress detailed output during merge
2925
+ });
2926
+ if (result.configRemoved) {
2927
+ saveThoughtsConfig(config, {});
2928
+ }
2929
+ if (result.thoughtsRemoved) {
2930
+ console.log(chalk16.gray("\u2713 Thoughts directory cleaned up"));
2931
+ }
2932
+ } catch (error) {
2933
+ console.log(
2934
+ chalk16.yellow(`Warning: Could not clean up thoughts: ${error.message}`)
2935
+ );
2936
+ }
2937
+ }
2938
+ if (!options.force && hasUncommittedChanges(wtEntry.worktreePath)) {
2939
+ console.error(
2940
+ chalk16.red(
2941
+ "Error: worktree has uncommitted changes. Commit/stash first or use --force."
2942
+ )
2943
+ );
2944
+ process.exit(1);
2945
+ }
2946
+ console.log(chalk16.blue(`Rebasing ${wtEntry.branch} onto ${targetBranch}...`));
2947
+ runGitCommandOrThrow(["rebase", targetBranch], { cwd: wtEntry.worktreePath });
2948
+ console.log(chalk16.blue(`Fast-forward merging into ${targetBranch}...`));
2949
+ runGitCommandOrThrow(["switch", targetBranch], { cwd: mainRoot });
2950
+ runGitCommandOrThrow(["merge", "--ff-only", wtEntry.branch], { cwd: mainRoot });
2951
+ const handle = path16.basename(wtEntry.worktreePath);
2952
+ const sessionNames = allSessionNamesForHandle(handle);
2953
+ if (!options.keepSession) {
2954
+ for (const s of sessionNames) {
2955
+ tmuxKillSession(s);
2956
+ }
2957
+ }
2958
+ if (!options.keepWorktree) {
2959
+ const removeArgs = ["worktree", "remove"];
2960
+ if (options.force) {
2961
+ removeArgs.push("--force");
2962
+ }
2963
+ removeArgs.push(wtEntry.worktreePath);
2964
+ runGitCommandOrThrow(removeArgs, { cwd: mainRoot });
2965
+ try {
2966
+ runGitCommandOrThrow(["worktree", "prune"], { cwd: mainRoot });
2967
+ } catch {
2968
+ }
2969
+ }
2970
+ if (!options.keepBranch) {
2971
+ try {
2972
+ runGitCommandOrThrow(["branch", "-d", wtEntry.branch], { cwd: mainRoot });
2973
+ } catch {
2974
+ if (options.force) {
2975
+ runGitCommandOrThrow(["branch", "-D", wtEntry.branch], { cwd: mainRoot });
2976
+ } else {
2977
+ throw new Error(
2978
+ `Failed to delete branch '${wtEntry.branch}'. Re-run with --force to delete it.`
2979
+ );
2980
+ }
2981
+ }
2982
+ }
2983
+ const postMergeHooks = getHooksForEvent(mergeHooksConfig, "PostWorktreeMerge");
2984
+ if (postMergeHooks.length > 0) {
2985
+ const hookInput = {
2986
+ hook_event_name: "PostWorktreeMerge",
2987
+ cwd: mainRoot,
2988
+ worktree_path: wtResolved,
2989
+ worktree_name: name,
2990
+ worktree_branch: wtEntry.branch,
2991
+ target_branch: targetBranch,
2992
+ main_root: mainRoot,
2993
+ kept_session: options.keepSession || false,
2994
+ kept_worktree: options.keepWorktree || false,
2995
+ kept_branch: options.keepBranch || false
2996
+ };
2997
+ const hookEnv = {
2998
+ THC_WORKTREE_PATH: wtResolved,
2999
+ THC_WORKTREE_NAME: name,
3000
+ THC_WORKTREE_BRANCH: wtEntry.branch,
3001
+ THC_TARGET_BRANCH: targetBranch,
3002
+ THC_MAIN_ROOT: mainRoot,
3003
+ THC_KEPT_SESSION: options.keepSession ? "true" : "false",
3004
+ THC_KEPT_WORKTREE: options.keepWorktree ? "true" : "false",
3005
+ THC_KEPT_BRANCH: options.keepBranch ? "true" : "false"
3006
+ };
3007
+ await executeHooks(postMergeHooks, hookInput, hookEnv, true);
3008
+ }
3009
+ console.log(chalk16.green("\u2713 Merged and cleaned up"));
3010
+ } catch (error) {
3011
+ console.error(chalk16.red(`Error: ${error.message}`));
3012
+ process.exit(1);
3013
+ }
3014
+ });
3015
+ }
3016
+
3017
+ // src/commands/hooks/init.ts
3018
+ import fs15 from "fs";
3019
+ import path17 from "path";
3020
+ import chalk17 from "chalk";
3021
+ import { fileURLToPath as fileURLToPath2 } from "url";
3022
+ import { dirname as dirname2 } from "path";
3023
+ var __filename3 = fileURLToPath2(import.meta.url);
3024
+ var __dirname3 = dirname2(__filename3);
3025
+ async function hooksInitCommand() {
3026
+ try {
3027
+ const repoPath = process.cwd();
3028
+ const configDir = path17.join(repoPath, HOOKS_CONFIG_DIR);
3029
+ const configPath = path17.join(repoPath, HOOKS_CONFIG_FILE);
3030
+ if (fs15.existsSync(configPath)) {
3031
+ console.log(chalk17.yellow(`${HOOKS_CONFIG_FILE} already exists.`));
3032
+ console.log(chalk17.gray(`Edit the file directly: ${configPath}`));
3033
+ return;
3034
+ }
3035
+ const possiblePaths = [
3036
+ // When running from built dist: one level up from dist/
3037
+ path17.resolve(__dirname3, "..", ".thought-cabinet/hooks.example.json"),
3038
+ // When installed via npm: one level up from dist/
3039
+ path17.resolve(__dirname3, "../..", ".thought-cabinet/hooks.example.json")
3040
+ ];
3041
+ let examplePath = null;
3042
+ for (const candidatePath of possiblePaths) {
3043
+ if (fs15.existsSync(candidatePath)) {
3044
+ examplePath = candidatePath;
3045
+ break;
3046
+ }
3047
+ }
3048
+ if (!examplePath) {
3049
+ console.error(chalk17.red("Error: hooks.example.json not found in expected locations"));
3050
+ console.log(chalk17.gray("Searched paths:"));
3051
+ possiblePaths.forEach((p5) => console.log(chalk17.gray(` - ${p5}`)));
3052
+ process.exit(1);
3053
+ }
3054
+ fs15.mkdirSync(configDir, { recursive: true });
3055
+ fs15.copyFileSync(examplePath, configPath);
3056
+ console.log(chalk17.green(`Created ${HOOKS_CONFIG_FILE}`));
3057
+ } catch (error) {
3058
+ console.error(chalk17.red(`Error during hooks init: ${error}`));
3059
+ process.exit(1);
3060
+ }
3061
+ }
3062
+
3063
+ // src/commands/hooks.ts
3064
+ function hooksCommand(program2) {
3065
+ const hooks = program2.command("hooks").description("Manage hook configuration");
3066
+ hooks.command("init").description("Initialize hooks configuration in current repository").action(hooksInitCommand);
3067
+ }
3068
+
2098
3069
  // src/index.ts
2099
3070
  import dotenv2 from "dotenv";
2100
3071
  import { createRequire } from "module";
@@ -2108,5 +3079,7 @@ program.name("thoughtcabinet").description(
2108
3079
  thoughtsCommand(program);
2109
3080
  agentCommand(program);
2110
3081
  metadataCommand(program);
3082
+ worktreeCommand(program);
3083
+ hooksCommand(program);
2111
3084
  program.parse(process.argv);
2112
3085
  //# sourceMappingURL=index.js.map