clank-cli 0.1.52 → 0.1.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  Store AI agent files and notes in a separate git repository. Git-ignored symlinks make the files visible in your projects.
4
4
 
5
+ Common commands:
5
6
  - **`clank add`** to move files to the overlay.
7
+ - **`clank rm`** to remove files from the overlay.
6
8
  - **`clank link`** to connect overlay files to your project.
7
- - **`clank unlink`** to disconnect.
8
9
  - **`clank commit`** to commit changes in the overlay repository.
9
- - **`clank check`** to show overlay status and help realign overlay files when your project restructures.
10
+ - **`clank check`** to show overlay status and find misaligned files.
10
11
 
11
12
  ## Why a Separate Repository?
12
13
 
13
- Clank stores your AI agent files (CLAUDE.md, commands, notes) in a separate git repository
14
- symlinks them into your project. This separation provides key advantages:
14
+ Clank stores your AI agent files (CLAUDE.md, commands, notes) in a separate git repository and symlinks them into your project. This separation provides key advantages:
15
15
 
16
16
  - **Different Review Cadence**: Update agent instructions, commands, and notes without requiring the same review process as production code.
17
17
  - **Work on Repos You Don't Control**: Add agent context to open source projects or third-party codebases without forking or modifying.
@@ -39,49 +39,27 @@ npx clank init
39
39
 
40
40
  ## Quick Start
41
41
 
42
- ### 1. Initialize Overlay Repository
43
-
44
- ```bash
45
- clank init
46
- # Creates ~/clankover with default structure
47
- # Creates ~/.config/clank/config.js
48
- ```
49
-
50
- ### 2. Link to Your Project
51
-
52
42
  ```bash
43
+ clank init # Create overlay repository (~/.clankover)
53
44
  cd ~/my-project
54
- clank link
55
- # Auto-detects project name from git
56
- # Creates symlinks from overlay to current directory
45
+ clank link # Connect project to overlay
46
+ clank add CLAUDE.md # Add agent file (creates symlinks)
47
+ clank add notes.md --global # Add global file (shared across projects)
57
48
  ```
58
49
 
59
- ### 3. Add Files with `clank add`
60
-
61
- The `clank add` command moves files to the overlay and creates symlinks.
62
- Agent files (CLAUDE.md, AGENTS.md) stay in place; other files go in `clank/`.
63
-
64
- > You can run `clank add` from any subdirectory. Agent files and `clank/` folders work at any level in your project tree.
65
-
66
- ```bash
67
- # Add to project scope (default) - shared across all branches
68
- clank add CLAUDE.md
69
-
70
- # Add to global scope - shared across all projects
71
- clank add style.md --global
50
+ Agent files (CLAUDE.md, AGENTS.md, GEMINI.md) stay in place; other files go in `clank/`. You can run `clank add` from any subdirectory.
72
51
 
73
- # Add to worktree scope - this branch only
74
- clank add notes.md --worktree
52
+ ## Commands
75
53
 
76
- # Add commands
77
- clank add .claude/commands/review.md --global # All projects
78
- clank add .claude/commands/build.md # This project
54
+ ### Scope Options
79
55
 
80
- # Add a directory (all files inside)
81
- clank add clank/
82
- ```
56
+ Several commands (`add`, `rm`, `mv`) accept scope flags to specify where files are stored:
83
57
 
84
- ## Commands
58
+ | Flag | Scope | Shared Across |
59
+ |------|-------|---------------|
60
+ | `--global`, `-g` | Global | All projects |
61
+ | `--project`, `-p` | Project (default) | All branches in project |
62
+ | `--worktree`, `-w` | Worktree | This branch only |
85
63
 
86
64
  ### `clank init [overlay-path]`
87
65
 
@@ -103,16 +81,7 @@ clank link ~/my-project # Link to specific project
103
81
 
104
82
  ### `clank add <file> [options]`
105
83
 
106
- Move a file to the overlay and replace it with a symlink.
107
- If the file doesn't exist, an empty file is created.
108
-
109
- **Scope Options:**
110
-
111
- | Flag | Scope | Shared Across |
112
- |------|-------|---------------|
113
- | `--global` | Global | All projects |
114
- | `--project` | Project (default) | All branches in project |
115
- | `--worktree` | Worktree | This branch only |
84
+ Move a file to the overlay and replace it with a symlink. If the file doesn't exist, an empty file is created. Accepts [scope options](#scope-options).
116
85
 
117
86
  **Examples:**
118
87
  ```bash
@@ -149,7 +118,7 @@ clank commit # Commits with "[clank] update"
149
118
  clank commit -m "add style guide" # Commits with "[clank] add style guide"
150
119
  ```
151
120
 
152
- ### `clank check`
121
+ ### `clank check` (alias: `status`)
153
122
 
154
123
  Check for orphaned overlay paths that don't match the target project structure.
155
124
  Useful when a target project has renamed directories and the overlay needs updating.
@@ -168,6 +137,40 @@ clank check
168
137
  # The following overlay files no longer match...
169
138
  ```
170
139
 
140
+ ### `clank rm <files...>` (alias: `remove`)
141
+
142
+ Remove file(s) from both the overlay repository and the local project symlinks. Accepts [scope options](#scope-options); if omitted, clank detects the scope from the symlink.
143
+
144
+ **Example:**
145
+ ```bash
146
+ clank rm clank/notes.md # Remove from whatever scope it belongs to
147
+ clank rm style.md --global # Remove global style guide
148
+ ```
149
+
150
+ ### `clank mv <files...>` (alias: `move`)
151
+
152
+ Move file(s) between overlay scopes. Requires one [scope option](#scope-options) to specify the destination.
153
+
154
+ **Example:**
155
+ ```bash
156
+ # Promote a worktree note to project scope
157
+ clank mv clank/notes.md --project
158
+
159
+ # Share a local command globally
160
+ clank mv .claude/commands/test.md --global
161
+ ```
162
+
163
+ ### `clank vscode`
164
+
165
+ Generate `.vscode/settings.json` to make clank files visible in VS Code's explorer and search, while still respecting your `.gitignore` rules.
166
+
167
+ Since clank relies on symlinked files that are git-ignored, VS Code often hides them by default. This command explicitly excludes your gitignored files in `settings.json` while un-hiding the clank folders.
168
+
169
+ **Options:**
170
+ - `--remove` - Remove the clank-generated settings
171
+
172
+ See [Configuration](#configuration) for `vscodeSettings` and `vscodeGitignore` options.
173
+
171
174
  ### `clank help structure`
172
175
 
173
176
  Show the overlay directory structure and mapping rules.
@@ -239,10 +242,7 @@ Run `clank unlink` then `clank link` to apply config changes.
239
242
 
240
243
  ## Worktree Templates
241
244
 
242
- Customize `overlay/global/init/` to create starter notes and planning files
243
- for worktrees.
244
- When you run `clank link` in a new worktree,
245
- these templates are copied into your overlay.
245
+ Customize `global/init/clank/` in your overlay to create starter notes and planning files for new worktrees. When you run `clank link` in a new worktree, these templates are copied into the worktree's overlay directory.
246
246
 
247
247
  Available placeholders:
248
248
 
@@ -254,9 +254,8 @@ Available placeholders:
254
254
 
255
255
  1. **Everything is linked, nothing is copied** - Single source of truth in overlay
256
256
  2. **Git-aware** - Automatic project and worktree detection
257
- 3. **Explicit scopes** - Three clear levels: global, project, worktree
258
- 4. **Flat target structure** - All clank notes show up together in `clank/`
259
- 5. **Simple commands** - mostly `clank add` and `clank link`
257
+ 3. **Explicit scopes** - Three levels: global, project, worktree
258
+ 4. **Flat target structure** - All notes show up together in `clank/`
260
259
 
261
260
  ## Reference
262
261
 
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "clank-cli",
3
- "version": "0.1.52",
4
3
  "description": "Keep AI files in a separate overlay repository",
4
+ "version": "0.1.58",
5
+ "author": "",
5
6
  "type": "module",
6
7
  "bin": {
7
8
  "clank": "./bin/clank.ts"
@@ -10,21 +11,7 @@
10
11
  "bin/",
11
12
  "src/"
12
13
  ],
13
- "engines": {
14
- "node": ">=22.6.0"
15
- },
16
- "keywords": [
17
- "ai",
18
- "assistant",
19
- "claude",
20
- "gemini",
21
- "copilot",
22
- "configuration",
23
- "worktree",
24
- "git"
25
- ],
26
- "author": "",
27
- "license": "MIT",
14
+ "repository": "github:mighdoll/clank",
28
15
  "dependencies": {
29
16
  "commander": "^14.0.2",
30
17
  "cosmiconfig": "^9.0.0"
@@ -32,19 +19,38 @@
32
19
  "devDependencies": {
33
20
  "@biomejs/biome": "^2.3.8",
34
21
  "@types/node": "^24.10.1",
22
+ "@typescript/native-preview": "7.0.0-dev.20251221.1",
23
+ "@vitest/ui": "^4.0.16",
35
24
  "execa": "^9.6.1",
36
25
  "monobump": "^0.1.5",
37
- "typescript": "^5.9.3",
26
+ "npm-run-all2": "^7.0.2",
27
+ "syncpack": "^13.0.4",
38
28
  "vitest": "^4.0.14"
39
29
  },
30
+ "license": "MIT",
31
+ "keywords": [
32
+ "agent",
33
+ "ai",
34
+ "assistant",
35
+ "claude",
36
+ "configuration",
37
+ "copilot",
38
+ "gemini",
39
+ "git",
40
+ "worktree"
41
+ ],
42
+ "engines": {
43
+ "node": ">=22.6.0"
44
+ },
40
45
  "scripts": {
41
- "test": "vitest",
42
- "test:ui": "vitest --ui",
43
- "typecheck": "tsc --noEmit",
44
46
  "bump": "monobump",
47
+ "fix": "biome check --fix --unsafe",
48
+ "fix:pkgJsonFormat": "syncpack format",
45
49
  "global": "pnpm link --global",
46
- "build": "tsc",
47
- "lint": "biome check",
48
- "fix": "biome check --fix --unsafe"
50
+ "lint": "biome check --error-on-warnings",
51
+ "test": "vitest",
52
+ "test:once": "vitest run",
53
+ "test:ui": "vitest --ui",
54
+ "typecheck": "tsgo"
49
55
  }
50
56
  }
@@ -1,7 +1,12 @@
1
1
  import { lstat } from "node:fs/promises";
2
2
  import { basename, dirname, join } from "node:path";
3
3
  import { agentFiles } from "./AgentFiles.ts";
4
- import { isTrackedByGit, relativePath, resolveSymlinkTarget, walkDirectory } from "./FsUtil.ts";
4
+ import {
5
+ isTrackedByGit,
6
+ relativePath,
7
+ resolveSymlinkTarget,
8
+ walkDirectory,
9
+ } from "./FsUtil.ts";
5
10
  import type { GitContext } from "./Git.ts";
6
11
  import { type MapperContext, targetToOverlay } from "./Mapper.ts";
7
12
 
package/src/Config.ts CHANGED
@@ -89,7 +89,9 @@ function getConfigDir(): string {
89
89
  }
90
90
 
91
91
  /** Validate overlay repository exists, throw if not */
92
- export async function validateOverlayExists(overlayRoot: string): Promise<void> {
92
+ export async function validateOverlayExists(
93
+ overlayRoot: string,
94
+ ): Promise<void> {
93
95
  if (!(await fileExists(overlayRoot))) {
94
96
  throw new Error(
95
97
  `Overlay repository not found at ${overlayRoot}\nRun 'clank init' to create it`,
package/src/FsUtil.ts CHANGED
@@ -1,4 +1,13 @@
1
- import { lstat, mkdir, readdir, readFile, readlink, symlink, unlink, writeFile } from "node:fs/promises";
1
+ import {
2
+ lstat,
3
+ mkdir,
4
+ readdir,
5
+ readFile,
6
+ readlink,
7
+ symlink,
8
+ unlink,
9
+ writeFile,
10
+ } from "node:fs/promises";
2
11
  import { dirname, isAbsolute, join, relative } from "node:path";
3
12
  import { execFileAsync } from "./Exec.ts";
4
13
 
package/src/Gitignore.ts CHANGED
@@ -169,7 +169,6 @@ async function parseGitignoreFile(
169
169
  result.negationWarnings.push(...parsed.negationWarnings);
170
170
  }
171
171
 
172
-
173
172
  /** Parse a single gitignore line */
174
173
  function parseLine(
175
174
  trimmed: string,
@@ -195,7 +194,9 @@ function parseGitignoreContent(
195
194
  options: ParseOptions,
196
195
  ): { patterns: GitignorePattern[]; negationWarnings: string[] } {
197
196
  const rawLines = content.split("\n");
198
- const lines = options.skipClankSection ? filterClankLines(rawLines) : rawLines;
197
+ const lines = options.skipClankSection
198
+ ? filterClankLines(rawLines)
199
+ : rawLines;
199
200
  const basePath = options.basePath ?? "";
200
201
 
201
202
  const patterns: GitignorePattern[] = [];
@@ -1,8 +1,18 @@
1
1
  import { lstat } from "node:fs/promises";
2
2
  import { dirname, join, relative } from "node:path";
3
3
  import { managedAgentDirs } from "./AgentFiles.ts";
4
- import { createSymlink, ensureDir, getLinkTarget, resolveSymlinkTarget, walkDirectory } from "./FsUtil.ts";
5
- import { getPromptRelPath, type MapperContext, overlayToTarget } from "./Mapper.ts";
4
+ import {
5
+ createSymlink,
6
+ ensureDir,
7
+ getLinkTarget,
8
+ resolveSymlinkTarget,
9
+ walkDirectory,
10
+ } from "./FsUtil.ts";
11
+ import {
12
+ getPromptRelPath,
13
+ type MapperContext,
14
+ overlayToTarget,
15
+ } from "./Mapper.ts";
6
16
 
7
17
  export type ManagedFileState =
8
18
  | { kind: "valid" }
@@ -119,25 +119,50 @@ async function addSingleFile(
119
119
  const params = { overlayPath, symlinkDir, gitRoot, overlayRoot, agents };
120
120
  await createAgentLinks(params);
121
121
  } else if (isPromptFile(normalizedPath)) {
122
- // Prompt files get symlinks in all agent directories
123
- const promptRelPath = getPromptRelPath(normalizedPath);
124
- if (promptRelPath) {
125
- const created = await createPromptLinks(overlayPath, promptRelPath, gitRoot);
126
- if (created.length) {
127
- console.log(`Created symlinks: ${created.map((p) => relative(cwd, p)).join(", ")}`);
128
- }
129
- }
122
+ await handlePromptFile(normalizedPath, overlayPath, gitRoot, cwd);
130
123
  } else {
131
- if (await isSymlinkToOverlay(normalizedPath, overlayRoot)) {
132
- console.log(`Symlink already exists: ${relative(cwd, normalizedPath)}`);
133
- } else {
134
- const linkTarget = getLinkTarget(normalizedPath, overlayPath);
135
- await createSymlink(linkTarget, normalizedPath);
136
- console.log(`Created symlink: ${relative(cwd, normalizedPath)}`);
124
+ await handleRegularFile(normalizedPath, overlayPath, overlayRoot, cwd);
125
+ }
126
+ }
127
+
128
+ /** Handle prompt file symlink creation */
129
+ async function handlePromptFile(
130
+ normalizedPath: string,
131
+ overlayPath: string,
132
+ gitRoot: string,
133
+ cwd: string,
134
+ ): Promise<void> {
135
+ const promptRelPath = getPromptRelPath(normalizedPath);
136
+ if (promptRelPath) {
137
+ const created = await createPromptLinks(
138
+ overlayPath,
139
+ promptRelPath,
140
+ gitRoot,
141
+ );
142
+ if (created.length) {
143
+ console.log(
144
+ `Created symlinks: ${created.map((p) => relative(cwd, p)).join(", ")}`,
145
+ );
137
146
  }
138
147
  }
139
148
  }
140
149
 
150
+ /** Handle regular file symlink creation */
151
+ async function handleRegularFile(
152
+ normalizedPath: string,
153
+ overlayPath: string,
154
+ overlayRoot: string,
155
+ cwd: string,
156
+ ): Promise<void> {
157
+ if (await isSymlinkToOverlay(normalizedPath, overlayRoot)) {
158
+ console.log(`Symlink already exists: ${relative(cwd, normalizedPath)}`);
159
+ } else {
160
+ const linkTarget = getLinkTarget(normalizedPath, overlayPath);
161
+ await createSymlink(linkTarget, normalizedPath);
162
+ console.log(`Created symlink: ${relative(cwd, normalizedPath)}`);
163
+ }
164
+ }
165
+
141
166
  async function isDirectory(path: string): Promise<boolean> {
142
167
  try {
143
168
  return (await lstat(path)).isDirectory();
@@ -6,7 +6,12 @@ import {
6
6
  classifyAgentFiles,
7
7
  formatAgentFileProblems,
8
8
  } from "../ClassifyFiles.ts";
9
- import { type ClankConfig, expandPath, loadConfig, validateOverlayExists } from "../Config.ts";
9
+ import {
10
+ type ClankConfig,
11
+ expandPath,
12
+ loadConfig,
13
+ validateOverlayExists,
14
+ } from "../Config.ts";
10
15
  import { addGitExcludes } from "../Exclude.ts";
11
16
  import {
12
17
  createSymlink,
@@ -167,6 +167,8 @@ async function recreatePromptLinks(
167
167
  const created = await createPromptLinks(overlayPath, promptRelPath, gitRoot);
168
168
 
169
169
  if (created.length > 0) {
170
- console.log(`Updated symlinks: ${created.map((p) => relative(gitRoot, p)).join(", ")}`);
170
+ console.log(
171
+ `Updated symlinks: ${created.map((p) => relative(gitRoot, p)).join(", ")}`,
172
+ );
171
173
  }
172
174
  }