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.
Files changed (46) hide show
  1. package/CHANGELOG.md +473 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/dist/changelog.d.ts +20 -0
  5. package/dist/changelog.js +134 -0
  6. package/dist/claude/cli.d.ts +42 -0
  7. package/dist/claude/cli.js +173 -0
  8. package/dist/claude/session.d.ts +256 -0
  9. package/dist/claude/session.js +1964 -0
  10. package/dist/config.d.ts +27 -0
  11. package/dist/config.js +94 -0
  12. package/dist/git/worktree.d.ts +50 -0
  13. package/dist/git/worktree.js +228 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +371 -0
  16. package/dist/logo.d.ts +31 -0
  17. package/dist/logo.js +57 -0
  18. package/dist/mattermost/api.d.ts +85 -0
  19. package/dist/mattermost/api.js +124 -0
  20. package/dist/mattermost/api.test.d.ts +1 -0
  21. package/dist/mattermost/api.test.js +319 -0
  22. package/dist/mattermost/client.d.ts +56 -0
  23. package/dist/mattermost/client.js +321 -0
  24. package/dist/mattermost/emoji.d.ts +43 -0
  25. package/dist/mattermost/emoji.js +65 -0
  26. package/dist/mattermost/emoji.test.d.ts +1 -0
  27. package/dist/mattermost/emoji.test.js +131 -0
  28. package/dist/mattermost/types.d.ts +71 -0
  29. package/dist/mattermost/types.js +1 -0
  30. package/dist/mcp/permission-server.d.ts +2 -0
  31. package/dist/mcp/permission-server.js +201 -0
  32. package/dist/onboarding.d.ts +1 -0
  33. package/dist/onboarding.js +116 -0
  34. package/dist/persistence/session-store.d.ts +65 -0
  35. package/dist/persistence/session-store.js +127 -0
  36. package/dist/update-notifier.d.ts +3 -0
  37. package/dist/update-notifier.js +31 -0
  38. package/dist/utils/logger.d.ts +34 -0
  39. package/dist/utils/logger.js +42 -0
  40. package/dist/utils/logger.test.d.ts +1 -0
  41. package/dist/utils/logger.test.js +121 -0
  42. package/dist/utils/tool-formatter.d.ts +56 -0
  43. package/dist/utils/tool-formatter.js +247 -0
  44. package/dist/utils/tool-formatter.test.d.ts +1 -0
  45. package/dist/utils/tool-formatter.test.js +357 -0
  46. package/package.json +85 -0
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};