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 +56 -57
- package/package.json +29 -23
- package/src/ClassifyFiles.ts +6 -1
- package/src/Config.ts +3 -1
- package/src/FsUtil.ts +10 -1
- package/src/Gitignore.ts +3 -2
- package/src/OverlayLinks.ts +12 -2
- package/src/commands/Add.ts +39 -14
- package/src/commands/Link.ts +6 -1
- package/src/commands/Move.ts +3 -1
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
|
|
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
|
-
#
|
|
56
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
clank add notes.md --worktree
|
|
52
|
+
## Commands
|
|
75
53
|
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
clank add clank/
|
|
82
|
-
```
|
|
56
|
+
Several commands (`add`, `rm`, `mv`) accept scope flags to specify where files are stored:
|
|
83
57
|
|
|
84
|
-
|
|
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 `
|
|
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
|
|
258
|
-
4. **Flat target structure** - All
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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
|
}
|
package/src/ClassifyFiles.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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 {
|
|
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
|
|
197
|
+
const lines = options.skipClankSection
|
|
198
|
+
? filterClankLines(rawLines)
|
|
199
|
+
: rawLines;
|
|
199
200
|
const basePath = options.basePath ?? "";
|
|
200
201
|
|
|
201
202
|
const patterns: GitignorePattern[] = [];
|
package/src/OverlayLinks.ts
CHANGED
|
@@ -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 {
|
|
5
|
-
|
|
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" }
|
package/src/commands/Add.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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();
|
package/src/commands/Link.ts
CHANGED
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
classifyAgentFiles,
|
|
7
7
|
formatAgentFileProblems,
|
|
8
8
|
} from "../ClassifyFiles.ts";
|
|
9
|
-
import {
|
|
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,
|
package/src/commands/Move.ts
CHANGED
|
@@ -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(
|
|
170
|
+
console.log(
|
|
171
|
+
`Updated symlinks: ${created.map((p) => relative(gitRoot, p)).join(", ")}`,
|
|
172
|
+
);
|
|
171
173
|
}
|
|
172
174
|
}
|