claude-threads 0.30.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 +9 -0
- package/dist/index.js +364 -180
- package/dist/statusline/writer.js +48 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ 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
|
+
|
|
10
19
|
## [0.30.0] - 2026-01-03
|
|
11
20
|
|
|
12
21
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -13722,10 +13722,12 @@ class SessionStore {
|
|
|
13722
13722
|
return sessions;
|
|
13723
13723
|
}
|
|
13724
13724
|
for (const session of Object.values(data.sessions)) {
|
|
13725
|
+
if (session.cleanedAt)
|
|
13726
|
+
continue;
|
|
13725
13727
|
const sessionId = `${session.platformId}:${session.threadId}`;
|
|
13726
13728
|
sessions.set(sessionId, session);
|
|
13727
13729
|
}
|
|
13728
|
-
log3.debug(`Loaded ${sessions.size} session(s)`);
|
|
13730
|
+
log3.debug(`Loaded ${sessions.size} active session(s)`);
|
|
13729
13731
|
} catch (err) {
|
|
13730
13732
|
log3.error(`Failed to load sessions: ${err}`);
|
|
13731
13733
|
}
|
|
@@ -13747,23 +13749,67 @@ class SessionStore {
|
|
|
13747
13749
|
log3.debug(`Removed session ${shortId}...`);
|
|
13748
13750
|
}
|
|
13749
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
|
+
}
|
|
13750
13761
|
cleanStale(maxAgeMs) {
|
|
13751
13762
|
const data = this.loadRaw();
|
|
13752
13763
|
const now = Date.now();
|
|
13753
13764
|
const staleIds = [];
|
|
13754
13765
|
for (const [sessionId, session] of Object.entries(data.sessions)) {
|
|
13766
|
+
if (session.cleanedAt)
|
|
13767
|
+
continue;
|
|
13755
13768
|
const lastActivity = new Date(session.lastActivityAt).getTime();
|
|
13756
13769
|
if (now - lastActivity > maxAgeMs) {
|
|
13757
13770
|
staleIds.push(sessionId);
|
|
13758
|
-
|
|
13771
|
+
session.cleanedAt = new Date().toISOString();
|
|
13759
13772
|
}
|
|
13760
13773
|
}
|
|
13761
13774
|
if (staleIds.length > 0) {
|
|
13762
13775
|
this.writeAtomic(data);
|
|
13763
|
-
log3.debug(`
|
|
13776
|
+
log3.debug(`Soft-deleted ${staleIds.length} stale session(s)`);
|
|
13764
13777
|
}
|
|
13765
13778
|
return staleIds;
|
|
13766
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
|
+
}
|
|
13767
13813
|
clear() {
|
|
13768
13814
|
const data = this.loadRaw();
|
|
13769
13815
|
this.writeAtomic({ version: STORE_VERSION, sessions: {}, stickyPostIds: data.stickyPostIds });
|
|
@@ -15503,12 +15549,35 @@ function updateUsageStats(session, event, ctx) {
|
|
|
15503
15549
|
const STATUS_BAR_UPDATE_INTERVAL = 30000;
|
|
15504
15550
|
session.statusBarTimer = setInterval(() => {
|
|
15505
15551
|
if (session.claude.isRunning()) {
|
|
15552
|
+
updateUsageFromStatusLine(session);
|
|
15506
15553
|
ctx.ops.updateSessionHeader(session).catch(() => {});
|
|
15507
15554
|
}
|
|
15508
15555
|
}, STATUS_BAR_UPDATE_INTERVAL);
|
|
15509
15556
|
}
|
|
15510
15557
|
ctx.ops.updateSessionHeader(session).catch(() => {});
|
|
15511
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
|
+
}
|
|
15512
15581
|
|
|
15513
15582
|
// src/session/reactions.ts
|
|
15514
15583
|
var log7 = createLogger("reactions");
|
|
@@ -15660,6 +15729,7 @@ import { spawn } from "child_process";
|
|
|
15660
15729
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
15661
15730
|
import { resolve as resolve2, dirname as dirname2 } from "path";
|
|
15662
15731
|
import { fileURLToPath } from "url";
|
|
15732
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, watchFile, unwatchFile, unlinkSync } from "fs";
|
|
15663
15733
|
var log8 = createLogger("claude");
|
|
15664
15734
|
|
|
15665
15735
|
class ClaudeCli extends EventEmitter2 {
|
|
@@ -15667,10 +15737,48 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
15667
15737
|
options;
|
|
15668
15738
|
buffer = "";
|
|
15669
15739
|
debug = process.env.DEBUG === "1" || process.argv.includes("--debug");
|
|
15740
|
+
statusFilePath = null;
|
|
15741
|
+
lastStatusData = null;
|
|
15670
15742
|
constructor(options) {
|
|
15671
15743
|
super();
|
|
15672
15744
|
this.options = options;
|
|
15673
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
|
+
}
|
|
15674
15782
|
start() {
|
|
15675
15783
|
if (this.process)
|
|
15676
15784
|
throw new Error("Already running");
|
|
@@ -15725,6 +15833,18 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
15725
15833
|
if (this.options.appendSystemPrompt) {
|
|
15726
15834
|
args.push("--append-system-prompt", this.options.appendSystemPrompt);
|
|
15727
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
|
+
}
|
|
15728
15848
|
log8.debug(`Starting: ${claudePath} ${args.slice(0, 5).join(" ")}...`);
|
|
15729
15849
|
this.process = spawn(claudePath, args, {
|
|
15730
15850
|
cwd: this.options.workingDir,
|
|
@@ -15800,6 +15920,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
15800
15920
|
return this.process !== null;
|
|
15801
15921
|
}
|
|
15802
15922
|
kill() {
|
|
15923
|
+
this.stopStatusWatch();
|
|
15803
15924
|
this.process?.kill("SIGTERM");
|
|
15804
15925
|
this.process = null;
|
|
15805
15926
|
}
|
|
@@ -15814,12 +15935,17 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
15814
15935
|
const __dirname2 = dirname2(__filename2);
|
|
15815
15936
|
return resolve2(__dirname2, "..", "mcp", "permission-server.js");
|
|
15816
15937
|
}
|
|
15938
|
+
getStatusLineWriterPath() {
|
|
15939
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
15940
|
+
const __dirname2 = dirname2(__filename2);
|
|
15941
|
+
return resolve2(__dirname2, "..", "statusline", "writer.js");
|
|
15942
|
+
}
|
|
15817
15943
|
}
|
|
15818
15944
|
|
|
15819
15945
|
// src/session/commands.ts
|
|
15820
|
-
import { randomUUID } from "crypto";
|
|
15946
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
15821
15947
|
import { resolve as resolve5 } from "path";
|
|
15822
|
-
import { existsSync as
|
|
15948
|
+
import { existsSync as existsSync7, statSync } from "fs";
|
|
15823
15949
|
|
|
15824
15950
|
// node_modules/update-notifier/update-notifier.js
|
|
15825
15951
|
import process10 from "process";
|
|
@@ -19110,7 +19236,7 @@ function updateNotifier(options) {
|
|
|
19110
19236
|
var import_semver2 = __toESM(require_semver2(), 1);
|
|
19111
19237
|
|
|
19112
19238
|
// src/version.ts
|
|
19113
|
-
import { readFileSync as
|
|
19239
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
19114
19240
|
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
19115
19241
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
19116
19242
|
var __dirname4 = dirname3(fileURLToPath4(import.meta.url));
|
|
@@ -19121,9 +19247,9 @@ function loadPackageJson() {
|
|
|
19121
19247
|
resolve3(process.cwd(), "package.json")
|
|
19122
19248
|
];
|
|
19123
19249
|
for (const candidate of candidates) {
|
|
19124
|
-
if (
|
|
19250
|
+
if (existsSync5(candidate)) {
|
|
19125
19251
|
try {
|
|
19126
|
-
const pkg = JSON.parse(
|
|
19252
|
+
const pkg = JSON.parse(readFileSync5(candidate, "utf-8"));
|
|
19127
19253
|
if (pkg.name === "claude-threads") {
|
|
19128
19254
|
return { version: pkg.version, name: pkg.name };
|
|
19129
19255
|
}
|
|
@@ -19166,7 +19292,7 @@ function getUpdateInfo() {
|
|
|
19166
19292
|
}
|
|
19167
19293
|
|
|
19168
19294
|
// src/changelog.ts
|
|
19169
|
-
import { readFileSync as
|
|
19295
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
19170
19296
|
import { dirname as dirname4, resolve as resolve4 } from "path";
|
|
19171
19297
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
19172
19298
|
var __dirname5 = dirname4(fileURLToPath5(import.meta.url));
|
|
@@ -19177,7 +19303,7 @@ function getReleaseNotes(version) {
|
|
|
19177
19303
|
];
|
|
19178
19304
|
let changelogPath = null;
|
|
19179
19305
|
for (const p of possiblePaths) {
|
|
19180
|
-
if (
|
|
19306
|
+
if (existsSync6(p)) {
|
|
19181
19307
|
changelogPath = p;
|
|
19182
19308
|
break;
|
|
19183
19309
|
}
|
|
@@ -19186,7 +19312,7 @@ function getReleaseNotes(version) {
|
|
|
19186
19312
|
return null;
|
|
19187
19313
|
}
|
|
19188
19314
|
try {
|
|
19189
|
-
const content =
|
|
19315
|
+
const content = readFileSync6(changelogPath, "utf-8");
|
|
19190
19316
|
return parseChangelog(content, version);
|
|
19191
19317
|
} catch {
|
|
19192
19318
|
return null;
|
|
@@ -19604,6 +19730,170 @@ async function postUser(session, message) {
|
|
|
19604
19730
|
return session.platform.createPost(`\uD83D\uDC64 ${message}`, session.threadId);
|
|
19605
19731
|
}
|
|
19606
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
|
+
|
|
19607
19897
|
// src/session/commands.ts
|
|
19608
19898
|
var log9 = createLogger("commands");
|
|
19609
19899
|
async function restartClaudeSession(session, cliOptions, ctx, actionName) {
|
|
@@ -19675,7 +19965,7 @@ async function changeDirectory(session, newDir, username, ctx) {
|
|
|
19675
19965
|
}
|
|
19676
19966
|
const expandedDir = newDir.startsWith("~") ? newDir.replace("~", process.env.HOME || "") : newDir;
|
|
19677
19967
|
const absoluteDir = resolve5(expandedDir);
|
|
19678
|
-
if (!
|
|
19968
|
+
if (!existsSync7(absoluteDir)) {
|
|
19679
19969
|
await postError(session, `Directory does not exist: \`${newDir}\``);
|
|
19680
19970
|
return;
|
|
19681
19971
|
}
|
|
@@ -19687,7 +19977,7 @@ async function changeDirectory(session, newDir, username, ctx) {
|
|
|
19687
19977
|
const shortDir = absoluteDir.replace(process.env.HOME || "", "~");
|
|
19688
19978
|
log9.info(`\uD83D\uDCC2 Session (${shortId}\u2026) changing directory to ${shortDir}`);
|
|
19689
19979
|
session.workingDir = absoluteDir;
|
|
19690
|
-
const newSessionId =
|
|
19980
|
+
const newSessionId = randomUUID2();
|
|
19691
19981
|
session.claudeSessionId = newSessionId;
|
|
19692
19982
|
const cliOptions = {
|
|
19693
19983
|
workingDir: absoluteDir,
|
|
@@ -19846,6 +20136,14 @@ async function updateSessionHeader(session, ctx) {
|
|
|
19846
20136
|
if (session.worktreeInfo) {
|
|
19847
20137
|
const shortRepoRoot = session.worktreeInfo.repoRoot.replace(process.env.HOME || "", "~");
|
|
19848
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
|
+
}
|
|
19849
20147
|
}
|
|
19850
20148
|
if (session.pullRequestUrl) {
|
|
19851
20149
|
rows.push(`| \uD83D\uDD17 **Pull Request** | ${formatPullRequestLink(session.pullRequestUrl)} |`);
|
|
@@ -19879,8 +20177,8 @@ async function updateSessionHeader(session, ctx) {
|
|
|
19879
20177
|
}
|
|
19880
20178
|
|
|
19881
20179
|
// src/session/lifecycle.ts
|
|
19882
|
-
import { randomUUID as
|
|
19883
|
-
import { existsSync as
|
|
20180
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
20181
|
+
import { existsSync as existsSync8 } from "fs";
|
|
19884
20182
|
var log10 = createLogger("lifecycle");
|
|
19885
20183
|
function mutableSessions(ctx) {
|
|
19886
20184
|
return ctx.state.sessions;
|
|
@@ -20003,7 +20301,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
20003
20301
|
return;
|
|
20004
20302
|
const actualThreadId = replyToPostId || post.id;
|
|
20005
20303
|
const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
|
|
20006
|
-
const claudeSessionId =
|
|
20304
|
+
const claudeSessionId = randomUUID3();
|
|
20007
20305
|
const platformMcpConfig = platform.getMcpConfig();
|
|
20008
20306
|
const cliOptions = {
|
|
20009
20307
|
workingDir: ctx.config.workingDir,
|
|
@@ -20112,7 +20410,7 @@ async function resumeSession(state, ctx) {
|
|
|
20112
20410
|
log10.warn(`Max sessions reached, skipping resume for ${shortId}...`);
|
|
20113
20411
|
return;
|
|
20114
20412
|
}
|
|
20115
|
-
if (!
|
|
20413
|
+
if (!existsSync8(state.workingDir)) {
|
|
20116
20414
|
log10.warn(`Working directory ${state.workingDir} no longer exists, skipping resume for ${shortId}...`);
|
|
20117
20415
|
ctx.state.sessionStore.remove(`${state.platformId}:${state.threadId}`);
|
|
20118
20416
|
await withErrorHandling(() => platform.createPost(`\u26A0\uFE0F **Cannot resume session** - working directory no longer exists:
|
|
@@ -20371,162 +20669,6 @@ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
|
|
|
20371
20669
|
}
|
|
20372
20670
|
}
|
|
20373
20671
|
|
|
20374
|
-
// src/git/worktree.ts
|
|
20375
|
-
import { spawn as spawn4 } from "child_process";
|
|
20376
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
20377
|
-
import * as path9 from "path";
|
|
20378
|
-
import * as fs5 from "fs/promises";
|
|
20379
|
-
async function execGit(args, cwd) {
|
|
20380
|
-
return new Promise((resolve6, reject) => {
|
|
20381
|
-
const proc = spawn4("git", args, { cwd });
|
|
20382
|
-
let stdout = "";
|
|
20383
|
-
let stderr = "";
|
|
20384
|
-
proc.stdout.on("data", (data) => {
|
|
20385
|
-
stdout += data.toString();
|
|
20386
|
-
});
|
|
20387
|
-
proc.stderr.on("data", (data) => {
|
|
20388
|
-
stderr += data.toString();
|
|
20389
|
-
});
|
|
20390
|
-
proc.on("close", (code) => {
|
|
20391
|
-
if (code === 0) {
|
|
20392
|
-
resolve6(stdout.trim());
|
|
20393
|
-
} else {
|
|
20394
|
-
reject(new Error(`git ${args.join(" ")} failed: ${stderr || stdout}`));
|
|
20395
|
-
}
|
|
20396
|
-
});
|
|
20397
|
-
proc.on("error", (err) => {
|
|
20398
|
-
reject(err);
|
|
20399
|
-
});
|
|
20400
|
-
});
|
|
20401
|
-
}
|
|
20402
|
-
async function isGitRepository(dir) {
|
|
20403
|
-
try {
|
|
20404
|
-
await execGit(["rev-parse", "--git-dir"], dir);
|
|
20405
|
-
return true;
|
|
20406
|
-
} catch {
|
|
20407
|
-
return false;
|
|
20408
|
-
}
|
|
20409
|
-
}
|
|
20410
|
-
async function getRepositoryRoot(dir) {
|
|
20411
|
-
return execGit(["rev-parse", "--show-toplevel"], dir);
|
|
20412
|
-
}
|
|
20413
|
-
async function hasUncommittedChanges(dir) {
|
|
20414
|
-
try {
|
|
20415
|
-
const staged = await execGit(["diff", "--cached", "--quiet"], dir).catch(() => "changes");
|
|
20416
|
-
if (staged === "changes")
|
|
20417
|
-
return true;
|
|
20418
|
-
const unstaged = await execGit(["diff", "--quiet"], dir).catch(() => "changes");
|
|
20419
|
-
if (unstaged === "changes")
|
|
20420
|
-
return true;
|
|
20421
|
-
const untracked = await execGit(["ls-files", "--others", "--exclude-standard"], dir);
|
|
20422
|
-
return untracked.length > 0;
|
|
20423
|
-
} catch {
|
|
20424
|
-
return false;
|
|
20425
|
-
}
|
|
20426
|
-
}
|
|
20427
|
-
async function listWorktrees(repoRoot) {
|
|
20428
|
-
const output = await execGit(["worktree", "list", "--porcelain"], repoRoot);
|
|
20429
|
-
const worktrees = [];
|
|
20430
|
-
if (!output)
|
|
20431
|
-
return worktrees;
|
|
20432
|
-
const blocks = output.split(`
|
|
20433
|
-
|
|
20434
|
-
`).filter(Boolean);
|
|
20435
|
-
for (const block of blocks) {
|
|
20436
|
-
const lines = block.split(`
|
|
20437
|
-
`);
|
|
20438
|
-
const worktree = {};
|
|
20439
|
-
for (const line of lines) {
|
|
20440
|
-
if (line.startsWith("worktree ")) {
|
|
20441
|
-
worktree.path = line.slice(9);
|
|
20442
|
-
} else if (line.startsWith("HEAD ")) {
|
|
20443
|
-
worktree.commit = line.slice(5);
|
|
20444
|
-
} else if (line.startsWith("branch ")) {
|
|
20445
|
-
worktree.branch = line.slice(7).replace("refs/heads/", "");
|
|
20446
|
-
} else if (line === "bare") {
|
|
20447
|
-
worktree.isBare = true;
|
|
20448
|
-
} else if (line === "detached") {
|
|
20449
|
-
worktree.branch = "(detached)";
|
|
20450
|
-
}
|
|
20451
|
-
}
|
|
20452
|
-
if (worktree.path) {
|
|
20453
|
-
worktrees.push({
|
|
20454
|
-
path: worktree.path,
|
|
20455
|
-
branch: worktree.branch || "(unknown)",
|
|
20456
|
-
commit: worktree.commit || "",
|
|
20457
|
-
isMain: worktrees.length === 0,
|
|
20458
|
-
isBare: worktree.isBare || false
|
|
20459
|
-
});
|
|
20460
|
-
}
|
|
20461
|
-
}
|
|
20462
|
-
return worktrees;
|
|
20463
|
-
}
|
|
20464
|
-
async function branchExists(repoRoot, branch) {
|
|
20465
|
-
try {
|
|
20466
|
-
await execGit(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot);
|
|
20467
|
-
return true;
|
|
20468
|
-
} catch {
|
|
20469
|
-
try {
|
|
20470
|
-
await execGit(["rev-parse", "--verify", `refs/remotes/origin/${branch}`], repoRoot);
|
|
20471
|
-
return true;
|
|
20472
|
-
} catch {
|
|
20473
|
-
return false;
|
|
20474
|
-
}
|
|
20475
|
-
}
|
|
20476
|
-
}
|
|
20477
|
-
function getWorktreeDir(repoRoot, branch) {
|
|
20478
|
-
const repoName = path9.basename(repoRoot);
|
|
20479
|
-
const parentDir = path9.dirname(repoRoot);
|
|
20480
|
-
const worktreesDir = path9.join(parentDir, `${repoName}-worktrees`);
|
|
20481
|
-
const sanitizedBranch = branch.replace(/\//g, "-").replace(/[^a-zA-Z0-9-_]/g, "");
|
|
20482
|
-
const shortUuid = randomUUID3().slice(0, 8);
|
|
20483
|
-
return path9.join(worktreesDir, `${sanitizedBranch}-${shortUuid}`);
|
|
20484
|
-
}
|
|
20485
|
-
async function createWorktree(repoRoot, branch, targetDir) {
|
|
20486
|
-
const parentDir = path9.dirname(targetDir);
|
|
20487
|
-
await fs5.mkdir(parentDir, { recursive: true });
|
|
20488
|
-
const exists = await branchExists(repoRoot, branch);
|
|
20489
|
-
if (exists) {
|
|
20490
|
-
await execGit(["worktree", "add", targetDir, branch], repoRoot);
|
|
20491
|
-
} else {
|
|
20492
|
-
await execGit(["worktree", "add", "-b", branch, targetDir], repoRoot);
|
|
20493
|
-
}
|
|
20494
|
-
return targetDir;
|
|
20495
|
-
}
|
|
20496
|
-
async function removeWorktree(repoRoot, worktreePath) {
|
|
20497
|
-
try {
|
|
20498
|
-
await execGit(["worktree", "remove", worktreePath], repoRoot);
|
|
20499
|
-
} catch {
|
|
20500
|
-
await execGit(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
20501
|
-
}
|
|
20502
|
-
await execGit(["worktree", "prune"], repoRoot);
|
|
20503
|
-
}
|
|
20504
|
-
async function findWorktreeByBranch(repoRoot, branch) {
|
|
20505
|
-
const worktrees = await listWorktrees(repoRoot);
|
|
20506
|
-
return worktrees.find((wt) => wt.branch === branch) || null;
|
|
20507
|
-
}
|
|
20508
|
-
function isValidBranchName(name) {
|
|
20509
|
-
if (!name || name.length === 0)
|
|
20510
|
-
return false;
|
|
20511
|
-
if (name.startsWith("/") || name.endsWith("/"))
|
|
20512
|
-
return false;
|
|
20513
|
-
if (name.includes(".."))
|
|
20514
|
-
return false;
|
|
20515
|
-
if (/[\s~^:?*[\]\\]/.test(name))
|
|
20516
|
-
return false;
|
|
20517
|
-
if (name.startsWith("-"))
|
|
20518
|
-
return false;
|
|
20519
|
-
if (name.endsWith(".lock"))
|
|
20520
|
-
return false;
|
|
20521
|
-
if (name.includes("@{"))
|
|
20522
|
-
return false;
|
|
20523
|
-
if (name === "@")
|
|
20524
|
-
return false;
|
|
20525
|
-
if (/\.\./.test(name))
|
|
20526
|
-
return false;
|
|
20527
|
-
return true;
|
|
20528
|
-
}
|
|
20529
|
-
|
|
20530
20672
|
// src/session/worktree.ts
|
|
20531
20673
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
20532
20674
|
var log11 = createLogger("worktree");
|
|
@@ -21094,6 +21236,26 @@ function getSessionTopic(session) {
|
|
|
21094
21236
|
}
|
|
21095
21237
|
return formatTopicFromPrompt(session.firstPrompt);
|
|
21096
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
|
+
}
|
|
21097
21259
|
async function buildStatusBar(sessionCount, config) {
|
|
21098
21260
|
const items = [];
|
|
21099
21261
|
items.push(`\`v${VERSION}\``);
|
|
@@ -21137,17 +21299,27 @@ function formatTopicFromPrompt(prompt) {
|
|
|
21137
21299
|
async function buildStickyMessage(sessions, platformId, config) {
|
|
21138
21300
|
const platformSessions = [...sessions.values()].filter((s) => s.platformId === platformId);
|
|
21139
21301
|
const statusBar = await buildStatusBar(platformSessions.length, config);
|
|
21302
|
+
const historySessions = sessionStore ? sessionStore.getHistory(platformId).slice(0, 5) : [];
|
|
21140
21303
|
if (platformSessions.length === 0) {
|
|
21141
|
-
|
|
21304
|
+
const lines2 = [
|
|
21142
21305
|
"---",
|
|
21143
21306
|
statusBar,
|
|
21144
21307
|
"",
|
|
21145
21308
|
"**Active Claude Threads**",
|
|
21146
21309
|
"",
|
|
21147
|
-
"_No active sessions_"
|
|
21148
|
-
|
|
21149
|
-
|
|
21150
|
-
|
|
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(`
|
|
21151
21323
|
`);
|
|
21152
21324
|
}
|
|
21153
21325
|
platformSessions.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
|
|
@@ -21180,6 +21352,14 @@ async function buildStickyMessage(sessions, platformId, config) {
|
|
|
21180
21352
|
lines.push(` \uD83D\uDD04 _${activeTask}_`);
|
|
21181
21353
|
}
|
|
21182
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
|
+
}
|
|
21183
21363
|
lines.push("");
|
|
21184
21364
|
lines.push("_Mention me to start a session_ \xB7 `npm i -g claude-threads`");
|
|
21185
21365
|
return lines.join(`
|
|
@@ -21613,7 +21793,7 @@ class SessionManager {
|
|
|
21613
21793
|
this.sessionStore.save(session.sessionId, state);
|
|
21614
21794
|
}
|
|
21615
21795
|
unpersistSession(sessionId) {
|
|
21616
|
-
this.sessionStore.
|
|
21796
|
+
this.sessionStore.softDelete(sessionId);
|
|
21617
21797
|
}
|
|
21618
21798
|
async updateSessionHeader(session) {
|
|
21619
21799
|
await updateSessionHeader(session, this.getContext());
|
|
@@ -21640,7 +21820,11 @@ class SessionManager {
|
|
|
21640
21820
|
}
|
|
21641
21821
|
const staleIds = this.sessionStore.cleanStale(SESSION_TIMEOUT_MS * 2);
|
|
21642
21822
|
if (staleIds.length > 0) {
|
|
21643
|
-
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`);
|
|
21644
21828
|
}
|
|
21645
21829
|
const persisted = this.sessionStore.load();
|
|
21646
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",
|