claude-threads 0.13.0 → 0.14.1
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 +32 -0
- package/README.md +78 -28
- package/dist/claude/cli.d.ts +8 -0
- package/dist/claude/cli.js +16 -8
- package/dist/config/migration.d.ts +45 -0
- package/dist/config/migration.js +35 -0
- package/dist/config.d.ts +12 -18
- package/dist/config.js +7 -94
- package/dist/git/worktree.d.ts +0 -4
- package/dist/git/worktree.js +1 -1
- package/dist/index.js +31 -13
- package/dist/logo.d.ts +3 -20
- package/dist/logo.js +7 -23
- package/dist/mcp/permission-server.js +61 -112
- package/dist/onboarding.js +262 -137
- package/dist/persistence/session-store.d.ts +8 -2
- package/dist/persistence/session-store.js +41 -16
- package/dist/platform/client.d.ts +140 -0
- package/dist/platform/formatter.d.ts +74 -0
- package/dist/platform/index.d.ts +11 -0
- package/dist/platform/index.js +8 -0
- package/dist/platform/mattermost/client.d.ts +70 -0
- package/dist/{mattermost → platform/mattermost}/client.js +117 -34
- package/dist/platform/mattermost/formatter.d.ts +20 -0
- package/dist/platform/mattermost/formatter.js +46 -0
- package/dist/platform/mattermost/permission-api.d.ts +10 -0
- package/dist/platform/mattermost/permission-api.js +139 -0
- package/dist/platform/mattermost/types.js +1 -0
- package/dist/platform/permission-api-factory.d.ts +11 -0
- package/dist/platform/permission-api-factory.js +21 -0
- package/dist/platform/permission-api.d.ts +67 -0
- package/dist/platform/permission-api.js +8 -0
- package/dist/platform/types.d.ts +70 -0
- package/dist/platform/types.js +7 -0
- package/dist/session/commands.d.ts +52 -0
- package/dist/session/commands.js +323 -0
- package/dist/session/events.d.ts +25 -0
- package/dist/session/events.js +368 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +6 -0
- package/dist/session/lifecycle.d.ts +70 -0
- package/dist/session/lifecycle.js +456 -0
- package/dist/session/manager.d.ts +96 -0
- package/dist/session/manager.js +537 -0
- package/dist/session/reactions.d.ts +25 -0
- package/dist/session/reactions.js +151 -0
- package/dist/session/streaming.d.ts +47 -0
- package/dist/session/streaming.js +152 -0
- package/dist/session/types.d.ts +78 -0
- package/dist/session/types.js +9 -0
- package/dist/session/worktree.d.ts +56 -0
- package/dist/session/worktree.js +339 -0
- package/dist/update-notifier.js +10 -0
- package/dist/{mattermost → utils}/emoji.d.ts +3 -3
- package/dist/{mattermost → utils}/emoji.js +3 -3
- package/dist/utils/emoji.test.d.ts +1 -0
- package/dist/utils/tool-formatter.d.ts +10 -13
- package/dist/utils/tool-formatter.js +48 -43
- package/dist/utils/tool-formatter.test.js +67 -52
- package/package.json +4 -3
- package/dist/claude/session.d.ts +0 -256
- package/dist/claude/session.js +0 -1964
- package/dist/mattermost/client.d.ts +0 -56
- /package/dist/{mattermost/emoji.test.d.ts → platform/client.js} +0 -0
- /package/dist/{mattermost/types.js → platform/formatter.js} +0 -0
- /package/dist/{mattermost → platform/mattermost}/types.d.ts +0 -0
- /package/dist/{mattermost → utils}/emoji.test.js +0 -0
package/dist/onboarding.js
CHANGED
|
@@ -1,85 +1,187 @@
|
|
|
1
1
|
import prompts from 'prompts';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { CONFIG_PATH, saveConfig, } from './config/migration.js';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
6
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
7
7
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
8
8
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
9
|
+
const onCancel = () => {
|
|
10
|
+
console.log('');
|
|
11
|
+
console.log(dim(' Setup cancelled.'));
|
|
12
|
+
process.exit(0);
|
|
13
|
+
};
|
|
14
|
+
export async function runOnboarding(reconfigure = false) {
|
|
15
|
+
console.log('');
|
|
16
|
+
console.log(bold(' claude-threads setup'));
|
|
17
|
+
console.log(dim(' ─────────────────────────────────'));
|
|
18
|
+
console.log('');
|
|
19
|
+
// Load existing config if reconfiguring
|
|
20
|
+
let existingConfig = null;
|
|
21
|
+
if (reconfigure && existsSync(CONFIG_PATH)) {
|
|
22
|
+
try {
|
|
23
|
+
const content = readFileSync(CONFIG_PATH, 'utf-8');
|
|
24
|
+
existingConfig = YAML.parse(content);
|
|
25
|
+
console.log(dim(' Reconfiguring existing setup.'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
console.log(dim(' Could not load existing config, starting fresh.'));
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const { path: existingPath, values: existing } = reconfigure ? loadExistingConfig() : { path: null, values: {} };
|
|
31
|
-
const hasExisting = Object.keys(existing).length > 0;
|
|
31
|
+
else {
|
|
32
|
+
console.log(' Welcome! Let\'s configure claude-threads.');
|
|
33
|
+
}
|
|
32
34
|
console.log('');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// Step 1: Global settings
|
|
36
|
+
const globalSettings = await prompts([
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
name: 'workingDir',
|
|
40
|
+
message: 'Default working directory',
|
|
41
|
+
initial: existingConfig?.workingDir || process.cwd(),
|
|
42
|
+
hint: 'Where Claude Code runs by default',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'confirm',
|
|
46
|
+
name: 'chrome',
|
|
47
|
+
message: 'Enable Chrome integration?',
|
|
48
|
+
initial: existingConfig?.chrome || false,
|
|
49
|
+
hint: 'Requires Claude in Chrome extension',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: 'select',
|
|
53
|
+
name: 'worktreeMode',
|
|
54
|
+
message: 'Git worktree mode',
|
|
55
|
+
choices: [
|
|
56
|
+
{ title: 'Prompt', value: 'prompt', description: 'Ask when starting sessions' },
|
|
57
|
+
{ title: 'Off', value: 'off', description: 'Never use worktrees' },
|
|
58
|
+
{ title: 'Require', value: 'require', description: 'Always require branch name' },
|
|
59
|
+
],
|
|
60
|
+
initial: existingConfig?.worktreeMode === 'off' ? 1 :
|
|
61
|
+
existingConfig?.worktreeMode === 'require' ? 2 : 0,
|
|
62
|
+
},
|
|
63
|
+
], { onCancel });
|
|
64
|
+
const config = {
|
|
65
|
+
version: 2,
|
|
66
|
+
...globalSettings,
|
|
67
|
+
platforms: [],
|
|
68
|
+
};
|
|
69
|
+
// Step 2: Add platforms (loop)
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(dim(' Now let\'s add your platform connections.'));
|
|
72
|
+
console.log('');
|
|
73
|
+
let platformNumber = 1;
|
|
74
|
+
let addMore = true;
|
|
75
|
+
while (addMore) {
|
|
76
|
+
const isFirst = platformNumber === 1;
|
|
77
|
+
const existingPlatform = existingConfig?.platforms[platformNumber - 1];
|
|
78
|
+
// Ask what platform type
|
|
79
|
+
const { platformType } = await prompts({
|
|
80
|
+
type: 'select',
|
|
81
|
+
name: 'platformType',
|
|
82
|
+
message: isFirst ? 'First platform' : `Platform #${platformNumber}`,
|
|
83
|
+
choices: [
|
|
84
|
+
{ title: 'Mattermost', value: 'mattermost' },
|
|
85
|
+
{ title: 'Slack', value: 'slack' },
|
|
86
|
+
...(isFirst ? [] : [{ title: '(Done - finish setup)', value: 'done' }]),
|
|
87
|
+
],
|
|
88
|
+
initial: existingPlatform?.type === 'slack' ? 1 : 0,
|
|
89
|
+
}, { onCancel });
|
|
90
|
+
if (platformType === 'done') {
|
|
91
|
+
addMore = false;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
// Get platform ID and name
|
|
95
|
+
const { platformId, displayName } = await prompts([
|
|
96
|
+
{
|
|
97
|
+
type: 'text',
|
|
98
|
+
name: 'platformId',
|
|
99
|
+
message: 'Platform ID',
|
|
100
|
+
initial: existingPlatform?.id ||
|
|
101
|
+
(config.platforms.length === 0 ? 'default' : `${platformType}-${platformNumber}`),
|
|
102
|
+
hint: 'Unique identifier (e.g., mattermost-main, slack-eng)',
|
|
103
|
+
validate: (v) => {
|
|
104
|
+
if (!v.match(/^[a-z0-9-]+$/))
|
|
105
|
+
return 'Use lowercase letters, numbers, hyphens only';
|
|
106
|
+
if (config.platforms.some(p => p.id === v))
|
|
107
|
+
return 'ID already in use';
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: 'text',
|
|
113
|
+
name: 'displayName',
|
|
114
|
+
message: 'Display name',
|
|
115
|
+
initial: existingPlatform?.displayName ||
|
|
116
|
+
(platformType === 'mattermost' ? 'Mattermost' : 'Slack'),
|
|
117
|
+
hint: 'Human-readable name (e.g., "Internal Team", "Engineering")',
|
|
118
|
+
},
|
|
119
|
+
], { onCancel });
|
|
120
|
+
// Configure the platform
|
|
121
|
+
if (platformType === 'mattermost') {
|
|
122
|
+
const platform = await setupMattermostPlatform(platformId, displayName, existingPlatform);
|
|
123
|
+
config.platforms.push(platform);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const platform = await setupSlackPlatform(platformId, displayName, existingPlatform);
|
|
127
|
+
config.platforms.push(platform);
|
|
128
|
+
}
|
|
129
|
+
console.log(green(` ✓ Added ${displayName}`));
|
|
36
130
|
console.log('');
|
|
37
|
-
|
|
131
|
+
// Ask to add more (after first one)
|
|
132
|
+
if (platformNumber === 1) {
|
|
133
|
+
const { addAnother } = await prompts({
|
|
134
|
+
type: 'confirm',
|
|
135
|
+
name: 'addAnother',
|
|
136
|
+
message: 'Add another platform?',
|
|
137
|
+
initial: (existingConfig?.platforms.length || 0) > 1,
|
|
138
|
+
}, { onCancel });
|
|
139
|
+
addMore = addAnother;
|
|
140
|
+
}
|
|
141
|
+
platformNumber++;
|
|
38
142
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
console.log(dim(' ─────────────────────────────────'));
|
|
143
|
+
// Validate at least one platform
|
|
144
|
+
if (config.platforms.length === 0) {
|
|
42
145
|
console.log('');
|
|
43
|
-
console.log(' No
|
|
146
|
+
console.log(dim(' ⚠️ No platforms configured. Setup cancelled.'));
|
|
147
|
+
process.exit(1);
|
|
44
148
|
}
|
|
149
|
+
// Save config
|
|
150
|
+
saveConfig(config);
|
|
45
151
|
console.log('');
|
|
46
|
-
console.log(
|
|
47
|
-
console.log(dim(
|
|
48
|
-
console.log(dim(' • A channel ID where the bot will listen'));
|
|
152
|
+
console.log(green(' ✓ Configuration saved!'));
|
|
153
|
+
console.log(dim(` ${CONFIG_PATH}`));
|
|
49
154
|
console.log('');
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return 0; // default to 'prompt'
|
|
64
|
-
};
|
|
155
|
+
console.log(dim(` Configured ${config.platforms.length} platform(s):`));
|
|
156
|
+
for (const platform of config.platforms) {
|
|
157
|
+
console.log(dim(` • ${platform.displayName} (${platform.type})`));
|
|
158
|
+
}
|
|
159
|
+
console.log('');
|
|
160
|
+
console.log(dim(' Starting claude-threads...'));
|
|
161
|
+
console.log('');
|
|
162
|
+
}
|
|
163
|
+
async function setupMattermostPlatform(id, displayName, existing) {
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(dim(' Mattermost setup:'));
|
|
166
|
+
console.log('');
|
|
167
|
+
const existingMattermost = existing?.type === 'mattermost' ? existing : undefined;
|
|
65
168
|
const response = await prompts([
|
|
66
169
|
{
|
|
67
170
|
type: 'text',
|
|
68
171
|
name: 'url',
|
|
69
|
-
message: '
|
|
70
|
-
initial:
|
|
71
|
-
validate: (v) => v.startsWith('http') ? true : '
|
|
172
|
+
message: 'Server URL',
|
|
173
|
+
initial: existingMattermost?.url || 'https://chat.example.com',
|
|
174
|
+
validate: (v) => v.startsWith('http') ? true : 'Must start with http(s)://',
|
|
72
175
|
},
|
|
73
176
|
{
|
|
74
177
|
type: 'password',
|
|
75
178
|
name: 'token',
|
|
76
179
|
message: 'Bot token',
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
: 'Create at: Integrations > Bot Accounts > Add Bot Account',
|
|
180
|
+
initial: existingMattermost?.token,
|
|
181
|
+
hint: existingMattermost?.token ? 'Enter to keep existing, or type new token' : 'Create at: Integrations > Bot Accounts',
|
|
80
182
|
validate: (v) => {
|
|
81
|
-
// Allow empty if we have
|
|
82
|
-
if (!v &&
|
|
183
|
+
// Allow empty if we have existing token
|
|
184
|
+
if (!v && existingMattermost?.token)
|
|
83
185
|
return true;
|
|
84
186
|
return v.length > 0 ? true : 'Token is required';
|
|
85
187
|
},
|
|
@@ -88,106 +190,129 @@ export async function runOnboarding(reconfigure = false) {
|
|
|
88
190
|
type: 'text',
|
|
89
191
|
name: 'channelId',
|
|
90
192
|
message: 'Channel ID',
|
|
91
|
-
initial:
|
|
92
|
-
hint: 'Click channel
|
|
193
|
+
initial: existingMattermost?.channelId || '',
|
|
194
|
+
hint: 'Click channel > View Info > copy ID from URL',
|
|
93
195
|
validate: (v) => v.length > 0 ? true : 'Channel ID is required',
|
|
94
196
|
},
|
|
95
197
|
{
|
|
96
198
|
type: 'text',
|
|
97
199
|
name: 'botName',
|
|
98
200
|
message: 'Bot mention name',
|
|
99
|
-
initial:
|
|
201
|
+
initial: existingMattermost?.botName || 'claude-code',
|
|
100
202
|
hint: 'Users will @mention this name',
|
|
101
203
|
},
|
|
102
204
|
{
|
|
103
205
|
type: 'text',
|
|
104
206
|
name: 'allowedUsers',
|
|
105
|
-
message: 'Allowed usernames',
|
|
106
|
-
initial:
|
|
107
|
-
hint: 'Comma-separated, or empty
|
|
207
|
+
message: 'Allowed usernames (optional)',
|
|
208
|
+
initial: existingMattermost?.allowedUsers?.join(',') || '',
|
|
209
|
+
hint: 'Comma-separated, or empty to allow everyone',
|
|
108
210
|
},
|
|
109
211
|
{
|
|
110
212
|
type: 'confirm',
|
|
111
213
|
name: 'skipPermissions',
|
|
112
|
-
message: '
|
|
113
|
-
initial:
|
|
114
|
-
|
|
115
|
-
: true,
|
|
116
|
-
hint: 'If no, you\'ll approve each action via emoji reactions',
|
|
214
|
+
message: 'Auto-approve all actions?',
|
|
215
|
+
initial: existingMattermost?.skipPermissions || false,
|
|
216
|
+
hint: 'If no, you\'ll approve via emoji reactions',
|
|
117
217
|
},
|
|
218
|
+
], { onCancel });
|
|
219
|
+
// Use existing token if user left it empty
|
|
220
|
+
const finalToken = response.token || existingMattermost?.token;
|
|
221
|
+
if (!finalToken) {
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log(dim(' ⚠️ Token is required. Setup cancelled.'));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
id,
|
|
228
|
+
type: 'mattermost',
|
|
229
|
+
displayName,
|
|
230
|
+
url: response.url,
|
|
231
|
+
token: finalToken,
|
|
232
|
+
channelId: response.channelId,
|
|
233
|
+
botName: response.botName,
|
|
234
|
+
allowedUsers: response.allowedUsers?.split(',').map((u) => u.trim()).filter((u) => u) || [],
|
|
235
|
+
skipPermissions: response.skipPermissions,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
async function setupSlackPlatform(id, displayName, existing) {
|
|
239
|
+
console.log('');
|
|
240
|
+
console.log(dim(' Slack setup (requires Socket Mode):'));
|
|
241
|
+
console.log(dim(' Create app at: api.slack.com/apps'));
|
|
242
|
+
console.log('');
|
|
243
|
+
const existingSlack = existing?.type === 'slack' ? existing : undefined;
|
|
244
|
+
const response = await prompts([
|
|
118
245
|
{
|
|
119
|
-
type: '
|
|
120
|
-
name: '
|
|
121
|
-
message: '
|
|
122
|
-
initial:
|
|
123
|
-
hint: '
|
|
246
|
+
type: 'password',
|
|
247
|
+
name: 'botToken',
|
|
248
|
+
message: 'Bot User OAuth Token',
|
|
249
|
+
initial: existingSlack?.botToken,
|
|
250
|
+
hint: existingSlack?.botToken ? 'Enter to keep existing' : 'Starts with xoxb-',
|
|
251
|
+
validate: (v) => {
|
|
252
|
+
if (!v && existingSlack?.botToken)
|
|
253
|
+
return true;
|
|
254
|
+
return v.startsWith('xoxb-') ? true : 'Must start with xoxb-';
|
|
255
|
+
},
|
|
124
256
|
},
|
|
125
257
|
{
|
|
126
|
-
type: '
|
|
127
|
-
name: '
|
|
128
|
-
message: '
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
258
|
+
type: 'password',
|
|
259
|
+
name: 'appToken',
|
|
260
|
+
message: 'App-Level Token',
|
|
261
|
+
initial: existingSlack?.appToken,
|
|
262
|
+
hint: existingSlack?.appToken ? 'Enter to keep existing' : 'Starts with xapp- (enable Socket Mode first)',
|
|
263
|
+
validate: (v) => {
|
|
264
|
+
if (!v && existingSlack?.appToken)
|
|
265
|
+
return true;
|
|
266
|
+
return v.startsWith('xapp-') ? true : 'Must start with xapp-';
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
type: 'text',
|
|
271
|
+
name: 'channelId',
|
|
272
|
+
message: 'Channel ID',
|
|
273
|
+
initial: existingSlack?.channelId || '',
|
|
274
|
+
hint: 'Right-click channel > View details > copy ID',
|
|
275
|
+
validate: (v) => v.length > 0 ? true : 'Channel ID is required',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
type: 'text',
|
|
279
|
+
name: 'botName',
|
|
280
|
+
message: 'Bot mention name',
|
|
281
|
+
initial: existingSlack?.botName || 'claude',
|
|
282
|
+
hint: 'Users will @mention this name',
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
type: 'text',
|
|
286
|
+
name: 'allowedUsers',
|
|
287
|
+
message: 'Allowed usernames (optional)',
|
|
288
|
+
initial: existingSlack?.allowedUsers?.join(',') || '',
|
|
289
|
+
hint: 'Comma-separated, or empty for everyone',
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
type: 'confirm',
|
|
293
|
+
name: 'skipPermissions',
|
|
294
|
+
message: 'Auto-approve all actions?',
|
|
295
|
+
initial: existingSlack?.skipPermissions || false,
|
|
296
|
+
hint: 'If no, you\'ll approve via emoji reactions',
|
|
136
297
|
},
|
|
137
298
|
], { onCancel });
|
|
138
|
-
//
|
|
139
|
-
const
|
|
140
|
-
|
|
299
|
+
// Use existing tokens if user left them empty
|
|
300
|
+
const finalBotToken = response.botToken || existingSlack?.botToken;
|
|
301
|
+
const finalAppToken = response.appToken || existingSlack?.appToken;
|
|
302
|
+
if (!finalBotToken || !finalAppToken) {
|
|
141
303
|
console.log('');
|
|
142
|
-
console.log(dim('
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
145
|
-
// Build .env content
|
|
146
|
-
const envContent = `# claude-threads configuration
|
|
147
|
-
# Generated by claude-threads onboarding
|
|
148
|
-
|
|
149
|
-
# Mattermost server URL
|
|
150
|
-
MATTERMOST_URL=${response.url}
|
|
151
|
-
|
|
152
|
-
# Bot token (from Integrations > Bot Accounts)
|
|
153
|
-
MATTERMOST_TOKEN=${finalToken}
|
|
154
|
-
|
|
155
|
-
# Channel ID where the bot listens
|
|
156
|
-
MATTERMOST_CHANNEL_ID=${response.channelId}
|
|
157
|
-
|
|
158
|
-
# Bot mention name (users @mention this)
|
|
159
|
-
MATTERMOST_BOT_NAME=${response.botName || 'claude-code'}
|
|
160
|
-
|
|
161
|
-
# Allowed usernames (comma-separated, empty = all users)
|
|
162
|
-
ALLOWED_USERS=${response.allowedUsers || ''}
|
|
163
|
-
|
|
164
|
-
# Skip permission prompts (true = auto-approve, false = require emoji approval)
|
|
165
|
-
SKIP_PERMISSIONS=${response.skipPermissions ? 'true' : 'false'}
|
|
166
|
-
|
|
167
|
-
# Chrome integration (requires Claude in Chrome extension)
|
|
168
|
-
CLAUDE_CHROME=${response.chrome ? 'true' : 'false'}
|
|
169
|
-
|
|
170
|
-
# Git worktree mode (off, prompt, require)
|
|
171
|
-
WORKTREE_MODE=${response.worktreeMode || 'prompt'}
|
|
172
|
-
`;
|
|
173
|
-
// Save to same location if reconfiguring, otherwise default location
|
|
174
|
-
const defaultConfigDir = resolve(homedir(), '.config', 'claude-threads');
|
|
175
|
-
const defaultEnvPath = resolve(defaultConfigDir, '.env');
|
|
176
|
-
const envPath = existingPath || defaultEnvPath;
|
|
177
|
-
const configDir = dirname(envPath);
|
|
178
|
-
try {
|
|
179
|
-
mkdirSync(configDir, { recursive: true });
|
|
180
|
-
writeFileSync(envPath, envContent, { mode: 0o600 }); // Secure permissions
|
|
181
|
-
}
|
|
182
|
-
catch (err) {
|
|
183
|
-
console.error('');
|
|
184
|
-
console.error(` Failed to save config: ${err}`);
|
|
304
|
+
console.log(dim(' ⚠️ Both tokens are required. Setup cancelled.'));
|
|
185
305
|
process.exit(1);
|
|
186
306
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
307
|
+
return {
|
|
308
|
+
id,
|
|
309
|
+
type: 'slack',
|
|
310
|
+
displayName,
|
|
311
|
+
botToken: finalBotToken,
|
|
312
|
+
appToken: finalAppToken,
|
|
313
|
+
channelId: response.channelId,
|
|
314
|
+
botName: response.botName,
|
|
315
|
+
allowedUsers: response.allowedUsers?.split(',').map((u) => u.trim()).filter((u) => u) || [],
|
|
316
|
+
skipPermissions: response.skipPermissions,
|
|
317
|
+
};
|
|
193
318
|
}
|
|
@@ -10,6 +10,7 @@ export interface WorktreeInfo {
|
|
|
10
10
|
* Persisted session state for resuming after bot restart
|
|
11
11
|
*/
|
|
12
12
|
export interface PersistedSession {
|
|
13
|
+
platformId: string;
|
|
13
14
|
threadId: string;
|
|
14
15
|
claudeSessionId: string;
|
|
15
16
|
startedBy: string;
|
|
@@ -36,18 +37,23 @@ export declare class SessionStore {
|
|
|
36
37
|
constructor();
|
|
37
38
|
/**
|
|
38
39
|
* Load all persisted sessions
|
|
40
|
+
* Returns Map with composite sessionId ("platformId:threadId") as key
|
|
39
41
|
*/
|
|
40
42
|
load(): Map<string, PersistedSession>;
|
|
41
43
|
/**
|
|
42
44
|
* Save a session (creates or updates)
|
|
45
|
+
* @param sessionId - Composite key "platformId:threadId"
|
|
46
|
+
* @param session - Session data to persist
|
|
43
47
|
*/
|
|
44
|
-
save(
|
|
48
|
+
save(sessionId: string, session: PersistedSession): void;
|
|
45
49
|
/**
|
|
46
50
|
* Remove a session
|
|
51
|
+
* @param sessionId - Composite key "platformId:threadId"
|
|
47
52
|
*/
|
|
48
|
-
remove(
|
|
53
|
+
remove(sessionId: string): void;
|
|
49
54
|
/**
|
|
50
55
|
* Remove sessions older than maxAgeMs
|
|
56
|
+
* @returns Array of sessionIds that were removed
|
|
51
57
|
*/
|
|
52
58
|
cleanStale(maxAgeMs: number): string[];
|
|
53
59
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'fs';
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
const STORE_VERSION =
|
|
4
|
+
const STORE_VERSION = 2; // v2: Added platformId for multi-platform support
|
|
5
5
|
const CONFIG_DIR = join(homedir(), '.config', 'claude-threads');
|
|
6
6
|
const SESSIONS_FILE = join(CONFIG_DIR, 'sessions.json');
|
|
7
7
|
/**
|
|
@@ -18,6 +18,7 @@ export class SessionStore {
|
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Load all persisted sessions
|
|
21
|
+
* Returns Map with composite sessionId ("platformId:threadId") as key
|
|
21
22
|
*/
|
|
22
23
|
load() {
|
|
23
24
|
const sessions = new Map();
|
|
@@ -28,13 +29,32 @@ export class SessionStore {
|
|
|
28
29
|
}
|
|
29
30
|
try {
|
|
30
31
|
const data = JSON.parse(readFileSync(SESSIONS_FILE, 'utf-8'));
|
|
31
|
-
//
|
|
32
|
-
if (data.version
|
|
33
|
-
console.
|
|
32
|
+
// Migration: v1 → v2 (add platformId and convert keys to composite format)
|
|
33
|
+
if (data.version === 1) {
|
|
34
|
+
console.log(' [persist] Migrating sessions from v1 to v2 (adding platformId)');
|
|
35
|
+
const newSessions = {};
|
|
36
|
+
for (const [_oldKey, session] of Object.entries(data.sessions)) {
|
|
37
|
+
const v1Session = session;
|
|
38
|
+
if (!v1Session.platformId) {
|
|
39
|
+
v1Session.platformId = 'default';
|
|
40
|
+
}
|
|
41
|
+
// Convert key from threadId to platformId:threadId
|
|
42
|
+
const newKey = `${v1Session.platformId}:${v1Session.threadId}`;
|
|
43
|
+
newSessions[newKey] = v1Session;
|
|
44
|
+
}
|
|
45
|
+
data.sessions = newSessions;
|
|
46
|
+
data.version = 2;
|
|
47
|
+
// Save migrated data
|
|
48
|
+
this.writeAtomic(data);
|
|
49
|
+
}
|
|
50
|
+
else if (data.version !== STORE_VERSION) {
|
|
51
|
+
console.warn(` [persist] Sessions file version ${data.version} not supported, starting fresh`);
|
|
34
52
|
return sessions;
|
|
35
53
|
}
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
// Load sessions with composite sessionId as key
|
|
55
|
+
for (const session of Object.values(data.sessions)) {
|
|
56
|
+
const sessionId = `${session.platformId}:${session.threadId}`;
|
|
57
|
+
sessions.set(sessionId, session);
|
|
38
58
|
}
|
|
39
59
|
if (this.debug) {
|
|
40
60
|
console.log(` [persist] Loaded ${sessions.size} session(s)`);
|
|
@@ -47,42 +67,47 @@ export class SessionStore {
|
|
|
47
67
|
}
|
|
48
68
|
/**
|
|
49
69
|
* Save a session (creates or updates)
|
|
70
|
+
* @param sessionId - Composite key "platformId:threadId"
|
|
71
|
+
* @param session - Session data to persist
|
|
50
72
|
*/
|
|
51
|
-
save(
|
|
73
|
+
save(sessionId, session) {
|
|
52
74
|
const data = this.loadRaw();
|
|
53
|
-
|
|
75
|
+
// Use sessionId as key (already composite)
|
|
76
|
+
data.sessions[sessionId] = session;
|
|
54
77
|
this.writeAtomic(data);
|
|
55
78
|
if (this.debug) {
|
|
56
|
-
const shortId =
|
|
79
|
+
const shortId = sessionId.substring(0, 20);
|
|
57
80
|
console.log(` [persist] Saved session ${shortId}...`);
|
|
58
81
|
}
|
|
59
82
|
}
|
|
60
83
|
/**
|
|
61
84
|
* Remove a session
|
|
85
|
+
* @param sessionId - Composite key "platformId:threadId"
|
|
62
86
|
*/
|
|
63
|
-
remove(
|
|
87
|
+
remove(sessionId) {
|
|
64
88
|
const data = this.loadRaw();
|
|
65
|
-
if (data.sessions[
|
|
66
|
-
delete data.sessions[
|
|
89
|
+
if (data.sessions[sessionId]) {
|
|
90
|
+
delete data.sessions[sessionId];
|
|
67
91
|
this.writeAtomic(data);
|
|
68
92
|
if (this.debug) {
|
|
69
|
-
const shortId =
|
|
93
|
+
const shortId = sessionId.substring(0, 20);
|
|
70
94
|
console.log(` [persist] Removed session ${shortId}...`);
|
|
71
95
|
}
|
|
72
96
|
}
|
|
73
97
|
}
|
|
74
98
|
/**
|
|
75
99
|
* Remove sessions older than maxAgeMs
|
|
100
|
+
* @returns Array of sessionIds that were removed
|
|
76
101
|
*/
|
|
77
102
|
cleanStale(maxAgeMs) {
|
|
78
103
|
const data = this.loadRaw();
|
|
79
104
|
const now = Date.now();
|
|
80
105
|
const staleIds = [];
|
|
81
|
-
for (const [
|
|
106
|
+
for (const [sessionId, session] of Object.entries(data.sessions)) {
|
|
82
107
|
const lastActivity = new Date(session.lastActivityAt).getTime();
|
|
83
108
|
if (now - lastActivity > maxAgeMs) {
|
|
84
|
-
staleIds.push(
|
|
85
|
-
delete data.sessions[
|
|
109
|
+
staleIds.push(sessionId);
|
|
110
|
+
delete data.sessions[sessionId];
|
|
86
111
|
}
|
|
87
112
|
}
|
|
88
113
|
if (staleIds.length > 0) {
|