claude-threads 0.12.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/CHANGELOG.md +473 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/changelog.d.ts +20 -0
- package/dist/changelog.js +134 -0
- package/dist/claude/cli.d.ts +42 -0
- package/dist/claude/cli.js +173 -0
- package/dist/claude/session.d.ts +256 -0
- package/dist/claude/session.js +1964 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +94 -0
- package/dist/git/worktree.d.ts +50 -0
- package/dist/git/worktree.js +228 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +371 -0
- package/dist/logo.d.ts +31 -0
- package/dist/logo.js +57 -0
- package/dist/mattermost/api.d.ts +85 -0
- package/dist/mattermost/api.js +124 -0
- package/dist/mattermost/api.test.d.ts +1 -0
- package/dist/mattermost/api.test.js +319 -0
- package/dist/mattermost/client.d.ts +56 -0
- package/dist/mattermost/client.js +321 -0
- package/dist/mattermost/emoji.d.ts +43 -0
- package/dist/mattermost/emoji.js +65 -0
- package/dist/mattermost/emoji.test.d.ts +1 -0
- package/dist/mattermost/emoji.test.js +131 -0
- package/dist/mattermost/types.d.ts +71 -0
- package/dist/mattermost/types.js +1 -0
- package/dist/mcp/permission-server.d.ts +2 -0
- package/dist/mcp/permission-server.js +201 -0
- package/dist/onboarding.d.ts +1 -0
- package/dist/onboarding.js +116 -0
- package/dist/persistence/session-store.d.ts +65 -0
- package/dist/persistence/session-store.js +127 -0
- package/dist/update-notifier.d.ts +3 -0
- package/dist/update-notifier.js +31 -0
- package/dist/utils/logger.d.ts +34 -0
- package/dist/utils/logger.js +42 -0
- package/dist/utils/logger.test.d.ts +1 -0
- package/dist/utils/logger.test.js +121 -0
- package/dist/utils/tool-formatter.d.ts +56 -0
- package/dist/utils/tool-formatter.js +247 -0
- package/dist/utils/tool-formatter.test.d.ts +1 -0
- package/dist/utils/tool-formatter.test.js +357 -0
- package/package.json +85 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Check if any .env config file exists */
|
|
2
|
+
export declare function configExists(): boolean;
|
|
3
|
+
export type WorktreeMode = 'off' | 'prompt' | 'require';
|
|
4
|
+
/** CLI arguments that can override config */
|
|
5
|
+
export interface CliArgs {
|
|
6
|
+
url?: string;
|
|
7
|
+
token?: string;
|
|
8
|
+
channel?: string;
|
|
9
|
+
botName?: string;
|
|
10
|
+
allowedUsers?: string;
|
|
11
|
+
skipPermissions?: boolean;
|
|
12
|
+
chrome?: boolean;
|
|
13
|
+
worktreeMode?: WorktreeMode;
|
|
14
|
+
}
|
|
15
|
+
export interface Config {
|
|
16
|
+
mattermost: {
|
|
17
|
+
url: string;
|
|
18
|
+
token: string;
|
|
19
|
+
channelId: string;
|
|
20
|
+
botName: string;
|
|
21
|
+
};
|
|
22
|
+
allowedUsers: string[];
|
|
23
|
+
skipPermissions: boolean;
|
|
24
|
+
chrome: boolean;
|
|
25
|
+
worktreeMode: WorktreeMode;
|
|
26
|
+
}
|
|
27
|
+
export declare function loadConfig(cliArgs?: CliArgs): Config;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { config } from 'dotenv';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
let envLoaded = false;
|
|
6
|
+
// Paths to search for .env files (in order of priority)
|
|
7
|
+
const ENV_PATHS = [
|
|
8
|
+
resolve(process.cwd(), '.env'), // Current directory
|
|
9
|
+
resolve(homedir(), '.config', 'claude-threads', '.env'), // ~/.config/claude-threads/.env
|
|
10
|
+
resolve(homedir(), '.claude-threads.env'), // ~/.claude-threads.env
|
|
11
|
+
];
|
|
12
|
+
function loadEnv() {
|
|
13
|
+
if (envLoaded)
|
|
14
|
+
return;
|
|
15
|
+
envLoaded = true;
|
|
16
|
+
for (const envPath of ENV_PATHS) {
|
|
17
|
+
if (existsSync(envPath)) {
|
|
18
|
+
if (process.env.DEBUG === '1' || process.argv.includes('--debug')) {
|
|
19
|
+
console.log(` [config] Loading from: ${envPath}`);
|
|
20
|
+
}
|
|
21
|
+
config({ path: envPath });
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Check if any .env config file exists */
|
|
27
|
+
export function configExists() {
|
|
28
|
+
return ENV_PATHS.some(p => existsSync(p));
|
|
29
|
+
}
|
|
30
|
+
function getRequired(cliValue, envName, name) {
|
|
31
|
+
const value = cliValue || process.env[envName];
|
|
32
|
+
if (!value) {
|
|
33
|
+
throw new Error(`Missing required config: ${name}. Set ${envName} in .env or use --${name.toLowerCase().replace(/ /g, '-')} flag.`);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
export function loadConfig(cliArgs) {
|
|
38
|
+
loadEnv();
|
|
39
|
+
// CLI args take priority over env vars
|
|
40
|
+
const url = getRequired(cliArgs?.url, 'MATTERMOST_URL', 'url');
|
|
41
|
+
const token = getRequired(cliArgs?.token, 'MATTERMOST_TOKEN', 'token');
|
|
42
|
+
const channelId = getRequired(cliArgs?.channel, 'MATTERMOST_CHANNEL_ID', 'channel');
|
|
43
|
+
const botName = cliArgs?.botName || process.env.MATTERMOST_BOT_NAME || 'claude-code';
|
|
44
|
+
const allowedUsersStr = cliArgs?.allowedUsers || process.env.ALLOWED_USERS || '';
|
|
45
|
+
const allowedUsers = allowedUsersStr
|
|
46
|
+
.split(',')
|
|
47
|
+
.map(u => u.trim())
|
|
48
|
+
.filter(u => u.length > 0);
|
|
49
|
+
// CLI --skip-permissions or --no-skip-permissions takes priority
|
|
50
|
+
// Then env SKIP_PERMISSIONS, then legacy flag
|
|
51
|
+
let skipPermissions;
|
|
52
|
+
if (cliArgs?.skipPermissions !== undefined) {
|
|
53
|
+
// CLI explicitly set (--skip-permissions or --no-skip-permissions)
|
|
54
|
+
skipPermissions = cliArgs.skipPermissions;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
skipPermissions = process.env.SKIP_PERMISSIONS === 'true' ||
|
|
58
|
+
process.argv.includes('--dangerously-skip-permissions');
|
|
59
|
+
}
|
|
60
|
+
// Chrome integration: CLI flag or env var
|
|
61
|
+
let chrome;
|
|
62
|
+
if (cliArgs?.chrome !== undefined) {
|
|
63
|
+
chrome = cliArgs.chrome;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
chrome = process.env.CLAUDE_CHROME === 'true';
|
|
67
|
+
}
|
|
68
|
+
// Worktree mode: CLI flag or env var, default to 'prompt'
|
|
69
|
+
let worktreeMode;
|
|
70
|
+
if (cliArgs?.worktreeMode !== undefined) {
|
|
71
|
+
worktreeMode = cliArgs.worktreeMode;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const envValue = process.env.WORKTREE_MODE?.toLowerCase();
|
|
75
|
+
if (envValue === 'off' || envValue === 'prompt' || envValue === 'require') {
|
|
76
|
+
worktreeMode = envValue;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
worktreeMode = 'prompt'; // Default
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
mattermost: {
|
|
84
|
+
url: url.replace(/\/$/, ''), // Remove trailing slash
|
|
85
|
+
token,
|
|
86
|
+
channelId,
|
|
87
|
+
botName,
|
|
88
|
+
},
|
|
89
|
+
allowedUsers,
|
|
90
|
+
skipPermissions,
|
|
91
|
+
chrome,
|
|
92
|
+
worktreeMode,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface WorktreeInfo {
|
|
2
|
+
path: string;
|
|
3
|
+
branch: string;
|
|
4
|
+
commit: string;
|
|
5
|
+
isMain: boolean;
|
|
6
|
+
isBare: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Check if a directory is inside a git repository
|
|
10
|
+
*/
|
|
11
|
+
export declare function isGitRepository(dir: string): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Get the root directory of the git repository
|
|
14
|
+
*/
|
|
15
|
+
export declare function getRepositoryRoot(dir: string): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Check if there are uncommitted changes (staged or unstaged)
|
|
18
|
+
*/
|
|
19
|
+
export declare function hasUncommittedChanges(dir: string): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* List all worktrees for a repository
|
|
22
|
+
*/
|
|
23
|
+
export declare function listWorktrees(repoRoot: string): Promise<WorktreeInfo[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Check if a branch exists (local or remote)
|
|
26
|
+
*/
|
|
27
|
+
export declare function branchExists(repoRoot: string, branch: string): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Generate the worktree directory path
|
|
30
|
+
* Creates path like: /path/to/repo-worktrees/branch-name-abc123
|
|
31
|
+
*/
|
|
32
|
+
export declare function getWorktreeDir(repoRoot: string, branch: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Create a new worktree for a branch
|
|
35
|
+
* If the branch doesn't exist, creates it from the current HEAD
|
|
36
|
+
*/
|
|
37
|
+
export declare function createWorktree(repoRoot: string, branch: string, targetDir: string): Promise<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Remove a worktree
|
|
40
|
+
*/
|
|
41
|
+
export declare function removeWorktree(repoRoot: string, worktreePath: string): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Find a worktree by branch name
|
|
44
|
+
*/
|
|
45
|
+
export declare function findWorktreeByBranch(repoRoot: string, branch: string): Promise<WorktreeInfo | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Validate a git branch name
|
|
48
|
+
* Based on git-check-ref-format rules
|
|
49
|
+
*/
|
|
50
|
+
export declare function isValidBranchName(name: string): boolean;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
/**
|
|
6
|
+
* Execute a git command and return stdout
|
|
7
|
+
*/
|
|
8
|
+
async function execGit(args, cwd) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const proc = spawn('git', args, { cwd });
|
|
11
|
+
let stdout = '';
|
|
12
|
+
let stderr = '';
|
|
13
|
+
proc.stdout.on('data', (data) => {
|
|
14
|
+
stdout += data.toString();
|
|
15
|
+
});
|
|
16
|
+
proc.stderr.on('data', (data) => {
|
|
17
|
+
stderr += data.toString();
|
|
18
|
+
});
|
|
19
|
+
proc.on('close', (code) => {
|
|
20
|
+
if (code === 0) {
|
|
21
|
+
resolve(stdout.trim());
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
reject(new Error(`git ${args.join(' ')} failed: ${stderr || stdout}`));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
proc.on('error', (err) => {
|
|
28
|
+
reject(err);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if a directory is inside a git repository
|
|
34
|
+
*/
|
|
35
|
+
export async function isGitRepository(dir) {
|
|
36
|
+
try {
|
|
37
|
+
await execGit(['rev-parse', '--git-dir'], dir);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the root directory of the git repository
|
|
46
|
+
*/
|
|
47
|
+
export async function getRepositoryRoot(dir) {
|
|
48
|
+
return execGit(['rev-parse', '--show-toplevel'], dir);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if there are uncommitted changes (staged or unstaged)
|
|
52
|
+
*/
|
|
53
|
+
export async function hasUncommittedChanges(dir) {
|
|
54
|
+
try {
|
|
55
|
+
// Check for staged changes
|
|
56
|
+
const staged = await execGit(['diff', '--cached', '--quiet'], dir).catch(() => 'changes');
|
|
57
|
+
if (staged === 'changes')
|
|
58
|
+
return true;
|
|
59
|
+
// Check for unstaged changes
|
|
60
|
+
const unstaged = await execGit(['diff', '--quiet'], dir).catch(() => 'changes');
|
|
61
|
+
if (unstaged === 'changes')
|
|
62
|
+
return true;
|
|
63
|
+
// Check for untracked files
|
|
64
|
+
const untracked = await execGit(['ls-files', '--others', '--exclude-standard'], dir);
|
|
65
|
+
return untracked.length > 0;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* List all worktrees for a repository
|
|
73
|
+
*/
|
|
74
|
+
export async function listWorktrees(repoRoot) {
|
|
75
|
+
const output = await execGit(['worktree', 'list', '--porcelain'], repoRoot);
|
|
76
|
+
const worktrees = [];
|
|
77
|
+
if (!output)
|
|
78
|
+
return worktrees;
|
|
79
|
+
// Parse porcelain output
|
|
80
|
+
// Format:
|
|
81
|
+
// worktree /path/to/worktree
|
|
82
|
+
// HEAD <commit>
|
|
83
|
+
// branch refs/heads/branch-name
|
|
84
|
+
// <blank line>
|
|
85
|
+
const blocks = output.split('\n\n').filter(Boolean);
|
|
86
|
+
for (const block of blocks) {
|
|
87
|
+
const lines = block.split('\n');
|
|
88
|
+
const worktree = {};
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
if (line.startsWith('worktree ')) {
|
|
91
|
+
worktree.path = line.slice(9);
|
|
92
|
+
}
|
|
93
|
+
else if (line.startsWith('HEAD ')) {
|
|
94
|
+
worktree.commit = line.slice(5);
|
|
95
|
+
}
|
|
96
|
+
else if (line.startsWith('branch ')) {
|
|
97
|
+
// refs/heads/branch-name -> branch-name
|
|
98
|
+
worktree.branch = line.slice(7).replace('refs/heads/', '');
|
|
99
|
+
}
|
|
100
|
+
else if (line === 'bare') {
|
|
101
|
+
worktree.isBare = true;
|
|
102
|
+
}
|
|
103
|
+
else if (line === 'detached') {
|
|
104
|
+
worktree.branch = '(detached)';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (worktree.path) {
|
|
108
|
+
worktrees.push({
|
|
109
|
+
path: worktree.path,
|
|
110
|
+
branch: worktree.branch || '(unknown)',
|
|
111
|
+
commit: worktree.commit || '',
|
|
112
|
+
isMain: worktrees.length === 0, // First worktree is the main one
|
|
113
|
+
isBare: worktree.isBare || false,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return worktrees;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Check if a branch exists (local or remote)
|
|
121
|
+
*/
|
|
122
|
+
export async function branchExists(repoRoot, branch) {
|
|
123
|
+
try {
|
|
124
|
+
// Check local branches
|
|
125
|
+
await execGit(['rev-parse', '--verify', `refs/heads/${branch}`], repoRoot);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
try {
|
|
130
|
+
// Check remote branches
|
|
131
|
+
await execGit(['rev-parse', '--verify', `refs/remotes/origin/${branch}`], repoRoot);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Generate the worktree directory path
|
|
141
|
+
* Creates path like: /path/to/repo-worktrees/branch-name-abc123
|
|
142
|
+
*/
|
|
143
|
+
export function getWorktreeDir(repoRoot, branch) {
|
|
144
|
+
const repoName = path.basename(repoRoot);
|
|
145
|
+
const parentDir = path.dirname(repoRoot);
|
|
146
|
+
const worktreesDir = path.join(parentDir, `${repoName}-worktrees`);
|
|
147
|
+
// Sanitize branch name for filesystem
|
|
148
|
+
const sanitizedBranch = branch
|
|
149
|
+
.replace(/\//g, '-')
|
|
150
|
+
.replace(/[^a-zA-Z0-9-_]/g, '');
|
|
151
|
+
const shortUuid = randomUUID().slice(0, 8);
|
|
152
|
+
return path.join(worktreesDir, `${sanitizedBranch}-${shortUuid}`);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Create a new worktree for a branch
|
|
156
|
+
* If the branch doesn't exist, creates it from the current HEAD
|
|
157
|
+
*/
|
|
158
|
+
export async function createWorktree(repoRoot, branch, targetDir) {
|
|
159
|
+
// Ensure the parent directory exists
|
|
160
|
+
const parentDir = path.dirname(targetDir);
|
|
161
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
162
|
+
// Check if branch exists
|
|
163
|
+
const exists = await branchExists(repoRoot, branch);
|
|
164
|
+
if (exists) {
|
|
165
|
+
// Use existing branch
|
|
166
|
+
await execGit(['worktree', 'add', targetDir, branch], repoRoot);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Create new branch from HEAD
|
|
170
|
+
await execGit(['worktree', 'add', '-b', branch, targetDir], repoRoot);
|
|
171
|
+
}
|
|
172
|
+
return targetDir;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Remove a worktree
|
|
176
|
+
*/
|
|
177
|
+
export async function removeWorktree(repoRoot, worktreePath) {
|
|
178
|
+
// First try to remove cleanly
|
|
179
|
+
try {
|
|
180
|
+
await execGit(['worktree', 'remove', worktreePath], repoRoot);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// If that fails, try force remove
|
|
184
|
+
await execGit(['worktree', 'remove', '--force', worktreePath], repoRoot);
|
|
185
|
+
}
|
|
186
|
+
// Prune any stale worktree references
|
|
187
|
+
await execGit(['worktree', 'prune'], repoRoot);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Find a worktree by branch name
|
|
191
|
+
*/
|
|
192
|
+
export async function findWorktreeByBranch(repoRoot, branch) {
|
|
193
|
+
const worktrees = await listWorktrees(repoRoot);
|
|
194
|
+
return worktrees.find((wt) => wt.branch === branch) || null;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Validate a git branch name
|
|
198
|
+
* Based on git-check-ref-format rules
|
|
199
|
+
*/
|
|
200
|
+
export function isValidBranchName(name) {
|
|
201
|
+
if (!name || name.length === 0)
|
|
202
|
+
return false;
|
|
203
|
+
// Cannot start or end with /
|
|
204
|
+
if (name.startsWith('/') || name.endsWith('/'))
|
|
205
|
+
return false;
|
|
206
|
+
// Cannot contain ..
|
|
207
|
+
if (name.includes('..'))
|
|
208
|
+
return false;
|
|
209
|
+
// Cannot contain special characters
|
|
210
|
+
if (/[\s~^:?*[\]\\]/.test(name))
|
|
211
|
+
return false;
|
|
212
|
+
// Cannot start with -
|
|
213
|
+
if (name.startsWith('-'))
|
|
214
|
+
return false;
|
|
215
|
+
// Cannot end with .lock
|
|
216
|
+
if (name.endsWith('.lock'))
|
|
217
|
+
return false;
|
|
218
|
+
// Cannot contain @{
|
|
219
|
+
if (name.includes('@{'))
|
|
220
|
+
return false;
|
|
221
|
+
// Cannot be @
|
|
222
|
+
if (name === '@')
|
|
223
|
+
return false;
|
|
224
|
+
// Cannot contain consecutive dots
|
|
225
|
+
if (/\.\./.test(name))
|
|
226
|
+
return false;
|
|
227
|
+
return true;
|
|
228
|
+
}
|
package/dist/index.d.ts
ADDED