claude-threads 0.29.0 → 0.31.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 +19 -0
- package/dist/index.js +472 -185
- package/dist/statusline/writer.js +48 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.31.0] - 2026-01-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Session history retention** - Sessions are now soft-deleted instead of permanently removed when they complete. Session history is kept for display in the sticky message (up to 5 recent sessions). Old history is permanently cleaned up after 3 days.
|
|
14
|
+
- **Git branch in session header** - Display the current git branch in the session header table when working in a git repository, providing visibility into which branch the session is operating on.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Accurate context usage via status line** - Uses Claude Code's status line feature to get accurate context window usage percentage instead of cumulative billing tokens. Adds a status line writer script that receives accurate per-request token data.
|
|
18
|
+
|
|
19
|
+
## [0.30.0] - 2026-01-03
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Pull request link detection** - When a session is working in a git worktree with an associated PR, the session header and sticky message now display a clickable link to the PR. Automatically detects PRs from GitHub URLs in branch names or upstream tracking.
|
|
23
|
+
- **User existence validation for invite/kick** - The `!invite` and `!kick` commands now validate that the user exists on the platform before attempting the action, providing helpful error messages for non-existent users.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **Accurate context window usage** - Now uses per-request usage data from Claude's result events instead of cumulative billing tokens, providing accurate context window percentage display.
|
|
27
|
+
- **Cancelled sessions no longer resume** - Fixed bug where cancelled sessions (killed by user) would incorrectly resume on bot restart by using the correct composite session key for unpersisting.
|
|
28
|
+
|
|
10
29
|
## [0.29.0] - 2026-01-03
|
|
11
30
|
|
|
12
31
|
### Changed
|
package/dist/index.js
CHANGED
|
@@ -13394,6 +13394,15 @@ class MattermostClient extends EventEmitter {
|
|
|
13394
13394
|
return null;
|
|
13395
13395
|
}
|
|
13396
13396
|
}
|
|
13397
|
+
async getUserByUsername(username) {
|
|
13398
|
+
try {
|
|
13399
|
+
const user = await this.api("GET", `/users/username/${username}`);
|
|
13400
|
+
this.userCache.set(user.id, user);
|
|
13401
|
+
return this.normalizePlatformUser(user);
|
|
13402
|
+
} catch {
|
|
13403
|
+
return null;
|
|
13404
|
+
}
|
|
13405
|
+
}
|
|
13397
13406
|
async createPost(message, threadId) {
|
|
13398
13407
|
const request = {
|
|
13399
13408
|
channel_id: this.channelId,
|
|
@@ -13713,10 +13722,12 @@ class SessionStore {
|
|
|
13713
13722
|
return sessions;
|
|
13714
13723
|
}
|
|
13715
13724
|
for (const session of Object.values(data.sessions)) {
|
|
13725
|
+
if (session.cleanedAt)
|
|
13726
|
+
continue;
|
|
13716
13727
|
const sessionId = `${session.platformId}:${session.threadId}`;
|
|
13717
13728
|
sessions.set(sessionId, session);
|
|
13718
13729
|
}
|
|
13719
|
-
log3.debug(`Loaded ${sessions.size} session(s)`);
|
|
13730
|
+
log3.debug(`Loaded ${sessions.size} active session(s)`);
|
|
13720
13731
|
} catch (err) {
|
|
13721
13732
|
log3.error(`Failed to load sessions: ${err}`);
|
|
13722
13733
|
}
|
|
@@ -13738,23 +13749,67 @@ class SessionStore {
|
|
|
13738
13749
|
log3.debug(`Removed session ${shortId}...`);
|
|
13739
13750
|
}
|
|
13740
13751
|
}
|
|
13752
|
+
softDelete(sessionId) {
|
|
13753
|
+
const data = this.loadRaw();
|
|
13754
|
+
if (data.sessions[sessionId]) {
|
|
13755
|
+
data.sessions[sessionId].cleanedAt = new Date().toISOString();
|
|
13756
|
+
this.writeAtomic(data);
|
|
13757
|
+
const shortId = sessionId.substring(0, 20);
|
|
13758
|
+
log3.debug(`Soft-deleted session ${shortId}...`);
|
|
13759
|
+
}
|
|
13760
|
+
}
|
|
13741
13761
|
cleanStale(maxAgeMs) {
|
|
13742
13762
|
const data = this.loadRaw();
|
|
13743
13763
|
const now = Date.now();
|
|
13744
13764
|
const staleIds = [];
|
|
13745
13765
|
for (const [sessionId, session] of Object.entries(data.sessions)) {
|
|
13766
|
+
if (session.cleanedAt)
|
|
13767
|
+
continue;
|
|
13746
13768
|
const lastActivity = new Date(session.lastActivityAt).getTime();
|
|
13747
13769
|
if (now - lastActivity > maxAgeMs) {
|
|
13748
13770
|
staleIds.push(sessionId);
|
|
13749
|
-
|
|
13771
|
+
session.cleanedAt = new Date().toISOString();
|
|
13750
13772
|
}
|
|
13751
13773
|
}
|
|
13752
13774
|
if (staleIds.length > 0) {
|
|
13753
13775
|
this.writeAtomic(data);
|
|
13754
|
-
log3.debug(`
|
|
13776
|
+
log3.debug(`Soft-deleted ${staleIds.length} stale session(s)`);
|
|
13755
13777
|
}
|
|
13756
13778
|
return staleIds;
|
|
13757
13779
|
}
|
|
13780
|
+
cleanHistory(historyRetentionMs = 3 * 24 * 60 * 60 * 1000) {
|
|
13781
|
+
const data = this.loadRaw();
|
|
13782
|
+
const now = Date.now();
|
|
13783
|
+
let removedCount = 0;
|
|
13784
|
+
for (const [sessionId, session] of Object.entries(data.sessions)) {
|
|
13785
|
+
if (!session.cleanedAt)
|
|
13786
|
+
continue;
|
|
13787
|
+
const cleanedTime = new Date(session.cleanedAt).getTime();
|
|
13788
|
+
if (now - cleanedTime > historyRetentionMs) {
|
|
13789
|
+
delete data.sessions[sessionId];
|
|
13790
|
+
removedCount++;
|
|
13791
|
+
}
|
|
13792
|
+
}
|
|
13793
|
+
if (removedCount > 0) {
|
|
13794
|
+
this.writeAtomic(data);
|
|
13795
|
+
log3.debug(`Permanently removed ${removedCount} old session(s) from history`);
|
|
13796
|
+
}
|
|
13797
|
+
return removedCount;
|
|
13798
|
+
}
|
|
13799
|
+
getHistory(platformId) {
|
|
13800
|
+
const data = this.loadRaw();
|
|
13801
|
+
const historySessions = [];
|
|
13802
|
+
for (const session of Object.values(data.sessions)) {
|
|
13803
|
+
if (session.platformId === platformId && session.cleanedAt) {
|
|
13804
|
+
historySessions.push(session);
|
|
13805
|
+
}
|
|
13806
|
+
}
|
|
13807
|
+
return historySessions.sort((a, b) => {
|
|
13808
|
+
const aTime = new Date(a.cleanedAt).getTime();
|
|
13809
|
+
const bTime = new Date(b.cleanedAt).getTime();
|
|
13810
|
+
return bTime - aTime;
|
|
13811
|
+
});
|
|
13812
|
+
}
|
|
13758
13813
|
clear() {
|
|
13759
13814
|
const data = this.loadRaw();
|
|
13760
13815
|
this.writeAtomic({ version: STORE_VERSION, sessions: {}, stickyPostIds: data.stickyPostIds });
|
|
@@ -15014,6 +15069,65 @@ async function logAndNotify(error2, context) {
|
|
|
15014
15069
|
await handleError(error2, { ...context, notifyUser: true }, "recoverable");
|
|
15015
15070
|
}
|
|
15016
15071
|
|
|
15072
|
+
// src/utils/pr-detector.ts
|
|
15073
|
+
var PR_PATTERNS = [
|
|
15074
|
+
{
|
|
15075
|
+
platform: "github",
|
|
15076
|
+
pattern: /https?:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/gi
|
|
15077
|
+
},
|
|
15078
|
+
{
|
|
15079
|
+
platform: "gitlab",
|
|
15080
|
+
pattern: /https?:\/\/[^/]*gitlab[^/]*\/([^/]+(?:\/[^/]+)+)\/-\/merge_requests\/(\d+)/gi
|
|
15081
|
+
},
|
|
15082
|
+
{
|
|
15083
|
+
platform: "bitbucket",
|
|
15084
|
+
pattern: /https?:\/\/bitbucket\.org\/([^/]+\/[^/]+)\/pull-requests\/(\d+)/gi
|
|
15085
|
+
},
|
|
15086
|
+
{
|
|
15087
|
+
platform: "azure",
|
|
15088
|
+
pattern: /https?:\/\/dev\.azure\.com\/([^/]+\/[^/]+\/_git\/[^/]+)\/pullrequest\/(\d+)/gi
|
|
15089
|
+
},
|
|
15090
|
+
{
|
|
15091
|
+
platform: "azure",
|
|
15092
|
+
pattern: /https?:\/\/[^/]+\.visualstudio\.com\/([^/]+\/_git\/[^/]+)\/pullrequest\/(\d+)/gi
|
|
15093
|
+
}
|
|
15094
|
+
];
|
|
15095
|
+
function detectPullRequests(text) {
|
|
15096
|
+
const results = [];
|
|
15097
|
+
const seenUrls = new Set;
|
|
15098
|
+
for (const { platform, pattern } of PR_PATTERNS) {
|
|
15099
|
+
pattern.lastIndex = 0;
|
|
15100
|
+
let match;
|
|
15101
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
15102
|
+
const url = match[0];
|
|
15103
|
+
if (seenUrls.has(url))
|
|
15104
|
+
continue;
|
|
15105
|
+
seenUrls.add(url);
|
|
15106
|
+
results.push({
|
|
15107
|
+
url,
|
|
15108
|
+
platform,
|
|
15109
|
+
repo: match[1],
|
|
15110
|
+
number: match[2]
|
|
15111
|
+
});
|
|
15112
|
+
}
|
|
15113
|
+
}
|
|
15114
|
+
return results;
|
|
15115
|
+
}
|
|
15116
|
+
function extractPullRequestUrl(text) {
|
|
15117
|
+
const prs = detectPullRequests(text);
|
|
15118
|
+
return prs.length > 0 ? prs[0].url : null;
|
|
15119
|
+
}
|
|
15120
|
+
function formatPullRequestLink(url) {
|
|
15121
|
+
const prs = detectPullRequests(url);
|
|
15122
|
+
if (prs.length === 0)
|
|
15123
|
+
return url;
|
|
15124
|
+
const pr = prs[0];
|
|
15125
|
+
if (pr.platform === "gitlab") {
|
|
15126
|
+
return `[\uD83D\uDD17 MR !${pr.number}](${url})`;
|
|
15127
|
+
}
|
|
15128
|
+
return `[\uD83D\uDD17 PR #${pr.number}](${url})`;
|
|
15129
|
+
}
|
|
15130
|
+
|
|
15017
15131
|
// src/session/events.ts
|
|
15018
15132
|
var log6 = createLogger("events");
|
|
15019
15133
|
function extractAndUpdateMetadata(text, session, config, sessionField, ctx) {
|
|
@@ -15044,6 +15158,18 @@ var DESCRIPTION_CONFIG = {
|
|
|
15044
15158
|
maxLength: 100,
|
|
15045
15159
|
placeholder: "<brief description>"
|
|
15046
15160
|
};
|
|
15161
|
+
function extractAndUpdatePullRequest(text, session, ctx) {
|
|
15162
|
+
if (session.pullRequestUrl)
|
|
15163
|
+
return;
|
|
15164
|
+
const prUrl = extractPullRequestUrl(text);
|
|
15165
|
+
if (prUrl) {
|
|
15166
|
+
session.pullRequestUrl = prUrl;
|
|
15167
|
+
log6.info(`\uD83D\uDD17 Detected PR URL: ${prUrl}`);
|
|
15168
|
+
ctx.ops.persistSession(session);
|
|
15169
|
+
ctx.ops.updateStickyMessage().catch(() => {});
|
|
15170
|
+
ctx.ops.updateSessionHeader(session).catch(() => {});
|
|
15171
|
+
}
|
|
15172
|
+
}
|
|
15047
15173
|
function handleEvent(session, event, ctx) {
|
|
15048
15174
|
session.lastActivityAt = new Date;
|
|
15049
15175
|
session.timeoutWarningPosted = false;
|
|
@@ -15100,6 +15226,7 @@ function formatEvent(session, e, ctx) {
|
|
|
15100
15226
|
let text = block.text.replace(/<thinking>[\s\S]*?<\/thinking>/g, "").trim();
|
|
15101
15227
|
text = extractAndUpdateMetadata(text, session, TITLE_CONFIG, "sessionTitle", ctx);
|
|
15102
15228
|
text = extractAndUpdateMetadata(text, session, DESCRIPTION_CONFIG, "sessionDescription", ctx);
|
|
15229
|
+
extractAndUpdatePullRequest(text, session, ctx);
|
|
15103
15230
|
if (text)
|
|
15104
15231
|
parts.push(text);
|
|
15105
15232
|
} else if (block.type === "tool_use" && block.name) {
|
|
@@ -15380,7 +15507,6 @@ function updateUsageStats(session, event, ctx) {
|
|
|
15380
15507
|
let primaryModel = "";
|
|
15381
15508
|
let highestCost = 0;
|
|
15382
15509
|
let contextWindowSize = 200000;
|
|
15383
|
-
let contextTokens = 0;
|
|
15384
15510
|
const modelUsage = {};
|
|
15385
15511
|
let totalTokensUsed = 0;
|
|
15386
15512
|
for (const [modelId, usage] of Object.entries(result.modelUsage)) {
|
|
@@ -15397,9 +15523,15 @@ function updateUsageStats(session, event, ctx) {
|
|
|
15397
15523
|
highestCost = usage.costUSD;
|
|
15398
15524
|
primaryModel = modelId;
|
|
15399
15525
|
contextWindowSize = usage.contextWindow;
|
|
15400
|
-
contextTokens = usage.inputTokens + usage.cacheReadInputTokens;
|
|
15401
15526
|
}
|
|
15402
15527
|
}
|
|
15528
|
+
let contextTokens = 0;
|
|
15529
|
+
if (result.usage) {
|
|
15530
|
+
contextTokens = result.usage.input_tokens + result.usage.cache_creation_input_tokens + result.usage.cache_read_input_tokens;
|
|
15531
|
+
} else if (primaryModel && result.modelUsage[primaryModel]) {
|
|
15532
|
+
const primary = result.modelUsage[primaryModel];
|
|
15533
|
+
contextTokens = primary.inputTokens + primary.cacheReadInputTokens;
|
|
15534
|
+
}
|
|
15403
15535
|
const usageStats = {
|
|
15404
15536
|
primaryModel,
|
|
15405
15537
|
modelDisplayName: getModelDisplayName(primaryModel),
|
|
@@ -15411,17 +15543,41 @@ function updateUsageStats(session, event, ctx) {
|
|
|
15411
15543
|
lastUpdated: new Date
|
|
15412
15544
|
};
|
|
15413
15545
|
session.usageStats = usageStats;
|
|
15414
|
-
|
|
15546
|
+
const contextPct = contextWindowSize > 0 ? Math.round(contextTokens / contextWindowSize * 100) : 0;
|
|
15547
|
+
log6.debug(`Updated usage stats: ${usageStats.modelDisplayName}, ` + `context ${contextTokens}/${contextWindowSize} (${contextPct}%), ` + `$${usageStats.totalCostUSD.toFixed(4)}`);
|
|
15415
15548
|
if (!session.statusBarTimer) {
|
|
15416
15549
|
const STATUS_BAR_UPDATE_INTERVAL = 30000;
|
|
15417
15550
|
session.statusBarTimer = setInterval(() => {
|
|
15418
15551
|
if (session.claude.isRunning()) {
|
|
15552
|
+
updateUsageFromStatusLine(session);
|
|
15419
15553
|
ctx.ops.updateSessionHeader(session).catch(() => {});
|
|
15420
15554
|
}
|
|
15421
15555
|
}, STATUS_BAR_UPDATE_INTERVAL);
|
|
15422
15556
|
}
|
|
15423
15557
|
ctx.ops.updateSessionHeader(session).catch(() => {});
|
|
15424
15558
|
}
|
|
15559
|
+
function updateUsageFromStatusLine(session) {
|
|
15560
|
+
const statusData = session.claude.getStatusData();
|
|
15561
|
+
if (!statusData || !statusData.current_usage)
|
|
15562
|
+
return;
|
|
15563
|
+
if (!session.usageStats)
|
|
15564
|
+
return;
|
|
15565
|
+
const contextTokens = statusData.current_usage.input_tokens + statusData.current_usage.cache_creation_input_tokens + statusData.current_usage.cache_read_input_tokens;
|
|
15566
|
+
if (statusData.timestamp > session.usageStats.lastUpdated.getTime()) {
|
|
15567
|
+
session.usageStats.contextTokens = contextTokens;
|
|
15568
|
+
session.usageStats.contextWindowSize = statusData.context_window_size;
|
|
15569
|
+
session.usageStats.lastUpdated = new Date(statusData.timestamp);
|
|
15570
|
+
if (statusData.model) {
|
|
15571
|
+
session.usageStats.primaryModel = statusData.model.id;
|
|
15572
|
+
session.usageStats.modelDisplayName = statusData.model.display_name;
|
|
15573
|
+
}
|
|
15574
|
+
if (statusData.cost) {
|
|
15575
|
+
session.usageStats.totalCostUSD = statusData.cost.total_cost_usd;
|
|
15576
|
+
}
|
|
15577
|
+
const contextPct = session.usageStats.contextWindowSize > 0 ? Math.round(contextTokens / session.usageStats.contextWindowSize * 100) : 0;
|
|
15578
|
+
log6.debug(`Updated from status line: context ${contextTokens}/${session.usageStats.contextWindowSize} (${contextPct}%)`);
|
|
15579
|
+
}
|
|
15580
|
+
}
|
|
15425
15581
|
|
|
15426
15582
|
// src/session/reactions.ts
|
|
15427
15583
|
var log7 = createLogger("reactions");
|
|
@@ -15573,6 +15729,7 @@ import { spawn } from "child_process";
|
|
|
15573
15729
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
15574
15730
|
import { resolve as resolve2, dirname as dirname2 } from "path";
|
|
15575
15731
|
import { fileURLToPath } from "url";
|
|
15732
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, watchFile, unwatchFile, unlinkSync } from "fs";
|
|
15576
15733
|
var log8 = createLogger("claude");
|
|
15577
15734
|
|
|
15578
15735
|
class ClaudeCli extends EventEmitter2 {
|
|
@@ -15580,10 +15737,48 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
15580
15737
|
options;
|
|
15581
15738
|
buffer = "";
|
|
15582
15739
|
debug = process.env.DEBUG === "1" || process.argv.includes("--debug");
|
|
15740
|
+
statusFilePath = null;
|
|
15741
|
+
lastStatusData = null;
|
|
15583
15742
|
constructor(options) {
|
|
15584
15743
|
super();
|
|
15585
15744
|
this.options = options;
|
|
15586
15745
|
}
|
|
15746
|
+
getStatusFilePath() {
|
|
15747
|
+
return this.statusFilePath;
|
|
15748
|
+
}
|
|
15749
|
+
getStatusData() {
|
|
15750
|
+
if (!this.statusFilePath)
|
|
15751
|
+
return null;
|
|
15752
|
+
try {
|
|
15753
|
+
if (existsSync4(this.statusFilePath)) {
|
|
15754
|
+
const data = readFileSync4(this.statusFilePath, "utf8");
|
|
15755
|
+
this.lastStatusData = JSON.parse(data);
|
|
15756
|
+
}
|
|
15757
|
+
} catch {}
|
|
15758
|
+
return this.lastStatusData;
|
|
15759
|
+
}
|
|
15760
|
+
startStatusWatch() {
|
|
15761
|
+
if (!this.statusFilePath)
|
|
15762
|
+
return;
|
|
15763
|
+
const checkStatus = () => {
|
|
15764
|
+
const data = this.getStatusData();
|
|
15765
|
+
if (data && data.timestamp !== this.lastStatusData?.timestamp) {
|
|
15766
|
+
this.lastStatusData = data;
|
|
15767
|
+
this.emit("status", data);
|
|
15768
|
+
}
|
|
15769
|
+
};
|
|
15770
|
+
watchFile(this.statusFilePath, { interval: 1000 }, checkStatus);
|
|
15771
|
+
}
|
|
15772
|
+
stopStatusWatch() {
|
|
15773
|
+
if (this.statusFilePath) {
|
|
15774
|
+
unwatchFile(this.statusFilePath);
|
|
15775
|
+
try {
|
|
15776
|
+
if (existsSync4(this.statusFilePath)) {
|
|
15777
|
+
unlinkSync(this.statusFilePath);
|
|
15778
|
+
}
|
|
15779
|
+
} catch {}
|
|
15780
|
+
}
|
|
15781
|
+
}
|
|
15587
15782
|
start() {
|
|
15588
15783
|
if (this.process)
|
|
15589
15784
|
throw new Error("Already running");
|
|
@@ -15638,6 +15833,18 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
15638
15833
|
if (this.options.appendSystemPrompt) {
|
|
15639
15834
|
args.push("--append-system-prompt", this.options.appendSystemPrompt);
|
|
15640
15835
|
}
|
|
15836
|
+
if (this.options.sessionId) {
|
|
15837
|
+
this.statusFilePath = `/tmp/claude-threads-status-${this.options.sessionId}.json`;
|
|
15838
|
+
const statusLineWriterPath = this.getStatusLineWriterPath();
|
|
15839
|
+
const statusLineSettings = {
|
|
15840
|
+
statusLine: {
|
|
15841
|
+
type: "command",
|
|
15842
|
+
command: `node ${statusLineWriterPath} ${this.options.sessionId}`,
|
|
15843
|
+
padding: 0
|
|
15844
|
+
}
|
|
15845
|
+
};
|
|
15846
|
+
args.push("--settings", JSON.stringify(statusLineSettings));
|
|
15847
|
+
}
|
|
15641
15848
|
log8.debug(`Starting: ${claudePath} ${args.slice(0, 5).join(" ")}...`);
|
|
15642
15849
|
this.process = spawn(claudePath, args, {
|
|
15643
15850
|
cwd: this.options.workingDir,
|
|
@@ -15713,6 +15920,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
15713
15920
|
return this.process !== null;
|
|
15714
15921
|
}
|
|
15715
15922
|
kill() {
|
|
15923
|
+
this.stopStatusWatch();
|
|
15716
15924
|
this.process?.kill("SIGTERM");
|
|
15717
15925
|
this.process = null;
|
|
15718
15926
|
}
|
|
@@ -15727,12 +15935,17 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
15727
15935
|
const __dirname2 = dirname2(__filename2);
|
|
15728
15936
|
return resolve2(__dirname2, "..", "mcp", "permission-server.js");
|
|
15729
15937
|
}
|
|
15938
|
+
getStatusLineWriterPath() {
|
|
15939
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
15940
|
+
const __dirname2 = dirname2(__filename2);
|
|
15941
|
+
return resolve2(__dirname2, "..", "statusline", "writer.js");
|
|
15942
|
+
}
|
|
15730
15943
|
}
|
|
15731
15944
|
|
|
15732
15945
|
// src/session/commands.ts
|
|
15733
|
-
import { randomUUID } from "crypto";
|
|
15946
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
15734
15947
|
import { resolve as resolve5 } from "path";
|
|
15735
|
-
import { existsSync as
|
|
15948
|
+
import { existsSync as existsSync7, statSync } from "fs";
|
|
15736
15949
|
|
|
15737
15950
|
// node_modules/update-notifier/update-notifier.js
|
|
15738
15951
|
import process10 from "process";
|
|
@@ -19023,7 +19236,7 @@ function updateNotifier(options) {
|
|
|
19023
19236
|
var import_semver2 = __toESM(require_semver2(), 1);
|
|
19024
19237
|
|
|
19025
19238
|
// src/version.ts
|
|
19026
|
-
import { readFileSync as
|
|
19239
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
19027
19240
|
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
19028
19241
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
19029
19242
|
var __dirname4 = dirname3(fileURLToPath4(import.meta.url));
|
|
@@ -19034,9 +19247,9 @@ function loadPackageJson() {
|
|
|
19034
19247
|
resolve3(process.cwd(), "package.json")
|
|
19035
19248
|
];
|
|
19036
19249
|
for (const candidate of candidates) {
|
|
19037
|
-
if (
|
|
19250
|
+
if (existsSync5(candidate)) {
|
|
19038
19251
|
try {
|
|
19039
|
-
const pkg = JSON.parse(
|
|
19252
|
+
const pkg = JSON.parse(readFileSync5(candidate, "utf-8"));
|
|
19040
19253
|
if (pkg.name === "claude-threads") {
|
|
19041
19254
|
return { version: pkg.version, name: pkg.name };
|
|
19042
19255
|
}
|
|
@@ -19079,7 +19292,7 @@ function getUpdateInfo() {
|
|
|
19079
19292
|
}
|
|
19080
19293
|
|
|
19081
19294
|
// src/changelog.ts
|
|
19082
|
-
import { readFileSync as
|
|
19295
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
19083
19296
|
import { dirname as dirname4, resolve as resolve4 } from "path";
|
|
19084
19297
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
19085
19298
|
var __dirname5 = dirname4(fileURLToPath5(import.meta.url));
|
|
@@ -19090,7 +19303,7 @@ function getReleaseNotes(version) {
|
|
|
19090
19303
|
];
|
|
19091
19304
|
let changelogPath = null;
|
|
19092
19305
|
for (const p of possiblePaths) {
|
|
19093
|
-
if (
|
|
19306
|
+
if (existsSync6(p)) {
|
|
19094
19307
|
changelogPath = p;
|
|
19095
19308
|
break;
|
|
19096
19309
|
}
|
|
@@ -19099,7 +19312,7 @@ function getReleaseNotes(version) {
|
|
|
19099
19312
|
return null;
|
|
19100
19313
|
}
|
|
19101
19314
|
try {
|
|
19102
|
-
const content =
|
|
19315
|
+
const content = readFileSync6(changelogPath, "utf-8");
|
|
19103
19316
|
return parseChangelog(content, version);
|
|
19104
19317
|
} catch {
|
|
19105
19318
|
return null;
|
|
@@ -19517,6 +19730,170 @@ async function postUser(session, message) {
|
|
|
19517
19730
|
return session.platform.createPost(`\uD83D\uDC64 ${message}`, session.threadId);
|
|
19518
19731
|
}
|
|
19519
19732
|
|
|
19733
|
+
// src/git/worktree.ts
|
|
19734
|
+
import { spawn as spawn4 } from "child_process";
|
|
19735
|
+
import { randomUUID } from "crypto";
|
|
19736
|
+
import * as path9 from "path";
|
|
19737
|
+
import * as fs5 from "fs/promises";
|
|
19738
|
+
async function execGit(args, cwd) {
|
|
19739
|
+
return new Promise((resolve5, reject) => {
|
|
19740
|
+
const proc = spawn4("git", args, { cwd });
|
|
19741
|
+
let stdout = "";
|
|
19742
|
+
let stderr = "";
|
|
19743
|
+
proc.stdout.on("data", (data) => {
|
|
19744
|
+
stdout += data.toString();
|
|
19745
|
+
});
|
|
19746
|
+
proc.stderr.on("data", (data) => {
|
|
19747
|
+
stderr += data.toString();
|
|
19748
|
+
});
|
|
19749
|
+
proc.on("close", (code) => {
|
|
19750
|
+
if (code === 0) {
|
|
19751
|
+
resolve5(stdout.trim());
|
|
19752
|
+
} else {
|
|
19753
|
+
reject(new Error(`git ${args.join(" ")} failed: ${stderr || stdout}`));
|
|
19754
|
+
}
|
|
19755
|
+
});
|
|
19756
|
+
proc.on("error", (err) => {
|
|
19757
|
+
reject(err);
|
|
19758
|
+
});
|
|
19759
|
+
});
|
|
19760
|
+
}
|
|
19761
|
+
async function isGitRepository(dir) {
|
|
19762
|
+
try {
|
|
19763
|
+
await execGit(["rev-parse", "--git-dir"], dir);
|
|
19764
|
+
return true;
|
|
19765
|
+
} catch {
|
|
19766
|
+
return false;
|
|
19767
|
+
}
|
|
19768
|
+
}
|
|
19769
|
+
async function getRepositoryRoot(dir) {
|
|
19770
|
+
return execGit(["rev-parse", "--show-toplevel"], dir);
|
|
19771
|
+
}
|
|
19772
|
+
async function getCurrentBranch(dir) {
|
|
19773
|
+
try {
|
|
19774
|
+
const branch = await execGit(["rev-parse", "--abbrev-ref", "HEAD"], dir);
|
|
19775
|
+
return branch === "HEAD" ? null : branch;
|
|
19776
|
+
} catch {
|
|
19777
|
+
return null;
|
|
19778
|
+
}
|
|
19779
|
+
}
|
|
19780
|
+
async function hasUncommittedChanges(dir) {
|
|
19781
|
+
try {
|
|
19782
|
+
const staged = await execGit(["diff", "--cached", "--quiet"], dir).catch(() => "changes");
|
|
19783
|
+
if (staged === "changes")
|
|
19784
|
+
return true;
|
|
19785
|
+
const unstaged = await execGit(["diff", "--quiet"], dir).catch(() => "changes");
|
|
19786
|
+
if (unstaged === "changes")
|
|
19787
|
+
return true;
|
|
19788
|
+
const untracked = await execGit(["ls-files", "--others", "--exclude-standard"], dir);
|
|
19789
|
+
return untracked.length > 0;
|
|
19790
|
+
} catch {
|
|
19791
|
+
return false;
|
|
19792
|
+
}
|
|
19793
|
+
}
|
|
19794
|
+
async function listWorktrees(repoRoot) {
|
|
19795
|
+
const output = await execGit(["worktree", "list", "--porcelain"], repoRoot);
|
|
19796
|
+
const worktrees = [];
|
|
19797
|
+
if (!output)
|
|
19798
|
+
return worktrees;
|
|
19799
|
+
const blocks = output.split(`
|
|
19800
|
+
|
|
19801
|
+
`).filter(Boolean);
|
|
19802
|
+
for (const block of blocks) {
|
|
19803
|
+
const lines = block.split(`
|
|
19804
|
+
`);
|
|
19805
|
+
const worktree = {};
|
|
19806
|
+
for (const line of lines) {
|
|
19807
|
+
if (line.startsWith("worktree ")) {
|
|
19808
|
+
worktree.path = line.slice(9);
|
|
19809
|
+
} else if (line.startsWith("HEAD ")) {
|
|
19810
|
+
worktree.commit = line.slice(5);
|
|
19811
|
+
} else if (line.startsWith("branch ")) {
|
|
19812
|
+
worktree.branch = line.slice(7).replace("refs/heads/", "");
|
|
19813
|
+
} else if (line === "bare") {
|
|
19814
|
+
worktree.isBare = true;
|
|
19815
|
+
} else if (line === "detached") {
|
|
19816
|
+
worktree.branch = "(detached)";
|
|
19817
|
+
}
|
|
19818
|
+
}
|
|
19819
|
+
if (worktree.path) {
|
|
19820
|
+
worktrees.push({
|
|
19821
|
+
path: worktree.path,
|
|
19822
|
+
branch: worktree.branch || "(unknown)",
|
|
19823
|
+
commit: worktree.commit || "",
|
|
19824
|
+
isMain: worktrees.length === 0,
|
|
19825
|
+
isBare: worktree.isBare || false
|
|
19826
|
+
});
|
|
19827
|
+
}
|
|
19828
|
+
}
|
|
19829
|
+
return worktrees;
|
|
19830
|
+
}
|
|
19831
|
+
async function branchExists(repoRoot, branch) {
|
|
19832
|
+
try {
|
|
19833
|
+
await execGit(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot);
|
|
19834
|
+
return true;
|
|
19835
|
+
} catch {
|
|
19836
|
+
try {
|
|
19837
|
+
await execGit(["rev-parse", "--verify", `refs/remotes/origin/${branch}`], repoRoot);
|
|
19838
|
+
return true;
|
|
19839
|
+
} catch {
|
|
19840
|
+
return false;
|
|
19841
|
+
}
|
|
19842
|
+
}
|
|
19843
|
+
}
|
|
19844
|
+
function getWorktreeDir(repoRoot, branch) {
|
|
19845
|
+
const repoName = path9.basename(repoRoot);
|
|
19846
|
+
const parentDir = path9.dirname(repoRoot);
|
|
19847
|
+
const worktreesDir = path9.join(parentDir, `${repoName}-worktrees`);
|
|
19848
|
+
const sanitizedBranch = branch.replace(/\//g, "-").replace(/[^a-zA-Z0-9-_]/g, "");
|
|
19849
|
+
const shortUuid = randomUUID().slice(0, 8);
|
|
19850
|
+
return path9.join(worktreesDir, `${sanitizedBranch}-${shortUuid}`);
|
|
19851
|
+
}
|
|
19852
|
+
async function createWorktree(repoRoot, branch, targetDir) {
|
|
19853
|
+
const parentDir = path9.dirname(targetDir);
|
|
19854
|
+
await fs5.mkdir(parentDir, { recursive: true });
|
|
19855
|
+
const exists = await branchExists(repoRoot, branch);
|
|
19856
|
+
if (exists) {
|
|
19857
|
+
await execGit(["worktree", "add", targetDir, branch], repoRoot);
|
|
19858
|
+
} else {
|
|
19859
|
+
await execGit(["worktree", "add", "-b", branch, targetDir], repoRoot);
|
|
19860
|
+
}
|
|
19861
|
+
return targetDir;
|
|
19862
|
+
}
|
|
19863
|
+
async function removeWorktree(repoRoot, worktreePath) {
|
|
19864
|
+
try {
|
|
19865
|
+
await execGit(["worktree", "remove", worktreePath], repoRoot);
|
|
19866
|
+
} catch {
|
|
19867
|
+
await execGit(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
19868
|
+
}
|
|
19869
|
+
await execGit(["worktree", "prune"], repoRoot);
|
|
19870
|
+
}
|
|
19871
|
+
async function findWorktreeByBranch(repoRoot, branch) {
|
|
19872
|
+
const worktrees = await listWorktrees(repoRoot);
|
|
19873
|
+
return worktrees.find((wt) => wt.branch === branch) || null;
|
|
19874
|
+
}
|
|
19875
|
+
function isValidBranchName(name) {
|
|
19876
|
+
if (!name || name.length === 0)
|
|
19877
|
+
return false;
|
|
19878
|
+
if (name.startsWith("/") || name.endsWith("/"))
|
|
19879
|
+
return false;
|
|
19880
|
+
if (name.includes(".."))
|
|
19881
|
+
return false;
|
|
19882
|
+
if (/[\s~^:?*[\]\\]/.test(name))
|
|
19883
|
+
return false;
|
|
19884
|
+
if (name.startsWith("-"))
|
|
19885
|
+
return false;
|
|
19886
|
+
if (name.endsWith(".lock"))
|
|
19887
|
+
return false;
|
|
19888
|
+
if (name.includes("@{"))
|
|
19889
|
+
return false;
|
|
19890
|
+
if (name === "@")
|
|
19891
|
+
return false;
|
|
19892
|
+
if (/\.\./.test(name))
|
|
19893
|
+
return false;
|
|
19894
|
+
return true;
|
|
19895
|
+
}
|
|
19896
|
+
|
|
19520
19897
|
// src/session/commands.ts
|
|
19521
19898
|
var log9 = createLogger("commands");
|
|
19522
19899
|
async function restartClaudeSession(session, cliOptions, ctx, actionName) {
|
|
@@ -19588,7 +19965,7 @@ async function changeDirectory(session, newDir, username, ctx) {
|
|
|
19588
19965
|
}
|
|
19589
19966
|
const expandedDir = newDir.startsWith("~") ? newDir.replace("~", process.env.HOME || "") : newDir;
|
|
19590
19967
|
const absoluteDir = resolve5(expandedDir);
|
|
19591
|
-
if (!
|
|
19968
|
+
if (!existsSync7(absoluteDir)) {
|
|
19592
19969
|
await postError(session, `Directory does not exist: \`${newDir}\``);
|
|
19593
19970
|
return;
|
|
19594
19971
|
}
|
|
@@ -19600,7 +19977,7 @@ async function changeDirectory(session, newDir, username, ctx) {
|
|
|
19600
19977
|
const shortDir = absoluteDir.replace(process.env.HOME || "", "~");
|
|
19601
19978
|
log9.info(`\uD83D\uDCC2 Session (${shortId}\u2026) changing directory to ${shortDir}`);
|
|
19602
19979
|
session.workingDir = absoluteDir;
|
|
19603
|
-
const newSessionId =
|
|
19980
|
+
const newSessionId = randomUUID2();
|
|
19604
19981
|
session.claudeSessionId = newSessionId;
|
|
19605
19982
|
const cliOptions = {
|
|
19606
19983
|
workingDir: absoluteDir,
|
|
@@ -19626,6 +20003,11 @@ async function inviteUser(session, invitedUser, invitedBy, ctx) {
|
|
|
19626
20003
|
if (!await requireSessionOwner(session, invitedBy, "invite others")) {
|
|
19627
20004
|
return;
|
|
19628
20005
|
}
|
|
20006
|
+
const user = await session.platform.getUserByUsername(invitedUser);
|
|
20007
|
+
if (!user) {
|
|
20008
|
+
await postWarning(session, `User @${invitedUser} does not exist on this platform`);
|
|
20009
|
+
return;
|
|
20010
|
+
}
|
|
19629
20011
|
session.sessionAllowedUsers.add(invitedUser);
|
|
19630
20012
|
await postSuccess(session, `@${invitedUser} can now participate in this session (invited by @${invitedBy})`);
|
|
19631
20013
|
log9.info(`\uD83D\uDC4B @${invitedUser} invited to session by @${invitedBy}`);
|
|
@@ -19636,6 +20018,11 @@ async function kickUser(session, kickedUser, kickedBy, ctx) {
|
|
|
19636
20018
|
if (!await requireSessionOwner(session, kickedBy, "kick others")) {
|
|
19637
20019
|
return;
|
|
19638
20020
|
}
|
|
20021
|
+
const user = await session.platform.getUserByUsername(kickedUser);
|
|
20022
|
+
if (!user) {
|
|
20023
|
+
await postWarning(session, `User @${kickedUser} does not exist on this platform`);
|
|
20024
|
+
return;
|
|
20025
|
+
}
|
|
19639
20026
|
if (kickedUser === session.startedBy) {
|
|
19640
20027
|
await postWarning(session, `Cannot kick session owner @${session.startedBy}`);
|
|
19641
20028
|
return;
|
|
@@ -19749,6 +20136,17 @@ async function updateSessionHeader(session, ctx) {
|
|
|
19749
20136
|
if (session.worktreeInfo) {
|
|
19750
20137
|
const shortRepoRoot = session.worktreeInfo.repoRoot.replace(process.env.HOME || "", "~");
|
|
19751
20138
|
rows.push(`| \uD83C\uDF3F **Worktree** | \`${session.worktreeInfo.branch}\` (from \`${shortRepoRoot}\`) |`);
|
|
20139
|
+
} else {
|
|
20140
|
+
const isRepo = await isGitRepository(session.workingDir);
|
|
20141
|
+
if (isRepo) {
|
|
20142
|
+
const branch = await getCurrentBranch(session.workingDir);
|
|
20143
|
+
if (branch) {
|
|
20144
|
+
rows.push(`| \uD83C\uDF3F **Branch** | \`${branch}\` |`);
|
|
20145
|
+
}
|
|
20146
|
+
}
|
|
20147
|
+
}
|
|
20148
|
+
if (session.pullRequestUrl) {
|
|
20149
|
+
rows.push(`| \uD83D\uDD17 **Pull Request** | ${formatPullRequestLink(session.pullRequestUrl)} |`);
|
|
19752
20150
|
}
|
|
19753
20151
|
if (otherParticipants) {
|
|
19754
20152
|
rows.push(`| \uD83D\uDC65 **Participants** | ${otherParticipants} |`);
|
|
@@ -19779,8 +20177,8 @@ async function updateSessionHeader(session, ctx) {
|
|
|
19779
20177
|
}
|
|
19780
20178
|
|
|
19781
20179
|
// src/session/lifecycle.ts
|
|
19782
|
-
import { randomUUID as
|
|
19783
|
-
import { existsSync as
|
|
20180
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
20181
|
+
import { existsSync as existsSync8 } from "fs";
|
|
19784
20182
|
var log10 = createLogger("lifecycle");
|
|
19785
20183
|
function mutableSessions(ctx) {
|
|
19786
20184
|
return ctx.state.sessions;
|
|
@@ -19903,7 +20301,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
19903
20301
|
return;
|
|
19904
20302
|
const actualThreadId = replyToPostId || post.id;
|
|
19905
20303
|
const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
|
|
19906
|
-
const claudeSessionId =
|
|
20304
|
+
const claudeSessionId = randomUUID3();
|
|
19907
20305
|
const platformMcpConfig = platform.getMcpConfig();
|
|
19908
20306
|
const cliOptions = {
|
|
19909
20307
|
workingDir: ctx.config.workingDir,
|
|
@@ -20012,7 +20410,7 @@ async function resumeSession(state, ctx) {
|
|
|
20012
20410
|
log10.warn(`Max sessions reached, skipping resume for ${shortId}...`);
|
|
20013
20411
|
return;
|
|
20014
20412
|
}
|
|
20015
|
-
if (!
|
|
20413
|
+
if (!existsSync8(state.workingDir)) {
|
|
20016
20414
|
log10.warn(`Working directory ${state.workingDir} no longer exists, skipping resume for ${shortId}...`);
|
|
20017
20415
|
ctx.state.sessionStore.remove(`${state.platformId}:${state.threadId}`);
|
|
20018
20416
|
await withErrorHandling(() => platform.createPost(`\u26A0\uFE0F **Cannot resume session** - working directory no longer exists:
|
|
@@ -20080,6 +20478,7 @@ Please start a new session.`, state.threadId), { action: "Post resume failure no
|
|
|
20080
20478
|
needsContextPromptOnNextMessage: state.needsContextPromptOnNextMessage,
|
|
20081
20479
|
sessionTitle: state.sessionTitle,
|
|
20082
20480
|
sessionDescription: state.sessionDescription,
|
|
20481
|
+
pullRequestUrl: state.pullRequestUrl,
|
|
20083
20482
|
messageCount: state.messageCount ?? 0,
|
|
20084
20483
|
statusBarTimer: null
|
|
20085
20484
|
};
|
|
@@ -20225,7 +20624,7 @@ async function killSession(session, unpersist, ctx) {
|
|
|
20225
20624
|
cleanupPostIndex(ctx, session.threadId);
|
|
20226
20625
|
keepAlive.sessionEnded();
|
|
20227
20626
|
if (unpersist) {
|
|
20228
|
-
ctx.ops.unpersistSession(session.
|
|
20627
|
+
ctx.ops.unpersistSession(session.sessionId);
|
|
20229
20628
|
}
|
|
20230
20629
|
log10.info(`\u2716 Session killed (${shortId}\u2026) \u2014 ${ctx.state.sessions.size} active`);
|
|
20231
20630
|
await ctx.ops.updateStickyMessage();
|
|
@@ -20270,162 +20669,6 @@ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
|
|
|
20270
20669
|
}
|
|
20271
20670
|
}
|
|
20272
20671
|
|
|
20273
|
-
// src/git/worktree.ts
|
|
20274
|
-
import { spawn as spawn4 } from "child_process";
|
|
20275
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
20276
|
-
import * as path9 from "path";
|
|
20277
|
-
import * as fs5 from "fs/promises";
|
|
20278
|
-
async function execGit(args, cwd) {
|
|
20279
|
-
return new Promise((resolve6, reject) => {
|
|
20280
|
-
const proc = spawn4("git", args, { cwd });
|
|
20281
|
-
let stdout = "";
|
|
20282
|
-
let stderr = "";
|
|
20283
|
-
proc.stdout.on("data", (data) => {
|
|
20284
|
-
stdout += data.toString();
|
|
20285
|
-
});
|
|
20286
|
-
proc.stderr.on("data", (data) => {
|
|
20287
|
-
stderr += data.toString();
|
|
20288
|
-
});
|
|
20289
|
-
proc.on("close", (code) => {
|
|
20290
|
-
if (code === 0) {
|
|
20291
|
-
resolve6(stdout.trim());
|
|
20292
|
-
} else {
|
|
20293
|
-
reject(new Error(`git ${args.join(" ")} failed: ${stderr || stdout}`));
|
|
20294
|
-
}
|
|
20295
|
-
});
|
|
20296
|
-
proc.on("error", (err) => {
|
|
20297
|
-
reject(err);
|
|
20298
|
-
});
|
|
20299
|
-
});
|
|
20300
|
-
}
|
|
20301
|
-
async function isGitRepository(dir) {
|
|
20302
|
-
try {
|
|
20303
|
-
await execGit(["rev-parse", "--git-dir"], dir);
|
|
20304
|
-
return true;
|
|
20305
|
-
} catch {
|
|
20306
|
-
return false;
|
|
20307
|
-
}
|
|
20308
|
-
}
|
|
20309
|
-
async function getRepositoryRoot(dir) {
|
|
20310
|
-
return execGit(["rev-parse", "--show-toplevel"], dir);
|
|
20311
|
-
}
|
|
20312
|
-
async function hasUncommittedChanges(dir) {
|
|
20313
|
-
try {
|
|
20314
|
-
const staged = await execGit(["diff", "--cached", "--quiet"], dir).catch(() => "changes");
|
|
20315
|
-
if (staged === "changes")
|
|
20316
|
-
return true;
|
|
20317
|
-
const unstaged = await execGit(["diff", "--quiet"], dir).catch(() => "changes");
|
|
20318
|
-
if (unstaged === "changes")
|
|
20319
|
-
return true;
|
|
20320
|
-
const untracked = await execGit(["ls-files", "--others", "--exclude-standard"], dir);
|
|
20321
|
-
return untracked.length > 0;
|
|
20322
|
-
} catch {
|
|
20323
|
-
return false;
|
|
20324
|
-
}
|
|
20325
|
-
}
|
|
20326
|
-
async function listWorktrees(repoRoot) {
|
|
20327
|
-
const output = await execGit(["worktree", "list", "--porcelain"], repoRoot);
|
|
20328
|
-
const worktrees = [];
|
|
20329
|
-
if (!output)
|
|
20330
|
-
return worktrees;
|
|
20331
|
-
const blocks = output.split(`
|
|
20332
|
-
|
|
20333
|
-
`).filter(Boolean);
|
|
20334
|
-
for (const block of blocks) {
|
|
20335
|
-
const lines = block.split(`
|
|
20336
|
-
`);
|
|
20337
|
-
const worktree = {};
|
|
20338
|
-
for (const line of lines) {
|
|
20339
|
-
if (line.startsWith("worktree ")) {
|
|
20340
|
-
worktree.path = line.slice(9);
|
|
20341
|
-
} else if (line.startsWith("HEAD ")) {
|
|
20342
|
-
worktree.commit = line.slice(5);
|
|
20343
|
-
} else if (line.startsWith("branch ")) {
|
|
20344
|
-
worktree.branch = line.slice(7).replace("refs/heads/", "");
|
|
20345
|
-
} else if (line === "bare") {
|
|
20346
|
-
worktree.isBare = true;
|
|
20347
|
-
} else if (line === "detached") {
|
|
20348
|
-
worktree.branch = "(detached)";
|
|
20349
|
-
}
|
|
20350
|
-
}
|
|
20351
|
-
if (worktree.path) {
|
|
20352
|
-
worktrees.push({
|
|
20353
|
-
path: worktree.path,
|
|
20354
|
-
branch: worktree.branch || "(unknown)",
|
|
20355
|
-
commit: worktree.commit || "",
|
|
20356
|
-
isMain: worktrees.length === 0,
|
|
20357
|
-
isBare: worktree.isBare || false
|
|
20358
|
-
});
|
|
20359
|
-
}
|
|
20360
|
-
}
|
|
20361
|
-
return worktrees;
|
|
20362
|
-
}
|
|
20363
|
-
async function branchExists(repoRoot, branch) {
|
|
20364
|
-
try {
|
|
20365
|
-
await execGit(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot);
|
|
20366
|
-
return true;
|
|
20367
|
-
} catch {
|
|
20368
|
-
try {
|
|
20369
|
-
await execGit(["rev-parse", "--verify", `refs/remotes/origin/${branch}`], repoRoot);
|
|
20370
|
-
return true;
|
|
20371
|
-
} catch {
|
|
20372
|
-
return false;
|
|
20373
|
-
}
|
|
20374
|
-
}
|
|
20375
|
-
}
|
|
20376
|
-
function getWorktreeDir(repoRoot, branch) {
|
|
20377
|
-
const repoName = path9.basename(repoRoot);
|
|
20378
|
-
const parentDir = path9.dirname(repoRoot);
|
|
20379
|
-
const worktreesDir = path9.join(parentDir, `${repoName}-worktrees`);
|
|
20380
|
-
const sanitizedBranch = branch.replace(/\//g, "-").replace(/[^a-zA-Z0-9-_]/g, "");
|
|
20381
|
-
const shortUuid = randomUUID3().slice(0, 8);
|
|
20382
|
-
return path9.join(worktreesDir, `${sanitizedBranch}-${shortUuid}`);
|
|
20383
|
-
}
|
|
20384
|
-
async function createWorktree(repoRoot, branch, targetDir) {
|
|
20385
|
-
const parentDir = path9.dirname(targetDir);
|
|
20386
|
-
await fs5.mkdir(parentDir, { recursive: true });
|
|
20387
|
-
const exists = await branchExists(repoRoot, branch);
|
|
20388
|
-
if (exists) {
|
|
20389
|
-
await execGit(["worktree", "add", targetDir, branch], repoRoot);
|
|
20390
|
-
} else {
|
|
20391
|
-
await execGit(["worktree", "add", "-b", branch, targetDir], repoRoot);
|
|
20392
|
-
}
|
|
20393
|
-
return targetDir;
|
|
20394
|
-
}
|
|
20395
|
-
async function removeWorktree(repoRoot, worktreePath) {
|
|
20396
|
-
try {
|
|
20397
|
-
await execGit(["worktree", "remove", worktreePath], repoRoot);
|
|
20398
|
-
} catch {
|
|
20399
|
-
await execGit(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
20400
|
-
}
|
|
20401
|
-
await execGit(["worktree", "prune"], repoRoot);
|
|
20402
|
-
}
|
|
20403
|
-
async function findWorktreeByBranch(repoRoot, branch) {
|
|
20404
|
-
const worktrees = await listWorktrees(repoRoot);
|
|
20405
|
-
return worktrees.find((wt) => wt.branch === branch) || null;
|
|
20406
|
-
}
|
|
20407
|
-
function isValidBranchName(name) {
|
|
20408
|
-
if (!name || name.length === 0)
|
|
20409
|
-
return false;
|
|
20410
|
-
if (name.startsWith("/") || name.endsWith("/"))
|
|
20411
|
-
return false;
|
|
20412
|
-
if (name.includes(".."))
|
|
20413
|
-
return false;
|
|
20414
|
-
if (/[\s~^:?*[\]\\]/.test(name))
|
|
20415
|
-
return false;
|
|
20416
|
-
if (name.startsWith("-"))
|
|
20417
|
-
return false;
|
|
20418
|
-
if (name.endsWith(".lock"))
|
|
20419
|
-
return false;
|
|
20420
|
-
if (name.includes("@{"))
|
|
20421
|
-
return false;
|
|
20422
|
-
if (name === "@")
|
|
20423
|
-
return false;
|
|
20424
|
-
if (/\.\./.test(name))
|
|
20425
|
-
return false;
|
|
20426
|
-
return true;
|
|
20427
|
-
}
|
|
20428
|
-
|
|
20429
20672
|
// src/session/worktree.ts
|
|
20430
20673
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
20431
20674
|
var log11 = createLogger("worktree");
|
|
@@ -20993,6 +21236,26 @@ function getSessionTopic(session) {
|
|
|
20993
21236
|
}
|
|
20994
21237
|
return formatTopicFromPrompt(session.firstPrompt);
|
|
20995
21238
|
}
|
|
21239
|
+
function getHistorySessionTopic(session) {
|
|
21240
|
+
if (session.sessionTitle) {
|
|
21241
|
+
return session.sessionTitle;
|
|
21242
|
+
}
|
|
21243
|
+
return formatTopicFromPrompt(session.firstPrompt);
|
|
21244
|
+
}
|
|
21245
|
+
function formatHistoryEntry(session) {
|
|
21246
|
+
const topic = getHistorySessionTopic(session);
|
|
21247
|
+
const threadLink = `[${topic}](/_redirect/pl/${session.threadId})`;
|
|
21248
|
+
const displayName = session.startedByDisplayName || session.startedBy;
|
|
21249
|
+
const cleanedAt = session.cleanedAt ? new Date(session.cleanedAt) : new Date(session.lastActivityAt);
|
|
21250
|
+
const time = formatRelativeTime(cleanedAt);
|
|
21251
|
+
const prStr = session.pullRequestUrl ? ` \xB7 ${formatPullRequestLink(session.pullRequestUrl)}` : "";
|
|
21252
|
+
const lines = [];
|
|
21253
|
+
lines.push(` \u2713 ${threadLink} \xB7 **${displayName}**${prStr} \xB7 ${time}`);
|
|
21254
|
+
if (session.sessionDescription) {
|
|
21255
|
+
lines.push(` _${session.sessionDescription}_`);
|
|
21256
|
+
}
|
|
21257
|
+
return lines;
|
|
21258
|
+
}
|
|
20996
21259
|
async function buildStatusBar(sessionCount, config) {
|
|
20997
21260
|
const items = [];
|
|
20998
21261
|
items.push(`\`v${VERSION}\``);
|
|
@@ -21036,17 +21299,27 @@ function formatTopicFromPrompt(prompt) {
|
|
|
21036
21299
|
async function buildStickyMessage(sessions, platformId, config) {
|
|
21037
21300
|
const platformSessions = [...sessions.values()].filter((s) => s.platformId === platformId);
|
|
21038
21301
|
const statusBar = await buildStatusBar(platformSessions.length, config);
|
|
21302
|
+
const historySessions = sessionStore ? sessionStore.getHistory(platformId).slice(0, 5) : [];
|
|
21039
21303
|
if (platformSessions.length === 0) {
|
|
21040
|
-
|
|
21304
|
+
const lines2 = [
|
|
21041
21305
|
"---",
|
|
21042
21306
|
statusBar,
|
|
21043
21307
|
"",
|
|
21044
21308
|
"**Active Claude Threads**",
|
|
21045
21309
|
"",
|
|
21046
|
-
"_No active sessions_"
|
|
21047
|
-
|
|
21048
|
-
|
|
21049
|
-
|
|
21310
|
+
"_No active sessions_"
|
|
21311
|
+
];
|
|
21312
|
+
if (historySessions.length > 0) {
|
|
21313
|
+
lines2.push("");
|
|
21314
|
+
lines2.push(`**Recent** (${historySessions.length})`);
|
|
21315
|
+
lines2.push("");
|
|
21316
|
+
for (const historySession of historySessions) {
|
|
21317
|
+
lines2.push(...formatHistoryEntry(historySession));
|
|
21318
|
+
}
|
|
21319
|
+
}
|
|
21320
|
+
lines2.push("");
|
|
21321
|
+
lines2.push("_Mention me to start a session_ \xB7 `npm i -g claude-threads`");
|
|
21322
|
+
return lines2.join(`
|
|
21050
21323
|
`);
|
|
21051
21324
|
}
|
|
21052
21325
|
platformSessions.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
|
|
@@ -21065,7 +21338,8 @@ async function buildStickyMessage(sessions, platformId, config) {
|
|
|
21065
21338
|
const time = formatRelativeTime(session.startedAt);
|
|
21066
21339
|
const taskProgress = getTaskProgress(session);
|
|
21067
21340
|
const progressStr = taskProgress ? ` \xB7 ${taskProgress}` : "";
|
|
21068
|
-
|
|
21341
|
+
const prStr = session.pullRequestUrl ? ` \xB7 ${formatPullRequestLink(session.pullRequestUrl)}` : "";
|
|
21342
|
+
lines.push(`\u25B8 ${threadLink} \xB7 **${displayName}**${progressStr}${prStr} \xB7 ${time}`);
|
|
21069
21343
|
if (session.sessionDescription) {
|
|
21070
21344
|
lines.push(` _${session.sessionDescription}_`);
|
|
21071
21345
|
}
|
|
@@ -21078,6 +21352,14 @@ async function buildStickyMessage(sessions, platformId, config) {
|
|
|
21078
21352
|
lines.push(` \uD83D\uDD04 _${activeTask}_`);
|
|
21079
21353
|
}
|
|
21080
21354
|
}
|
|
21355
|
+
if (historySessions.length > 0) {
|
|
21356
|
+
lines.push("");
|
|
21357
|
+
lines.push(`**Recent** (${historySessions.length})`);
|
|
21358
|
+
lines.push("");
|
|
21359
|
+
for (const historySession of historySessions) {
|
|
21360
|
+
lines.push(...formatHistoryEntry(historySession));
|
|
21361
|
+
}
|
|
21362
|
+
}
|
|
21081
21363
|
lines.push("");
|
|
21082
21364
|
lines.push("_Mention me to start a session_ \xB7 `npm i -g claude-threads`");
|
|
21083
21365
|
return lines.join(`
|
|
@@ -21505,12 +21787,13 @@ class SessionManager {
|
|
|
21505
21787
|
timeoutPostId: session.timeoutPostId,
|
|
21506
21788
|
sessionTitle: session.sessionTitle,
|
|
21507
21789
|
sessionDescription: session.sessionDescription,
|
|
21790
|
+
pullRequestUrl: session.pullRequestUrl,
|
|
21508
21791
|
messageCount: session.messageCount
|
|
21509
21792
|
};
|
|
21510
21793
|
this.sessionStore.save(session.sessionId, state);
|
|
21511
21794
|
}
|
|
21512
21795
|
unpersistSession(sessionId) {
|
|
21513
|
-
this.sessionStore.
|
|
21796
|
+
this.sessionStore.softDelete(sessionId);
|
|
21514
21797
|
}
|
|
21515
21798
|
async updateSessionHeader(session) {
|
|
21516
21799
|
await updateSessionHeader(session, this.getContext());
|
|
@@ -21537,7 +21820,11 @@ class SessionManager {
|
|
|
21537
21820
|
}
|
|
21538
21821
|
const staleIds = this.sessionStore.cleanStale(SESSION_TIMEOUT_MS * 2);
|
|
21539
21822
|
if (staleIds.length > 0) {
|
|
21540
|
-
log14.info(`\uD83E\uDDF9
|
|
21823
|
+
log14.info(`\uD83E\uDDF9 Soft-deleted ${staleIds.length} stale session(s) (kept for history)`);
|
|
21824
|
+
}
|
|
21825
|
+
const removedCount = this.sessionStore.cleanHistory();
|
|
21826
|
+
if (removedCount > 0) {
|
|
21827
|
+
log14.info(`\uD83D\uDDD1\uFE0F Permanently removed ${removedCount} old session(s) from history`);
|
|
21541
21828
|
}
|
|
21542
21829
|
const persisted = this.sessionStore.load();
|
|
21543
21830
|
log14.info(`\uD83D\uDCC2 Loaded ${persisted.size} session(s) from persistence`);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/statusline/writer.ts
|
|
5
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
6
|
+
import { dirname } from "path";
|
|
7
|
+
var sessionId = process.argv[2];
|
|
8
|
+
if (!sessionId) {
|
|
9
|
+
console.log("");
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
var input = "";
|
|
13
|
+
process.stdin.setEncoding("utf8");
|
|
14
|
+
process.stdin.on("data", (chunk) => {
|
|
15
|
+
input += chunk;
|
|
16
|
+
});
|
|
17
|
+
process.stdin.on("end", () => {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(input);
|
|
20
|
+
const contextWindow = data.context_window;
|
|
21
|
+
if (contextWindow) {
|
|
22
|
+
const usage = contextWindow.current_usage;
|
|
23
|
+
const output = {
|
|
24
|
+
context_window_size: contextWindow.context_window_size,
|
|
25
|
+
total_input_tokens: contextWindow.total_input_tokens,
|
|
26
|
+
total_output_tokens: contextWindow.total_output_tokens,
|
|
27
|
+
current_usage: usage ? {
|
|
28
|
+
input_tokens: usage.input_tokens || 0,
|
|
29
|
+
output_tokens: usage.output_tokens || 0,
|
|
30
|
+
cache_creation_input_tokens: usage.cache_creation_input_tokens || 0,
|
|
31
|
+
cache_read_input_tokens: usage.cache_read_input_tokens || 0
|
|
32
|
+
} : null,
|
|
33
|
+
model: data.model ? {
|
|
34
|
+
id: data.model.id,
|
|
35
|
+
display_name: data.model.display_name
|
|
36
|
+
} : null,
|
|
37
|
+
cost: data.cost ? {
|
|
38
|
+
total_cost_usd: data.cost.total_cost_usd
|
|
39
|
+
} : null,
|
|
40
|
+
timestamp: Date.now()
|
|
41
|
+
};
|
|
42
|
+
const filePath = `/tmp/claude-threads-status-${sessionId}.json`;
|
|
43
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
44
|
+
writeFileSync(filePath, JSON.stringify(output, null, 2));
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
console.log("");
|
|
48
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-threads",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Share Claude Code sessions live in a Mattermost channel with interactive features",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "bun --watch src/index.ts",
|
|
13
|
-
"build": "bun build src/index.ts --outdir dist --target bun && bun build src/mcp/permission-server.ts --outdir dist/mcp --target bun",
|
|
13
|
+
"build": "bun build src/index.ts --outdir dist --target bun && bun build src/mcp/permission-server.ts --outdir dist/mcp --target bun && bun build src/statusline/writer.ts --outdir dist/statusline --target bun",
|
|
14
14
|
"start": "bun dist/index.js",
|
|
15
15
|
"test": "bun test",
|
|
16
16
|
"test:watch": "bun test --watch",
|