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 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
- delete data.sessions[sessionId];
13771
+ session.cleanedAt = new Date().toISOString();
13750
13772
  }
13751
13773
  }
13752
13774
  if (staleIds.length > 0) {
13753
13775
  this.writeAtomic(data);
13754
- log3.debug(`Cleaned ${staleIds.length} stale session(s)`);
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
- log6.debug(`Updated usage stats: ${usageStats.modelDisplayName}, ` + `${usageStats.totalTokensUsed}/${usageStats.contextWindowSize} tokens, ` + `$${usageStats.totalCostUSD.toFixed(4)}`);
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 existsSync6, statSync } from "fs";
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 readFileSync4, existsSync as existsSync4 } from "fs";
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 (existsSync4(candidate)) {
19250
+ if (existsSync5(candidate)) {
19038
19251
  try {
19039
- const pkg = JSON.parse(readFileSync4(candidate, "utf-8"));
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 readFileSync5, existsSync as existsSync5 } from "fs";
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 (existsSync5(p)) {
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 = readFileSync5(changelogPath, "utf-8");
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 (!existsSync6(absoluteDir)) {
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 = randomUUID();
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 randomUUID2 } from "crypto";
19783
- import { existsSync as existsSync7 } from "fs";
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 = randomUUID2();
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 (!existsSync7(state.workingDir)) {
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.threadId);
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
- return [
21304
+ const lines2 = [
21041
21305
  "---",
21042
21306
  statusBar,
21043
21307
  "",
21044
21308
  "**Active Claude Threads**",
21045
21309
  "",
21046
- "_No active sessions_",
21047
- "",
21048
- "_Mention me to start a session_ \xB7 `npm i -g claude-threads`"
21049
- ].join(`
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
- lines.push(`\u25B8 ${threadLink} \xB7 **${displayName}**${progressStr} \xB7 ${time}`);
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.remove(sessionId);
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 Cleaned ${staleIds.length} stale session(s) from persistence`);
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.29.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",