Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions 29 plugins/cc/bin/cc-quick
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
CC_DIR="${CC_STATE_DIR:-$CLAUDE_DIR/channels/cc}"
DB="$CC_DIR/sessions.db"
INBOX="$CC_DIR/inbox"
STALE_SESSION_AFTER_MS="${CC_STALE_SESSION_MS:-300000}"

die() { echo "cc-quick: $*" >&2; exit 1; }
env_err() { echo "cc-quick: $*" >&2; exit 2; }

[ -f "$DB" ] || env_err "sessions.db not at $DB — install/start cc first"
[[ "$STALE_SESSION_AFTER_MS" =~ ^[1-9][0-9]*$ ]] ||
env_err "CC_STALE_SESSION_MS must be a positive integer"
MY_SID="${CLAUDE_CODE_SESSION_ID:-${CLAUDE_SESSION_ID:-}}"
[ -n "$MY_SID" ] || env_err "CLAUDE_CODE_SESSION_ID not set; can't identify self"
MY_SHORT="${MY_SID:0:8}"
Expand All @@ -34,6 +37,19 @@ MY_CWD_BASENAME="$(basename "$PWD")"
# ms-precision timestamp (date +%s%N is ns; trim to 13 chars for ms)
now_ms() { date +%s%N | cut -c1-13; }

stale_cutoff_ms() {
printf '%s\n' "$(( $(now_ms) - STALE_SESSION_AFTER_MS ))"
}

# Dirty exits leave ended_at_ms unset. Reconcile old rows before any command
# treats the database as a live roster. Historical rows remain queryable.
reconcile_stale_sessions() {
local cutoff
cutoff="$(stale_cutoff_ms)"
sqlite3 "$DB" \
"UPDATE sessions SET ended_at_ms = last_seen_at_ms WHERE ended_at_ms IS NULL AND last_seen_at_ms <= $cutoff;"
}

# urlsafe random hex of length N. uuidgen if available else /dev/urandom.
rand_hex() {
if command -v uuidgen >/dev/null 2>&1; then
Expand All @@ -47,25 +63,32 @@ rand_hex() {
# session_id. Echos the full id, or empty string if not found.
resolve_target() {
local ref="$1"
local cutoff
cutoff="$(stale_cutoff_ms)"
# try short id first (8 hex), then full id, then cwd basename. Prefer
# most-recently-active match on ambiguity.
sqlite3 "$DB" <<SQL | head -1
SELECT id FROM sessions
WHERE ended_at_ms IS NULL
AND last_seen_at_ms > $cutoff
AND (substr(id,1,8) = '$ref' OR id = '$ref' OR cwd LIKE '%/$ref')
ORDER BY last_seen_at_ms DESC
LIMIT 1;
SQL
}

cmd_roster() {
local cutoff
reconcile_stale_sessions
cutoff="$(stale_cutoff_ms)"
sqlite3 -header -column "$DB" <<SQL
SELECT substr(id,1,8) AS sid,
cwd,
COALESCE(branch, '') AS branch,
datetime(last_seen_at_ms/1000, 'unixepoch', 'localtime') AS last_seen
FROM sessions
WHERE ended_at_ms IS NULL
AND last_seen_at_ms > $cutoff
ORDER BY last_seen_at_ms DESC;
SQL
}
Expand All @@ -76,6 +99,7 @@ cmd_send() {
local subject="${3:-}"
[ -n "$target_ref" ] || die "send: missing target (short id, full id, or cwd basename)"
[ -n "$message" ] || die "send: missing message"
reconcile_stale_sessions

local target_sid
target_sid=$(resolve_target "$target_ref")
Expand Down Expand Up @@ -128,7 +152,10 @@ SQL
cmd_check() {
local since_s="${1:-1800}" # default 30min
local since_ms
local cutoff
reconcile_stale_sessions
since_ms=$(( $(now_ms) - since_s * 1000 ))
cutoff="$(stale_cutoff_ms)"

echo "=== announcements (last ${since_s}s, peers only) ==="
sqlite3 -header -column "$DB" <<SQL
Expand All @@ -138,6 +165,7 @@ SELECT substr(a.session_id,1,8) AS sid,
FROM announcements a
JOIN sessions s ON s.id = a.session_id
WHERE s.ended_at_ms IS NULL
AND s.last_seen_at_ms > $cutoff
AND a.session_id != '$MY_SID'
AND a.created_at_ms > $since_ms
ORDER BY a.created_at_ms DESC LIMIT 20;
Expand All @@ -151,6 +179,7 @@ SELECT substr(rf.session_id,1,8) AS sid,
FROM recent_files rf
JOIN sessions s ON s.id = rf.session_id
WHERE s.ended_at_ms IS NULL
AND s.last_seen_at_ms > $cutoff
AND rf.session_id != '$MY_SID'
AND rf.touched_at_ms > $since_ms
ORDER BY rf.touched_at_ms DESC LIMIT 20;
Expand Down
113 changes: 113 additions & 0 deletions 113 plugins/cc/tests/cc-quick.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// tested with: claude code v2.1.133 + bun 1.3

import { Database } from "bun:sqlite";
import { afterEach, describe, expect, it } from "bun:test";
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const fixtures: string[] = [];
const script = join(import.meta.dir, "..", "bin", "cc-quick");

afterEach(() => {
for (const fixture of fixtures.splice(0)) {
rmSync(fixture, { recursive: true, force: true });
}
});

function makeFixture(): {
root: string;
dbPath: string;
now: number;
} {
const root = mkdtempSync(join(tmpdir(), "cc-quick-"));
fixtures.push(root);
const ccDir = join(root, "channels", "cc");
mkdirSync(ccDir, { recursive: true });
const dbPath = join(ccDir, "sessions.db");
const db = new Database(dbPath);
db.exec(`
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
name TEXT,
cwd TEXT,
project_root TEXT,
branch TEXT,
worktree_root TEXT,
role TEXT,
pid INTEGER,
started_at_ms INTEGER NOT NULL,
last_seen_at_ms INTEGER NOT NULL,
last_checked_at_ms INTEGER,
ended_at_ms INTEGER
);
CREATE TABLE announcements (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
summary TEXT NOT NULL,
detail TEXT,
created_at_ms INTEGER NOT NULL
);
CREATE TABLE recent_files (
session_id TEXT NOT NULL,
path TEXT NOT NULL,
touched_at_ms INTEGER NOT NULL
);
`);
const now = Date.now();
const insert = db.prepare(
`INSERT INTO sessions
(id, cwd, branch, started_at_ms, last_seen_at_ms, ended_at_ms)
VALUES (?, ?, ?, ?, ?, NULL)`,
);
insert.run("fresh-session", "/tmp/fresh", "main", now - 1000, now - 1000);
insert.run("stale-session", "/tmp/stale", "main", now - 600000, now - 600000);
db.close();
return { root, dbPath, now };
}

describe("cc-quick live roster", () => {
it("shows fresh rows and marks stale rows ended", () => {
const fixture = makeFixture();
const result = Bun.spawnSync(["bash", script, "roster"], {
env: {
...process.env,
CLAUDE_CODE_SESSION_ID: "fixture-caller",
CLAUDE_CONFIG_DIR: fixture.root,
CC_STALE_SESSION_MS: "300000",
},
});

expect(result.exitCode).toBe(0);
const output = result.stdout.toString();
expect(output).toContain("fresh-se");
expect(output).not.toContain("stale-se");

const db = new Database(fixture.dbPath);
const stale = db
.query("SELECT last_seen_at_ms, ended_at_ms FROM sessions WHERE id = ?")
.get("stale-session") as {
last_seen_at_ms: number;
ended_at_ms: number | null;
};
expect(stale.ended_at_ms).toBe(stale.last_seen_at_ms);
db.close();
});

it("rejects a non-numeric stale-session window", () => {
const fixture = makeFixture();
const result = Bun.spawnSync(["bash", script, "roster"], {
env: {
...process.env,
CLAUDE_CODE_SESSION_ID: "fixture-caller",
CLAUDE_CONFIG_DIR: fixture.root,
CC_STALE_SESSION_MS: "unsafe",
},
});

expect(result.exitCode).toBe(2);
expect(result.stderr.toString()).toContain(
"CC_STALE_SESSION_MS must be a positive integer",
);
});
});
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.