vibecoder-discord-presence 1.0.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.
@@ -0,0 +1,114 @@
1
+ import {
2
+ removeSessionMarker,
3
+ updateSessionMarker
4
+ } from "./chunk-4V354UFY.js";
5
+ import {
6
+ isProcessAlive,
7
+ readLock,
8
+ spawnDaemon
9
+ } from "./chunk-KDLAS6ED.js";
10
+ import "./chunk-FRGALQ5R.js";
11
+
12
+ // src/provider/claude-code.ts
13
+ import { readFileSync } from "fs";
14
+ import { basename } from "path";
15
+ function readStdin() {
16
+ try {
17
+ return readFileSync(0, "utf8");
18
+ } catch {
19
+ return "";
20
+ }
21
+ }
22
+ function parsePayload(raw) {
23
+ if (!raw) return null;
24
+ try {
25
+ const clean = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
26
+ return JSON.parse(clean);
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ function fileFromInput(input) {
32
+ const fp = input?.file_path;
33
+ return typeof fp === "string" ? basename(fp) : void 0;
34
+ }
35
+ function toolActivity(payload) {
36
+ const tool = payload.tool_name ?? "";
37
+ const file = fileFromInput(payload.tool_input);
38
+ switch (tool) {
39
+ case "Edit":
40
+ case "Write":
41
+ case "MultiEdit":
42
+ case "NotebookEdit":
43
+ return { state: "editing", activity: file ? `Editing ${file}` : "Editing", file };
44
+ case "Read":
45
+ return { state: "searching", activity: file ? `Reading ${file}` : "Reading", file };
46
+ case "Bash":
47
+ return { state: "running", activity: "Running a command" };
48
+ case "Grep":
49
+ case "Glob":
50
+ case "LS":
51
+ return { state: "searching", activity: "Searching the codebase" };
52
+ case "WebFetch":
53
+ case "WebSearch":
54
+ return { state: "browsing", activity: "Browsing the web" };
55
+ case "Task":
56
+ case "Agent":
57
+ return { state: "delegating", activity: "Running a subagent" };
58
+ default:
59
+ return { state: "running", activity: tool ? `Using ${tool}` : "Working" };
60
+ }
61
+ }
62
+ function activityFor(event, payload) {
63
+ switch (event) {
64
+ case "session-start":
65
+ return { state: "idle", activity: "Starting a session" };
66
+ case "user-prompt-submit":
67
+ return { state: "thinking", activity: "Thinking" };
68
+ case "pre-tool-use":
69
+ return toolActivity(payload);
70
+ case "notification":
71
+ return payload.message?.toLowerCase().includes("permission") ? { state: "waiting", activity: "Waiting for permission" } : { state: "idle", activity: "Idle" };
72
+ case "stop":
73
+ return { state: "idle", activity: "Idle" };
74
+ default:
75
+ return { state: "idle", activity: "Working" };
76
+ }
77
+ }
78
+ function ensureDaemon() {
79
+ const lock = readLock();
80
+ if (lock && isProcessAlive(lock.pid)) return;
81
+ spawnDaemon();
82
+ }
83
+ async function runHook(args = []) {
84
+ try {
85
+ const event = args[0] ?? "unknown";
86
+ const payload = parsePayload(readStdin()) ?? {};
87
+ const id = payload.session_id ?? process.env.CLAUDE_CODE_SESSION_ID;
88
+ if (!id) return;
89
+ if (event === "session-end") {
90
+ removeSessionMarker(id);
91
+ return;
92
+ }
93
+ const now = Date.now();
94
+ const cwd = payload.cwd ?? process.cwd();
95
+ const a = activityFor(event, payload);
96
+ const patch = {
97
+ cwd,
98
+ project: basename(cwd),
99
+ transcriptPath: payload.transcript_path,
100
+ state: a.state,
101
+ activity: a.activity,
102
+ file: a.file
103
+ };
104
+ if (event === "session-start" && payload.source !== "compact") {
105
+ patch.startedAt = now;
106
+ }
107
+ updateSessionMarker(id, patch, now);
108
+ ensureDaemon();
109
+ } catch {
110
+ }
111
+ }
112
+ export {
113
+ runHook
114
+ };
@@ -0,0 +1,245 @@
1
+ import {
2
+ renderPresence
3
+ } from "./chunk-XYXO7VLI.js";
4
+ import {
5
+ DEFAULT_CONFIG,
6
+ THEMES,
7
+ readUserConfig,
8
+ resolveTheme,
9
+ saveUserConfig
10
+ } from "./chunk-2A7GKQFO.js";
11
+ import {
12
+ ui
13
+ } from "./chunk-LKE6H3YG.js";
14
+ import {
15
+ isProcessAlive,
16
+ readLock
17
+ } from "./chunk-KDLAS6ED.js";
18
+ import {
19
+ configPath
20
+ } from "./chunk-FRGALQ5R.js";
21
+
22
+ // src/commands/config.ts
23
+ import { Separator, confirm, input, select } from "@inquirer/prompts";
24
+ var APP_NAME = "ClaudeCode";
25
+ function sampleState(now) {
26
+ return {
27
+ sessionCount: 1,
28
+ startedAt: now - 83e3,
29
+ // ~1m 23s ago
30
+ project: "my-app",
31
+ branch: "main",
32
+ model: "Opus 4.8",
33
+ state: "editing",
34
+ activity: "Editing index.ts",
35
+ file: "index.ts",
36
+ tokens: 12345,
37
+ cost: 0.42
38
+ };
39
+ }
40
+ function box(lines, title) {
41
+ const width = Math.max(title.length + 2, ...lines.map((l) => l.length)) + 2;
42
+ const top = ui.dim("\u250C\u2500 ") + ui.title(title) + ui.dim(` ${"\u2500".repeat(Math.max(0, width - title.length - 3))}\u2510`);
43
+ const body = lines.map((l) => `${ui.dim("\u2502")} ${l.padEnd(width - 1)}${ui.dim("\u2502")}`);
44
+ const bottom = ui.dim(`\u2514${"\u2500".repeat(width + 1)}\u2518`);
45
+ return [top, ...body, bottom].join("\n");
46
+ }
47
+ function previewCard(theme) {
48
+ const now = Date.now();
49
+ const p = renderPresence(theme, sampleState(now), now);
50
+ const lines = [];
51
+ lines.push(`icon: ${p.largeImageKey ?? "(none)"}`);
52
+ lines.push("");
53
+ lines.push(APP_NAME);
54
+ if (p.details) lines.push(p.details);
55
+ if (p.state) lines.push(p.state);
56
+ if (p.startTimestamp) lines.push("elapsed 01:23");
57
+ if (p.smallImageKey)
58
+ lines.push(`badge: ${p.smallImageKey}${p.smallImageText ? ` (${p.smallImageText})` : ""}`);
59
+ if (p.buttons?.length) lines.push(p.buttons.map((b) => `[ ${b.label} ]`).join(" "));
60
+ const compact = p.statusDisplayType === 1 ? p.state || `Playing ${APP_NAME}` : p.statusDisplayType === 2 ? p.details || `Playing ${APP_NAME}` : `Playing ${APP_NAME}`;
61
+ return `${box(lines, "Discord card preview")}
62
+ ${ui.dim("member list shows:")} ${ui.accent(compact)}`;
63
+ }
64
+ function short(s, max = 30) {
65
+ const clean = s.replace(/\s+/g, " ").trim();
66
+ return clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean || "(empty)";
67
+ }
68
+ function row(label, value) {
69
+ return `${label.padEnd(28)} ${ui.dim(`[${value}]`)}`;
70
+ }
71
+ async function editButtons(currentButtons) {
72
+ const buttons = [];
73
+ let addMore = await confirm({
74
+ message: "Add a button? (max 2)",
75
+ default: currentButtons.length > 0
76
+ });
77
+ while (addMore && buttons.length < 2) {
78
+ const i = buttons.length;
79
+ const label = await input({
80
+ message: `Button ${i + 1} label:`,
81
+ default: currentButtons[i]?.label ?? ""
82
+ });
83
+ const url = await input({
84
+ message: `Button ${i + 1} URL:`,
85
+ default: currentButtons[i]?.url ?? "",
86
+ validate: (v) => v.trim() === "" || /^https?:\/\//.test(v.trim()) || "Must start with http(s)://"
87
+ });
88
+ if (label.trim() && url.trim()) buttons.push({ label: label.trim(), url: url.trim() });
89
+ addMore = buttons.length < 2 ? await confirm({ message: "Add another?", default: false }) : false;
90
+ }
91
+ return buttons;
92
+ }
93
+ async function editThemeMenu(start) {
94
+ const t = structuredClone(start);
95
+ for (; ; ) {
96
+ console.log(`
97
+ ${previewCard(t)}
98
+ `);
99
+ const choice = await select({
100
+ message: "Pick a field to edit, or save:",
101
+ choices: [
102
+ { name: row("Details \u2014 top line", short(t.details)), value: "details" },
103
+ { name: row("State \u2014 second line", short(t.state)), value: "state" },
104
+ { name: row("Timer \u2014 elapsed clock", t.timer ? "on" : "off"), value: "timer" },
105
+ { name: row("Large image \u2014 big icon", t.largeImage.key), value: "large" },
106
+ { name: row("Small badge \u2014 corner", t.smallImage.key), value: "small" },
107
+ { name: row("Buttons", `${t.buttons.length} set`), value: "buttons" },
108
+ { name: row("Compact status \u2014 member list", t.statusDisplay ?? "name"), value: "status" },
109
+ new Separator(),
110
+ { name: ui.ok("\u2713 Save & exit"), value: "__save" },
111
+ { name: ui.warn("\u2717 Discard changes"), value: "__cancel" }
112
+ ]
113
+ });
114
+ switch (choice) {
115
+ case "__save":
116
+ return t;
117
+ case "__cancel":
118
+ return null;
119
+ case "details":
120
+ t.details = await input({ message: "Details (top line):", default: t.details });
121
+ break;
122
+ case "state":
123
+ t.state = await input({ message: "State (second line):", default: t.state });
124
+ break;
125
+ case "timer":
126
+ t.timer = await confirm({ message: "Show the elapsed timer?", default: t.timer });
127
+ break;
128
+ case "large":
129
+ t.largeImage = {
130
+ key: await input({ message: "Large image asset key:", default: t.largeImage.key }),
131
+ text: await input({ message: "Large image tooltip:", default: t.largeImage.text })
132
+ };
133
+ break;
134
+ case "small":
135
+ t.smallImage = {
136
+ key: await input({ message: "Small badge asset key:", default: t.smallImage.key }),
137
+ text: await input({ message: "Small badge tooltip:", default: t.smallImage.text })
138
+ };
139
+ break;
140
+ case "status":
141
+ t.statusDisplay = await select({
142
+ message: "Compact member-list status shows:",
143
+ default: t.statusDisplay ?? "name",
144
+ choices: [
145
+ { name: 'App name ("Playing ClaudeCode")', value: "name" },
146
+ { name: "The state line (your activity)", value: "state" },
147
+ { name: "The details line", value: "details" }
148
+ ]
149
+ });
150
+ break;
151
+ case "buttons":
152
+ t.buttons = await editButtons(t.buttons);
153
+ break;
154
+ }
155
+ }
156
+ }
157
+ function applyNote() {
158
+ const lock = readLock();
159
+ if (lock && isProcessAlive(lock.pid)) {
160
+ return "Your live card will update within ~15s.";
161
+ }
162
+ return "It will apply next time you start Claude Code with Discord open.";
163
+ }
164
+ async function config(args = []) {
165
+ const current = readUserConfig(configPath());
166
+ if (args.includes("--show")) {
167
+ console.log(`${ui.dim("theme:")} ${ui.accent(current.theme)}
168
+ `);
169
+ console.log(previewCard(resolveTheme(current)));
170
+ return;
171
+ }
172
+ if (args.includes("--reset")) {
173
+ saveUserConfig(DEFAULT_CONFIG);
174
+ console.log(`${ui.check} reset to the default ${ui.accent('"minimal"')} theme.`);
175
+ console.log(` ${ui.dim(applyNote())}`);
176
+ return;
177
+ }
178
+ try {
179
+ console.log(
180
+ `
181
+ ${ui.title("Customize your Discord presence")} ${ui.dim("\u2014 Ctrl+C to cancel")}
182
+ `
183
+ );
184
+ console.log(
185
+ ui.dim(
186
+ "Tip: text fields accept placeholders like {project} {branch} {model} {file} {activity} {elapsed}.\n"
187
+ )
188
+ );
189
+ const theme = await select({
190
+ message: "Pick a theme:",
191
+ default: current.theme,
192
+ choices: [
193
+ { name: "minimal \u2014 privacy-safe, nothing about your work", value: "minimal" },
194
+ { name: "developer \u2014 project, branch, file, model", value: "developer" },
195
+ { name: "focus \u2014 deep-work timer", value: "focus" },
196
+ { name: "playful \u2014 vibey", value: "playful" },
197
+ { name: "chaos \u2014 \u{1F680} every stat, all the emojis, peak vibes", value: "chaos" },
198
+ { name: "custom \u2014 edit every field yourself", value: "custom" }
199
+ ]
200
+ });
201
+ let next;
202
+ if (theme === "custom") {
203
+ const edited = await editThemeMenu(resolveTheme(current));
204
+ if (!edited) {
205
+ console.log(ui.warn("Cancelled \u2014 no changes saved."));
206
+ return;
207
+ }
208
+ next = { theme: "custom", overrides: edited, clientId: current.clientId };
209
+ } else {
210
+ console.log(`
211
+ ${previewCard(THEMES[theme])}
212
+ `);
213
+ const action = await select({
214
+ message: `Use "${theme}" as-is, or customize it?`,
215
+ choices: [
216
+ { name: `Use ${theme} as-is`, value: "asis" },
217
+ { name: "Customize \u2014 edit fields, then save", value: "edit" }
218
+ ]
219
+ });
220
+ if (action === "asis") {
221
+ next = { theme, overrides: {}, clientId: current.clientId };
222
+ } else {
223
+ const edited = await editThemeMenu(structuredClone(THEMES[theme]));
224
+ if (!edited) {
225
+ console.log(ui.warn("Cancelled \u2014 no changes saved."));
226
+ return;
227
+ }
228
+ next = { theme: "custom", overrides: edited, clientId: current.clientId };
229
+ }
230
+ }
231
+ saveUserConfig(next);
232
+ console.log(`
233
+ ${ui.check} ${ui.bold("Saved")} ${ui.dim(`to ${configPath()}`)}`);
234
+ console.log(` ${ui.dim(applyNote())}`);
235
+ } catch (err) {
236
+ if (err instanceof Error && err.name === "ExitPromptError") {
237
+ console.log(ui.warn("\nCancelled \u2014 no changes saved."));
238
+ return;
239
+ }
240
+ throw err;
241
+ }
242
+ }
243
+ export {
244
+ config
245
+ };
@@ -0,0 +1,276 @@
1
+ import {
2
+ renderPresence
3
+ } from "./chunk-XYXO7VLI.js";
4
+ import {
5
+ readUserConfig,
6
+ resolveClientId,
7
+ resolveTheme
8
+ } from "./chunk-2A7GKQFO.js";
9
+ import {
10
+ PRUNE_AFTER_MS,
11
+ aggregate,
12
+ readMarkers,
13
+ removeSessionMarker
14
+ } from "./chunk-4V354UFY.js";
15
+ import {
16
+ acquireLock,
17
+ clearDaemonStatus,
18
+ releaseLock,
19
+ writeDaemonStatus
20
+ } from "./chunk-KDLAS6ED.js";
21
+ import {
22
+ configPath
23
+ } from "./chunk-FRGALQ5R.js";
24
+
25
+ // src/provider/transcript.ts
26
+ import { readFileSync, statSync } from "fs";
27
+ var cache = /* @__PURE__ */ new Map();
28
+ function prettyModel(raw) {
29
+ const m = /claude-(opus|sonnet|haiku)-(\d+)-(\d+)/.exec(raw);
30
+ if (!m) return raw;
31
+ const tier = m[1].charAt(0).toUpperCase() + m[1].slice(1);
32
+ return `${tier} ${m[2]}.${m[3]}`;
33
+ }
34
+ function parse(path) {
35
+ let raw;
36
+ try {
37
+ raw = readFileSync(path, "utf8");
38
+ } catch {
39
+ return {};
40
+ }
41
+ let model;
42
+ let branch;
43
+ let tokens = 0;
44
+ let sawTokens = false;
45
+ for (const line of raw.split("\n")) {
46
+ if (!line) continue;
47
+ let entry;
48
+ try {
49
+ entry = JSON.parse(line);
50
+ } catch {
51
+ continue;
52
+ }
53
+ if (typeof entry.gitBranch === "string" && entry.gitBranch && entry.gitBranch !== "HEAD") {
54
+ branch = entry.gitBranch;
55
+ }
56
+ const msg = entry.message;
57
+ if (msg && typeof msg === "object") {
58
+ if (typeof msg.model === "string" && msg.model && msg.model !== "<synthetic>") {
59
+ model = msg.model;
60
+ }
61
+ const out = msg.usage?.output_tokens;
62
+ if (typeof out === "number") {
63
+ tokens += out;
64
+ sawTokens = true;
65
+ }
66
+ }
67
+ }
68
+ return {
69
+ model: model ? prettyModel(model) : void 0,
70
+ branch,
71
+ tokens: sawTokens ? tokens : void 0
72
+ };
73
+ }
74
+ function readTranscriptMeta(transcriptPath) {
75
+ if (!transcriptPath) return {};
76
+ let st;
77
+ try {
78
+ st = statSync(transcriptPath);
79
+ } catch {
80
+ return {};
81
+ }
82
+ const cached = cache.get(transcriptPath);
83
+ if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
84
+ return cached.meta;
85
+ }
86
+ const meta = parse(transcriptPath);
87
+ cache.set(transcriptPath, { mtimeMs: st.mtimeMs, size: st.size, meta });
88
+ return meta;
89
+ }
90
+
91
+ // src/daemon/discord.ts
92
+ import { Client } from "@xhayper/discord-rpc";
93
+ var CONNECT_TIMEOUT_MS = 1e4;
94
+ function withTimeout(p, ms, label) {
95
+ return new Promise((resolve, reject) => {
96
+ const timer = setTimeout(() => reject(new Error(`${label} timed out`)), ms);
97
+ p.then(
98
+ (v) => {
99
+ clearTimeout(timer);
100
+ resolve(v);
101
+ },
102
+ (e) => {
103
+ clearTimeout(timer);
104
+ reject(e instanceof Error ? e : new Error(String(e)));
105
+ }
106
+ );
107
+ });
108
+ }
109
+ function toSetActivity(p) {
110
+ return {
111
+ details: p.details,
112
+ state: p.state,
113
+ largeImageKey: p.largeImageKey,
114
+ largeImageText: p.largeImageText,
115
+ smallImageKey: p.smallImageKey,
116
+ smallImageText: p.smallImageText,
117
+ startTimestamp: p.startTimestamp,
118
+ buttons: p.buttons,
119
+ statusDisplayType: p.statusDisplayType
120
+ };
121
+ }
122
+ var DiscordPresence = class {
123
+ constructor(clientId) {
124
+ this.clientId = clientId;
125
+ }
126
+ clientId;
127
+ client;
128
+ connected = false;
129
+ connecting = false;
130
+ get isConnected() {
131
+ return this.connected;
132
+ }
133
+ /** Ensure we have a live connection. Returns false (no-op) if Discord is down. */
134
+ async ensureClient() {
135
+ if (this.connected && this.client) return true;
136
+ if (this.connecting) return false;
137
+ if (!this.clientId) return false;
138
+ this.connecting = true;
139
+ if (this.client) {
140
+ try {
141
+ await this.client.destroy();
142
+ } catch {
143
+ }
144
+ this.client = void 0;
145
+ }
146
+ try {
147
+ const client = new Client({ clientId: this.clientId, transport: { type: "ipc" } });
148
+ client.on("disconnected", () => {
149
+ this.connected = false;
150
+ });
151
+ try {
152
+ await withTimeout(client.connect(), CONNECT_TIMEOUT_MS, "discord connect");
153
+ } catch (err) {
154
+ try {
155
+ await client.destroy();
156
+ } catch {
157
+ }
158
+ throw err;
159
+ }
160
+ const transportSocket = client.transport.socket;
161
+ transportSocket?.on("error", () => {
162
+ this.connected = false;
163
+ });
164
+ this.client = client;
165
+ this.connected = true;
166
+ return true;
167
+ } catch {
168
+ this.connected = false;
169
+ return false;
170
+ } finally {
171
+ this.connecting = false;
172
+ }
173
+ }
174
+ async setActivity(payload) {
175
+ if (Object.keys(payload).length === 0) {
176
+ await this.clearActivity();
177
+ return;
178
+ }
179
+ if (!await this.ensureClient()) return;
180
+ try {
181
+ await this.client?.user?.setActivity(toSetActivity(payload));
182
+ } catch {
183
+ this.connected = false;
184
+ }
185
+ }
186
+ async clearActivity() {
187
+ if (!this.connected || !this.client) return;
188
+ try {
189
+ await this.client.user?.clearActivity();
190
+ } catch {
191
+ this.connected = false;
192
+ }
193
+ }
194
+ async destroy() {
195
+ try {
196
+ await this.client?.destroy();
197
+ } catch {
198
+ }
199
+ this.client = void 0;
200
+ this.connected = false;
201
+ }
202
+ };
203
+
204
+ // src/daemon/index.ts
205
+ var TICK_MS = 15 * 1e3;
206
+ var IDLE_GRACE_MS = 60 * 1e3;
207
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
208
+ async function startDaemon(_args = []) {
209
+ if (!acquireLock(Date.now())) return;
210
+ const discord = new DiscordPresence(resolveClientId(readUserConfig(configPath())));
211
+ let running = true;
212
+ const shutdown = async () => {
213
+ running = false;
214
+ await discord.clearActivity();
215
+ await discord.destroy();
216
+ clearDaemonStatus();
217
+ releaseLock();
218
+ };
219
+ process.once("SIGINT", () => void shutdown().then(() => process.exit(0)));
220
+ process.once("SIGTERM", () => void shutdown().then(() => process.exit(0)));
221
+ process.on("exit", () => releaseLock());
222
+ const RECOVERABLE = /* @__PURE__ */ new Set([
223
+ "ECONNRESET",
224
+ "EPIPE",
225
+ "ECONNREFUSED",
226
+ "ENOENT",
227
+ "EBADF",
228
+ "ERR_STREAM_DESTROYED"
229
+ ]);
230
+ process.on("uncaughtException", (err) => {
231
+ if (RECOVERABLE.has(err.code ?? "")) return;
232
+ void shutdown().finally(() => process.exit(1));
233
+ });
234
+ process.on("unhandledRejection", () => {
235
+ });
236
+ let idleSince = null;
237
+ while (running) {
238
+ const now = Date.now();
239
+ const markers = readMarkers();
240
+ const state = aggregate(markers, now);
241
+ for (const m of markers) {
242
+ if (now - m.heartbeat > PRUNE_AFTER_MS) removeSessionMarker(m.id);
243
+ }
244
+ if (state) {
245
+ idleSince = null;
246
+ const meta = readTranscriptMeta(state.transcriptPath);
247
+ state.model ??= meta.model;
248
+ state.branch ??= meta.branch;
249
+ state.tokens ??= meta.tokens;
250
+ const theme = resolveTheme(readUserConfig(configPath()));
251
+ await discord.setActivity(renderPresence(theme, state, now));
252
+ writeDaemonStatus({
253
+ pid: process.pid,
254
+ connected: discord.isConnected,
255
+ sessionCount: state.sessionCount,
256
+ activity: state.activity,
257
+ updatedAt: now
258
+ });
259
+ } else {
260
+ if (idleSince === null) idleSince = now;
261
+ await discord.clearActivity();
262
+ writeDaemonStatus({
263
+ pid: process.pid,
264
+ connected: discord.isConnected,
265
+ sessionCount: 0,
266
+ updatedAt: now
267
+ });
268
+ if (now - idleSince >= IDLE_GRACE_MS) break;
269
+ }
270
+ await sleep(TICK_MS);
271
+ }
272
+ await shutdown();
273
+ }
274
+ export {
275
+ startDaemon
276
+ };
@@ -0,0 +1,45 @@
1
+ import {
2
+ HOOK_EVENTS,
3
+ backupSettings,
4
+ mergeHooks,
5
+ readSettings,
6
+ writeSettings
7
+ } from "./chunk-CEWLOUQO.js";
8
+ import {
9
+ DEFAULT_CONFIG
10
+ } from "./chunk-2A7GKQFO.js";
11
+ import {
12
+ ui
13
+ } from "./chunk-LKE6H3YG.js";
14
+ import {
15
+ configPath,
16
+ entryPath,
17
+ presenceDir,
18
+ sessionsDir
19
+ } from "./chunk-FRGALQ5R.js";
20
+
21
+ // src/commands/install.ts
22
+ import { existsSync } from "fs";
23
+ import { mkdir, writeFile } from "fs/promises";
24
+ async function install(_args = []) {
25
+ await mkdir(sessionsDir(), { recursive: true });
26
+ if (!existsSync(configPath())) {
27
+ await mkdir(presenceDir(), { recursive: true });
28
+ await writeFile(configPath(), `${JSON.stringify(DEFAULT_CONFIG, null, 2)}
29
+ `);
30
+ }
31
+ const existing = await readSettings();
32
+ const backupPath = await backupSettings(existing);
33
+ await writeSettings(mergeHooks(existing, entryPath()));
34
+ console.log(`${ui.check} ${ui.bold("vibecoder-discord-presence installed")}`);
35
+ console.log(` ${ui.dim("hooks:")} ${HOOK_EVENTS.map((e) => e.name).join(", ")}`);
36
+ console.log(` ${ui.dim("config:")} ${ui.accent(configPath())}`);
37
+ if (backupPath) console.log(` ${ui.dim("backup:")} ${ui.accent(backupPath)}`);
38
+ console.log(
39
+ `
40
+ ${ui.dim("Open Claude Code with Discord running and your presence will appear.")}`
41
+ );
42
+ }
43
+ export {
44
+ install
45
+ };
@@ -0,0 +1,28 @@
1
+ import {
2
+ ui
3
+ } from "./chunk-LKE6H3YG.js";
4
+ import {
5
+ isProcessAlive,
6
+ readLock,
7
+ spawnDaemon,
8
+ stopDaemon
9
+ } from "./chunk-KDLAS6ED.js";
10
+ import "./chunk-FRGALQ5R.js";
11
+
12
+ // src/commands/restart.ts
13
+ async function restart(_args = []) {
14
+ const old = await stopDaemon();
15
+ if (old) console.log(ui.dim(` stopped daemon (pid ${old})`));
16
+ spawnDaemon();
17
+ await new Promise((r) => setTimeout(r, 1e3));
18
+ const lock = readLock();
19
+ if (lock && isProcessAlive(lock.pid)) {
20
+ console.log(`${ui.check} ${ui.bold("daemon restarted")} ${ui.dim(`(pid ${lock.pid})`)}`);
21
+ } else {
22
+ console.log(`${ui.check} ${ui.bold("daemon restart triggered")}`);
23
+ console.log(ui.dim(" (it shows a card once a Claude Code session is active)"));
24
+ }
25
+ }
26
+ export {
27
+ restart
28
+ };