@sneub/pair 0.0.6 → 0.0.7
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/dist/cli.js +297 -170
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7304,7 +7304,7 @@ function currentDateTime() {
|
|
|
7304
7304
|
const tz = Intl.DateTimeFormat("en-US", { timeZoneName: "short" }).formatToParts(now).find((p) => p.type === "timeZoneName")?.value ?? "UTC";
|
|
7305
7305
|
return `${date} ${time} ${tz}`;
|
|
7306
7306
|
}
|
|
7307
|
-
async function assembleSystemPrompt() {
|
|
7307
|
+
async function assembleSystemPrompt(options) {
|
|
7308
7308
|
const config = getConfig();
|
|
7309
7309
|
const workspaceDir = config.pair.workspaceDir;
|
|
7310
7310
|
const claudePath = join9(workspaceDir, "CLAUDE.md");
|
|
@@ -7389,6 +7389,17 @@ ${heartbeatContent}`);
|
|
|
7389
7389
|
## Today's Notes
|
|
7390
7390
|
${notesContent}`);
|
|
7391
7391
|
}
|
|
7392
|
+
if (options?.includeTranscript) {
|
|
7393
|
+
const maxEntries = config.pair.conversationLogMaxEntries;
|
|
7394
|
+
const transcript = await getRecentTranscript(maxEntries);
|
|
7395
|
+
if (transcript) {
|
|
7396
|
+
sections.push(`---
|
|
7397
|
+
## Recent Conversation
|
|
7398
|
+
` + `The following is a transcript of your recent conversation with the user, ` + `provided for context continuity. The previous session has expired, but ` + `this log captures what was discussed. Use it to maintain conversational ` + `context \u2014 the user may be responding to something you said earlier.
|
|
7399
|
+
|
|
7400
|
+
` + transcript);
|
|
7401
|
+
}
|
|
7402
|
+
}
|
|
7392
7403
|
const tools = await loadTools();
|
|
7393
7404
|
if (tools.systemPromptBlock) {
|
|
7394
7405
|
sections.push(tools.systemPromptBlock);
|
|
@@ -7423,27 +7434,131 @@ var log2, fileStates;
|
|
|
7423
7434
|
var init_memory = __esm(() => {
|
|
7424
7435
|
init_config();
|
|
7425
7436
|
init_logger();
|
|
7437
|
+
init_conversation_log();
|
|
7426
7438
|
init_tools();
|
|
7427
7439
|
log2 = createLogger("memory");
|
|
7428
7440
|
fileStates = new Map;
|
|
7429
7441
|
});
|
|
7430
7442
|
|
|
7443
|
+
// src/core/conversation-log.ts
|
|
7444
|
+
import { existsSync as existsSync10 } from "fs";
|
|
7445
|
+
import { readFile as readFile8, writeFile, appendFile as appendFile2, mkdir } from "fs/promises";
|
|
7446
|
+
import { join as join10, dirname as dirname2 } from "path";
|
|
7447
|
+
function getLogPath() {
|
|
7448
|
+
return join10(getWorkspaceDir2(), LOG_FILENAME);
|
|
7449
|
+
}
|
|
7450
|
+
async function readEntries() {
|
|
7451
|
+
const logPath = getLogPath();
|
|
7452
|
+
if (!existsSync10(logPath))
|
|
7453
|
+
return [];
|
|
7454
|
+
try {
|
|
7455
|
+
const content = await readFile8(logPath, "utf-8");
|
|
7456
|
+
const entries = [];
|
|
7457
|
+
for (const line of content.split(`
|
|
7458
|
+
`)) {
|
|
7459
|
+
const trimmed = line.trim();
|
|
7460
|
+
if (!trimmed)
|
|
7461
|
+
continue;
|
|
7462
|
+
try {
|
|
7463
|
+
entries.push(JSON.parse(trimmed));
|
|
7464
|
+
} catch {}
|
|
7465
|
+
}
|
|
7466
|
+
return entries;
|
|
7467
|
+
} catch (err) {
|
|
7468
|
+
log3.warn(`Failed to read conversation log: ${err}`);
|
|
7469
|
+
return [];
|
|
7470
|
+
}
|
|
7471
|
+
}
|
|
7472
|
+
async function rotateIfNeeded2(maxEntries) {
|
|
7473
|
+
const entries = await readEntries();
|
|
7474
|
+
if (entries.length <= maxEntries * 2)
|
|
7475
|
+
return;
|
|
7476
|
+
const trimmed = entries.slice(-maxEntries);
|
|
7477
|
+
const logPath = getLogPath();
|
|
7478
|
+
const content = trimmed.map((e) => JSON.stringify(e)).join(`
|
|
7479
|
+
`) + `
|
|
7480
|
+
`;
|
|
7481
|
+
try {
|
|
7482
|
+
await writeFile(logPath, content, "utf-8");
|
|
7483
|
+
log3.info(`Conversation log rotated: ${entries.length} \u2192 ${trimmed.length} entries`);
|
|
7484
|
+
} catch (err) {
|
|
7485
|
+
log3.warn(`Failed to rotate conversation log: ${err}`);
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
7488
|
+
function formatTimestamp(iso) {
|
|
7489
|
+
try {
|
|
7490
|
+
const d = new Date(iso);
|
|
7491
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
7492
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
7493
|
+
} catch {
|
|
7494
|
+
return iso;
|
|
7495
|
+
}
|
|
7496
|
+
}
|
|
7497
|
+
async function logMessage(role, text, options) {
|
|
7498
|
+
const entry = {
|
|
7499
|
+
ts: new Date().toISOString(),
|
|
7500
|
+
role,
|
|
7501
|
+
text,
|
|
7502
|
+
...options?.name ? { name: options.name } : {},
|
|
7503
|
+
...options?.channel ? { channel: options.channel } : {}
|
|
7504
|
+
};
|
|
7505
|
+
const logPath = getLogPath();
|
|
7506
|
+
try {
|
|
7507
|
+
const dir = dirname2(logPath);
|
|
7508
|
+
if (!existsSync10(dir)) {
|
|
7509
|
+
await mkdir(dir, { recursive: true });
|
|
7510
|
+
}
|
|
7511
|
+
await appendFile2(logPath, JSON.stringify(entry) + `
|
|
7512
|
+
`, "utf-8");
|
|
7513
|
+
const maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
7514
|
+
rotateIfNeeded2(maxEntries).catch((err) => {
|
|
7515
|
+
log3.warn(`Rotation check failed: ${err}`);
|
|
7516
|
+
});
|
|
7517
|
+
} catch (err) {
|
|
7518
|
+
log3.warn(`Failed to write conversation log entry: ${err}`);
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
async function getRecentTranscript(maxEntries) {
|
|
7522
|
+
const limit = maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
7523
|
+
const entries = await readEntries();
|
|
7524
|
+
if (entries.length === 0)
|
|
7525
|
+
return null;
|
|
7526
|
+
const recent = entries.slice(-limit);
|
|
7527
|
+
const lines = recent.map((entry) => {
|
|
7528
|
+
const time = formatTimestamp(entry.ts);
|
|
7529
|
+
const speaker = entry.role === "user" ? `${entry.name ?? "User"}${entry.channel ? ` (${entry.channel})` : ""}` : "Assistant";
|
|
7530
|
+
let text = entry.text;
|
|
7531
|
+
if (text.length > MAX_TRANSCRIPT_CHARS_PER_ENTRY) {
|
|
7532
|
+
text = text.slice(0, MAX_TRANSCRIPT_CHARS_PER_ENTRY) + " [...]";
|
|
7533
|
+
}
|
|
7534
|
+
return `[${time}] ${speaker}: ${text}`;
|
|
7535
|
+
});
|
|
7536
|
+
return lines.join(`
|
|
7537
|
+
`);
|
|
7538
|
+
}
|
|
7539
|
+
var log3, LOG_FILENAME = "conversation-log.jsonl", DEFAULT_MAX_ENTRIES = 50, MAX_TRANSCRIPT_CHARS_PER_ENTRY = 800;
|
|
7540
|
+
var init_conversation_log = __esm(() => {
|
|
7541
|
+
init_logger();
|
|
7542
|
+
init_memory();
|
|
7543
|
+
log3 = createLogger("conversation-log");
|
|
7544
|
+
});
|
|
7545
|
+
|
|
7431
7546
|
// src/core/engine.ts
|
|
7432
7547
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
7433
7548
|
import { EventEmitter } from "events";
|
|
7434
|
-
var
|
|
7549
|
+
var log4, AUTH_ERRORS, Engine;
|
|
7435
7550
|
var init_engine = __esm(() => {
|
|
7436
7551
|
init_config();
|
|
7437
7552
|
init_logger();
|
|
7438
7553
|
init_memory();
|
|
7439
|
-
|
|
7554
|
+
log4 = createLogger("engine");
|
|
7440
7555
|
AUTH_ERRORS = new Set(["authentication_failed", "billing_error"]);
|
|
7441
7556
|
Engine = class Engine extends EventEmitter {
|
|
7442
7557
|
async runTurn(prompt, options) {
|
|
7443
7558
|
const config = getConfig();
|
|
7444
7559
|
const model = options.model ?? config.pair.model ?? "claude-sonnet-4-6";
|
|
7445
7560
|
const workspaceDir = getWorkspaceDir2();
|
|
7446
|
-
|
|
7561
|
+
log4.info(`Starting turn (model: ${model}, resume: ${options.sdkSessionId ?? "new"})`);
|
|
7447
7562
|
const startTime = Date.now();
|
|
7448
7563
|
const settings = config.pair.apiKeyHelper ? { apiKeyHelper: config.pair.apiKeyHelper } : undefined;
|
|
7449
7564
|
const stderrLines = [];
|
|
@@ -7482,9 +7597,9 @@ var init_engine = __esm(() => {
|
|
|
7482
7597
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
7483
7598
|
const stderrTail = stderrLines.slice(-15).join(`
|
|
7484
7599
|
`);
|
|
7485
|
-
|
|
7600
|
+
log4.error(`Turn failed after ${elapsed}s: ${errorMsg}`);
|
|
7486
7601
|
if (stderrTail) {
|
|
7487
|
-
|
|
7602
|
+
log4.error(`claude stderr (last ${Math.min(stderrLines.length, 15)} lines):
|
|
7488
7603
|
${stderrTail}`);
|
|
7489
7604
|
}
|
|
7490
7605
|
const isAuthError = errorMsg.includes("401") || errorMsg.includes("auth") || errorMsg.includes("Unauthorized") || /\b(invalid[_ ]api[_ ]key|not authenticated|credentials)\b/i.test(stderrTail);
|
|
@@ -7503,7 +7618,7 @@ ${stderrTail}`);
|
|
|
7503
7618
|
}
|
|
7504
7619
|
handleMessage(message) {
|
|
7505
7620
|
if (message.type === "system" && message.subtype === "init") {
|
|
7506
|
-
|
|
7621
|
+
log4.info(`SDK session initialized: ${message.session_id}`);
|
|
7507
7622
|
this.emit("started", { sessionId: message.session_id });
|
|
7508
7623
|
return;
|
|
7509
7624
|
}
|
|
@@ -7511,7 +7626,7 @@ ${stderrTail}`);
|
|
|
7511
7626
|
const authMsg = message;
|
|
7512
7627
|
if (authMsg.error) {
|
|
7513
7628
|
const config = getConfig();
|
|
7514
|
-
|
|
7629
|
+
log4.error(`Auth error: ${authMsg.error}`);
|
|
7515
7630
|
if (config.pair.authMode === "oauth") {
|
|
7516
7631
|
this.emit("error", {
|
|
7517
7632
|
message: "Authentication failed \u2014 your OAuth session may have expired. Run `claude login` to re-authenticate.",
|
|
@@ -7519,14 +7634,14 @@ ${stderrTail}`);
|
|
|
7519
7634
|
});
|
|
7520
7635
|
}
|
|
7521
7636
|
} else if (authMsg.isAuthenticating) {
|
|
7522
|
-
|
|
7637
|
+
log4.info("Authenticating...");
|
|
7523
7638
|
}
|
|
7524
7639
|
return;
|
|
7525
7640
|
}
|
|
7526
7641
|
if (message.type === "assistant") {
|
|
7527
7642
|
if (message.error && AUTH_ERRORS.has(message.error)) {
|
|
7528
7643
|
const config = getConfig();
|
|
7529
|
-
|
|
7644
|
+
log4.error(`Assistant auth error: ${message.error}`);
|
|
7530
7645
|
if (config.pair.authMode === "oauth") {
|
|
7531
7646
|
this.emit("error", {
|
|
7532
7647
|
message: "Authentication failed \u2014 your OAuth session may have expired. Run `claude login` to re-authenticate.",
|
|
@@ -7538,7 +7653,7 @@ ${stderrTail}`);
|
|
|
7538
7653
|
for (const block of message.message.content) {
|
|
7539
7654
|
if (typeof block === "object" && "type" in block && block.type === "tool_use") {
|
|
7540
7655
|
const toolName = "name" in block ? String(block.name) : "unknown";
|
|
7541
|
-
|
|
7656
|
+
log4.info(`Tool use: ${toolName}`);
|
|
7542
7657
|
this.emit("tool_use", { toolName });
|
|
7543
7658
|
}
|
|
7544
7659
|
}
|
|
@@ -7548,7 +7663,7 @@ ${stderrTail}`);
|
|
|
7548
7663
|
if (message.subtype === "success") {
|
|
7549
7664
|
const cost = message.total_cost_usd ?? 0;
|
|
7550
7665
|
const elapsed = (message.duration_ms / 1000).toFixed(1);
|
|
7551
|
-
|
|
7666
|
+
log4.info(`Turn complete (${elapsed}s, $${cost.toFixed(4)}, ` + `${message.num_turns} turns)`);
|
|
7552
7667
|
this.emit("done", {
|
|
7553
7668
|
response: message.result,
|
|
7554
7669
|
sessionId: message.session_id,
|
|
@@ -7556,7 +7671,7 @@ ${stderrTail}`);
|
|
|
7556
7671
|
});
|
|
7557
7672
|
} else {
|
|
7558
7673
|
const errorDetails = message.errors?.join("; ") ?? message.subtype;
|
|
7559
|
-
|
|
7674
|
+
log4.error(`Turn error: ${message.subtype} \u2014 ${errorDetails}`);
|
|
7560
7675
|
this.emit("error", {
|
|
7561
7676
|
message: `Turn failed: ${message.subtype}`,
|
|
7562
7677
|
details: errorDetails
|
|
@@ -21451,7 +21566,7 @@ function createPairMcpServer(adapters) {
|
|
|
21451
21566
|
const channelId = args.channelId ?? config2.pair.defaultChannelId;
|
|
21452
21567
|
if (!channel) {
|
|
21453
21568
|
const msg = "No channel specified and no defaultChannel configured. " + "Set pair.defaultChannel in config.json or pass channel explicitly.";
|
|
21454
|
-
|
|
21569
|
+
log5.warn(`send_message: ${msg}`);
|
|
21455
21570
|
return {
|
|
21456
21571
|
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
21457
21572
|
isError: true
|
|
@@ -21459,7 +21574,7 @@ function createPairMcpServer(adapters) {
|
|
|
21459
21574
|
}
|
|
21460
21575
|
if (!channelId) {
|
|
21461
21576
|
const msg = "No channelId specified and no defaultChannelId configured. " + "Set pair.defaultChannelId in config.json or pass channelId explicitly.";
|
|
21462
|
-
|
|
21577
|
+
log5.warn(`send_message: ${msg}`);
|
|
21463
21578
|
return {
|
|
21464
21579
|
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
21465
21580
|
isError: true
|
|
@@ -21468,7 +21583,7 @@ function createPairMcpServer(adapters) {
|
|
|
21468
21583
|
const adapter = adapters.get(channel);
|
|
21469
21584
|
if (!adapter) {
|
|
21470
21585
|
const msg = `No ${channel} adapter is registered \u2014 is it configured and started?`;
|
|
21471
|
-
|
|
21586
|
+
log5.warn(`send_message: ${msg}`);
|
|
21472
21587
|
return {
|
|
21473
21588
|
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
21474
21589
|
isError: true
|
|
@@ -21477,7 +21592,8 @@ function createPairMcpServer(adapters) {
|
|
|
21477
21592
|
try {
|
|
21478
21593
|
await adapter.sendMessage(channelId, args.text);
|
|
21479
21594
|
const preview = args.text.length > 60 ? `${args.text.slice(0, 60)}...` : args.text;
|
|
21480
|
-
|
|
21595
|
+
log5.info(`send_message \u2192 ${channel}:${channelId} (${preview})`);
|
|
21596
|
+
logMessage("assistant", args.text, { channel }).catch(() => {});
|
|
21481
21597
|
return {
|
|
21482
21598
|
content: [
|
|
21483
21599
|
{
|
|
@@ -21488,7 +21604,7 @@ function createPairMcpServer(adapters) {
|
|
|
21488
21604
|
};
|
|
21489
21605
|
} catch (err) {
|
|
21490
21606
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
21491
|
-
|
|
21607
|
+
log5.error(`send_message failed for ${channel}:${channelId}: ${errMsg}`);
|
|
21492
21608
|
return {
|
|
21493
21609
|
content: [
|
|
21494
21610
|
{
|
|
@@ -21503,12 +21619,13 @@ function createPairMcpServer(adapters) {
|
|
|
21503
21619
|
]
|
|
21504
21620
|
});
|
|
21505
21621
|
}
|
|
21506
|
-
var
|
|
21622
|
+
var log5;
|
|
21507
21623
|
var init_pair_mcp = __esm(() => {
|
|
21508
21624
|
init_zod();
|
|
21509
21625
|
init_config();
|
|
21510
21626
|
init_logger();
|
|
21511
|
-
|
|
21627
|
+
init_conversation_log();
|
|
21628
|
+
log5 = createLogger("pair-mcp");
|
|
21512
21629
|
});
|
|
21513
21630
|
|
|
21514
21631
|
// src/core/session.ts
|
|
@@ -21522,7 +21639,7 @@ class SessionManager {
|
|
|
21522
21639
|
const existing = this.sessions.get(key);
|
|
21523
21640
|
if (existing) {
|
|
21524
21641
|
if (this.isExpired(existing)) {
|
|
21525
|
-
|
|
21642
|
+
log6.info(`Session expired for ${key} (idle ${this.idleMinutes(existing).toFixed(1)} min) \u2014 creating new`);
|
|
21526
21643
|
this.expire(key);
|
|
21527
21644
|
} else {
|
|
21528
21645
|
existing.lastActiveAt = new Date;
|
|
@@ -21542,7 +21659,7 @@ class SessionManager {
|
|
|
21542
21659
|
isProcessing: false
|
|
21543
21660
|
};
|
|
21544
21661
|
this.sessions.set(key, session);
|
|
21545
|
-
|
|
21662
|
+
log6.info(`New session created: ${key} (id: ${session.id})`);
|
|
21546
21663
|
return session;
|
|
21547
21664
|
}
|
|
21548
21665
|
get(sessionKey) {
|
|
@@ -21551,7 +21668,7 @@ class SessionManager {
|
|
|
21551
21668
|
expire(sessionKey) {
|
|
21552
21669
|
const session = this.sessions.get(sessionKey);
|
|
21553
21670
|
if (session) {
|
|
21554
|
-
|
|
21671
|
+
log6.info(`Session expired: ${sessionKey} (id: ${session.id}, ` + `sdk: ${session.sdkSessionId ?? "none"}, ` + `created: ${session.createdAt.toISOString()})`);
|
|
21555
21672
|
this.sessions.delete(sessionKey);
|
|
21556
21673
|
}
|
|
21557
21674
|
}
|
|
@@ -21562,13 +21679,13 @@ class SessionManager {
|
|
|
21562
21679
|
this.sweepTimer = setInterval(() => {
|
|
21563
21680
|
this.sweep();
|
|
21564
21681
|
}, SWEEP_INTERVAL_MS);
|
|
21565
|
-
|
|
21682
|
+
log6.info("Session sweep started (every 5 minutes)");
|
|
21566
21683
|
}
|
|
21567
21684
|
stopSweep() {
|
|
21568
21685
|
if (this.sweepTimer) {
|
|
21569
21686
|
clearInterval(this.sweepTimer);
|
|
21570
21687
|
this.sweepTimer = null;
|
|
21571
|
-
|
|
21688
|
+
log6.info("Session sweep stopped");
|
|
21572
21689
|
}
|
|
21573
21690
|
}
|
|
21574
21691
|
get size() {
|
|
@@ -21585,7 +21702,7 @@ class SessionManager {
|
|
|
21585
21702
|
this.expire(key);
|
|
21586
21703
|
}
|
|
21587
21704
|
if (expired.length > 0) {
|
|
21588
|
-
|
|
21705
|
+
log6.info(`Sweep expired ${expired.length} session(s): ${expired.join(", ")}`);
|
|
21589
21706
|
}
|
|
21590
21707
|
}
|
|
21591
21708
|
isExpired(session) {
|
|
@@ -21597,11 +21714,11 @@ class SessionManager {
|
|
|
21597
21714
|
return (Date.now() - session.lastActiveAt.getTime()) / 60000;
|
|
21598
21715
|
}
|
|
21599
21716
|
}
|
|
21600
|
-
var
|
|
21717
|
+
var log6;
|
|
21601
21718
|
var init_session = __esm(() => {
|
|
21602
21719
|
init_config();
|
|
21603
21720
|
init_logger();
|
|
21604
|
-
|
|
21721
|
+
log6 = createLogger("session");
|
|
21605
21722
|
});
|
|
21606
21723
|
|
|
21607
21724
|
// src/core/router.ts
|
|
@@ -21621,10 +21738,10 @@ class Router {
|
|
|
21621
21738
|
this.adapters.set(adapter.name, adapter);
|
|
21622
21739
|
adapter.onMessage((msg) => {
|
|
21623
21740
|
this.handleMessage(msg).catch((err) => {
|
|
21624
|
-
|
|
21741
|
+
log7.error(`Unhandled error in handleMessage for ${msg.channel}:${msg.userId}: ${err}`);
|
|
21625
21742
|
});
|
|
21626
21743
|
});
|
|
21627
|
-
|
|
21744
|
+
log7.info(`Adapter registered: ${adapter.name}`);
|
|
21628
21745
|
}
|
|
21629
21746
|
isAuthorized(msg) {
|
|
21630
21747
|
const config2 = getConfig();
|
|
@@ -21639,10 +21756,10 @@ class Router {
|
|
|
21639
21756
|
}
|
|
21640
21757
|
async handleMessage(msg) {
|
|
21641
21758
|
if (!this.isAuthorized(msg)) {
|
|
21642
|
-
|
|
21759
|
+
log7.warn(`Unauthorized message from ${msg.channel}:${msg.userId} (${msg.userName}) \u2014 ignoring`);
|
|
21643
21760
|
return;
|
|
21644
21761
|
}
|
|
21645
|
-
|
|
21762
|
+
log7.info(`Message from ${msg.channel}:${msg.userId} (${msg.userName}): ` + `${msg.text.slice(0, 100)}${msg.text.length > 100 ? "..." : ""}`);
|
|
21646
21763
|
const session = this.sessionManager.getOrCreate(msg.channel, msg.userId, msg.userName);
|
|
21647
21764
|
session.pendingMessages.push(msg.text);
|
|
21648
21765
|
const existingTimer = this.debounceTimers.get(session.key);
|
|
@@ -21652,14 +21769,14 @@ class Router {
|
|
|
21652
21769
|
const timer = setTimeout(() => {
|
|
21653
21770
|
this.debounceTimers.delete(session.key);
|
|
21654
21771
|
this.drainIfReady(session, msg).catch((err) => {
|
|
21655
|
-
|
|
21772
|
+
log7.error(`Failed to drain messages for ${session.key}: ${err}`);
|
|
21656
21773
|
});
|
|
21657
21774
|
}, DEBOUNCE_MS);
|
|
21658
21775
|
this.debounceTimers.set(session.key, timer);
|
|
21659
21776
|
}
|
|
21660
21777
|
async drainIfReady(session, msg) {
|
|
21661
21778
|
if (session.isProcessing) {
|
|
21662
|
-
|
|
21779
|
+
log7.info(`Session ${session.key} is processing \u2014 ${session.pendingMessages.length} message(s) queued`);
|
|
21663
21780
|
return;
|
|
21664
21781
|
}
|
|
21665
21782
|
if (session.pendingMessages.length === 0)
|
|
@@ -21674,13 +21791,18 @@ class Router {
|
|
|
21674
21791
|
const startTime = Date.now();
|
|
21675
21792
|
const adapter = this.adapters.get(msg.channel);
|
|
21676
21793
|
if (!adapter) {
|
|
21677
|
-
|
|
21794
|
+
log7.error(`No adapter registered for channel: ${msg.channel}`);
|
|
21678
21795
|
return;
|
|
21679
21796
|
}
|
|
21680
21797
|
session.isProcessing = true;
|
|
21681
21798
|
await adapter.sendTypingIndicator(msg.channelId).catch((err) => {
|
|
21682
|
-
|
|
21799
|
+
log7.warn(`Failed to send typing indicator: ${err}`);
|
|
21683
21800
|
});
|
|
21801
|
+
logMessage("user", prompt, {
|
|
21802
|
+
name: session.userName,
|
|
21803
|
+
channel: msg.channel,
|
|
21804
|
+
maxEntries: getConfig().pair.conversationLogMaxEntries
|
|
21805
|
+
}).catch(() => {});
|
|
21684
21806
|
let systemPrompt;
|
|
21685
21807
|
let enginePrompt = prompt;
|
|
21686
21808
|
if (session.sdkSessionId) {
|
|
@@ -21698,7 +21820,7 @@ ${prompt}`;
|
|
|
21698
21820
|
}
|
|
21699
21821
|
systemPrompt = await assembleSystemPrompt();
|
|
21700
21822
|
} else {
|
|
21701
|
-
systemPrompt = await assembleSystemPrompt();
|
|
21823
|
+
systemPrompt = await assembleSystemPrompt({ includeTranscript: true });
|
|
21702
21824
|
}
|
|
21703
21825
|
const config2 = getConfig();
|
|
21704
21826
|
if (!this.pairMcpServer) {
|
|
@@ -21715,7 +21837,7 @@ ${prompt}`;
|
|
|
21715
21837
|
return;
|
|
21716
21838
|
acknowledged = true;
|
|
21717
21839
|
await adapter.sendMessage(msg.channelId, "Working on that...").catch((err) => {
|
|
21718
|
-
|
|
21840
|
+
log7.warn(`Failed to send acknowledge: ${err}`);
|
|
21719
21841
|
});
|
|
21720
21842
|
};
|
|
21721
21843
|
const ackTimer = setTimeout(() => {
|
|
@@ -21729,19 +21851,23 @@ ${prompt}`;
|
|
|
21729
21851
|
session.sdkSessionId = sessionId;
|
|
21730
21852
|
session.lastActiveAt = new Date;
|
|
21731
21853
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
21732
|
-
|
|
21854
|
+
log7.info(`Response for ${msg.channel}:${msg.userId} (${elapsed}s, $${cost.toFixed(4)}): ` + `${response.slice(0, 100)}${response.length > 100 ? "..." : ""}`);
|
|
21855
|
+
logMessage("assistant", response, {
|
|
21856
|
+
channel: msg.channel,
|
|
21857
|
+
maxEntries: getConfig().pair.conversationLogMaxEntries
|
|
21858
|
+
}).catch(() => {});
|
|
21733
21859
|
adapter.sendMessage(msg.channelId, response).catch((err) => {
|
|
21734
|
-
|
|
21860
|
+
log7.error(`Failed to send response: ${err}`);
|
|
21735
21861
|
});
|
|
21736
21862
|
};
|
|
21737
21863
|
const onError = ({ message, details }) => {
|
|
21738
21864
|
clearTimeout(ackTimer);
|
|
21739
21865
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
21740
|
-
|
|
21866
|
+
log7.error(`Error for ${msg.channel}:${msg.userId} (${elapsed}s): ${message}` + (details ? ` \u2014 ${details}` : ""));
|
|
21741
21867
|
const friendlyMessage = `Something went wrong: ${message}
|
|
21742
21868
|
` + "Please try again, or rephrase your request.";
|
|
21743
21869
|
adapter.sendMessage(msg.channelId, friendlyMessage).catch((err) => {
|
|
21744
|
-
|
|
21870
|
+
log7.error(`Failed to send error message: ${err}`);
|
|
21745
21871
|
});
|
|
21746
21872
|
};
|
|
21747
21873
|
this.engine.on("tool_use", onToolUse);
|
|
@@ -21761,40 +21887,41 @@ ${prompt}`;
|
|
|
21761
21887
|
this.engine.removeListener("error", onError);
|
|
21762
21888
|
session.isProcessing = false;
|
|
21763
21889
|
if (session.pendingMessages.length > 0) {
|
|
21764
|
-
|
|
21890
|
+
log7.info(`Draining ${session.pendingMessages.length} queued message(s) for ${session.key}`);
|
|
21765
21891
|
setTimeout(() => {
|
|
21766
21892
|
this.drainIfReady(session, msg).catch((err) => {
|
|
21767
|
-
|
|
21893
|
+
log7.error(`Failed to drain queued messages for ${session.key}: ${err}`);
|
|
21768
21894
|
});
|
|
21769
21895
|
}, 0);
|
|
21770
21896
|
}
|
|
21771
21897
|
}
|
|
21772
21898
|
}
|
|
21773
21899
|
}
|
|
21774
|
-
var
|
|
21900
|
+
var log7, DEBOUNCE_MS = 2000;
|
|
21775
21901
|
var init_router = __esm(() => {
|
|
21776
21902
|
init_config();
|
|
21777
21903
|
init_logger();
|
|
21904
|
+
init_conversation_log();
|
|
21778
21905
|
init_engine();
|
|
21779
21906
|
init_memory();
|
|
21780
21907
|
init_pair_mcp();
|
|
21781
21908
|
init_session();
|
|
21782
|
-
|
|
21909
|
+
log7 = createLogger("router");
|
|
21783
21910
|
});
|
|
21784
21911
|
|
|
21785
21912
|
// src/core/cleanup.ts
|
|
21786
|
-
import { existsSync as
|
|
21787
|
-
import { join as
|
|
21913
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, statSync as statSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
21914
|
+
import { join as join11 } from "path";
|
|
21788
21915
|
function cleanupSessions(workspaceDir) {
|
|
21789
|
-
const dir =
|
|
21916
|
+
const dir = join11(workspaceDir, "sessions");
|
|
21790
21917
|
cleanupDir(dir, ".jsonl", SEVEN_DAYS_MS, "session");
|
|
21791
21918
|
}
|
|
21792
21919
|
function cleanupUploads(workspaceDir) {
|
|
21793
|
-
const dir =
|
|
21920
|
+
const dir = join11(workspaceDir, "uploads");
|
|
21794
21921
|
cleanupDir(dir, null, TWENTY_FOUR_HOURS_MS, "upload");
|
|
21795
21922
|
}
|
|
21796
21923
|
function cleanupNotes(workspaceDir) {
|
|
21797
|
-
const dir =
|
|
21924
|
+
const dir = join11(workspaceDir, "notes");
|
|
21798
21925
|
cleanupDir(dir, ".md", SEVEN_DAYS_MS, "note");
|
|
21799
21926
|
}
|
|
21800
21927
|
function startCleanupJobs(workspaceDir) {
|
|
@@ -21802,31 +21929,31 @@ function startCleanupJobs(workspaceDir) {
|
|
|
21802
21929
|
const timer = setInterval(() => {
|
|
21803
21930
|
runAllCleanups(workspaceDir);
|
|
21804
21931
|
}, CLEANUP_INTERVAL_MS);
|
|
21805
|
-
|
|
21932
|
+
log8.info("Cleanup jobs started (every hour)");
|
|
21806
21933
|
return () => {
|
|
21807
21934
|
clearInterval(timer);
|
|
21808
|
-
|
|
21935
|
+
log8.info("Cleanup jobs stopped");
|
|
21809
21936
|
};
|
|
21810
21937
|
}
|
|
21811
21938
|
function runAllCleanups(workspaceDir) {
|
|
21812
21939
|
try {
|
|
21813
21940
|
cleanupSessions(workspaceDir);
|
|
21814
21941
|
} catch (err) {
|
|
21815
|
-
|
|
21942
|
+
log8.error(`Session cleanup failed: ${err}`);
|
|
21816
21943
|
}
|
|
21817
21944
|
try {
|
|
21818
21945
|
cleanupUploads(workspaceDir);
|
|
21819
21946
|
} catch (err) {
|
|
21820
|
-
|
|
21947
|
+
log8.error(`Upload cleanup failed: ${err}`);
|
|
21821
21948
|
}
|
|
21822
21949
|
try {
|
|
21823
21950
|
cleanupNotes(workspaceDir);
|
|
21824
21951
|
} catch (err) {
|
|
21825
|
-
|
|
21952
|
+
log8.error(`Note cleanup failed: ${err}`);
|
|
21826
21953
|
}
|
|
21827
21954
|
}
|
|
21828
21955
|
function cleanupDir(dir, extension, maxAgeMs, label) {
|
|
21829
|
-
if (!
|
|
21956
|
+
if (!existsSync11(dir))
|
|
21830
21957
|
return;
|
|
21831
21958
|
const now = Date.now();
|
|
21832
21959
|
let deleted = 0;
|
|
@@ -21835,7 +21962,7 @@ function cleanupDir(dir, extension, maxAgeMs, label) {
|
|
|
21835
21962
|
for (const entry of entries) {
|
|
21836
21963
|
if (extension && !entry.endsWith(extension))
|
|
21837
21964
|
continue;
|
|
21838
|
-
const filePath =
|
|
21965
|
+
const filePath = join11(dir, entry);
|
|
21839
21966
|
try {
|
|
21840
21967
|
const stats = statSync7(filePath);
|
|
21841
21968
|
if (!stats.isFile())
|
|
@@ -21844,31 +21971,31 @@ function cleanupDir(dir, extension, maxAgeMs, label) {
|
|
|
21844
21971
|
if (ageMs > maxAgeMs) {
|
|
21845
21972
|
unlinkSync2(filePath);
|
|
21846
21973
|
deleted++;
|
|
21847
|
-
|
|
21974
|
+
log8.info(`Deleted old ${label}: ${entry} (age: ${(ageMs / 86400000).toFixed(1)} days)`);
|
|
21848
21975
|
}
|
|
21849
21976
|
} catch (err) {
|
|
21850
|
-
|
|
21977
|
+
log8.warn(`Failed to clean up ${filePath}: ${err}`);
|
|
21851
21978
|
}
|
|
21852
21979
|
}
|
|
21853
21980
|
} catch (err) {
|
|
21854
|
-
|
|
21981
|
+
log8.warn(`Failed to read directory ${dir}: ${err}`);
|
|
21855
21982
|
}
|
|
21856
21983
|
if (deleted > 0) {
|
|
21857
|
-
|
|
21984
|
+
log8.info(`Cleaned up ${deleted} ${label} file(s) from ${dir}`);
|
|
21858
21985
|
}
|
|
21859
21986
|
}
|
|
21860
|
-
var
|
|
21987
|
+
var log8, SEVEN_DAYS_MS, TWENTY_FOUR_HOURS_MS, CLEANUP_INTERVAL_MS;
|
|
21861
21988
|
var init_cleanup = __esm(() => {
|
|
21862
21989
|
init_logger();
|
|
21863
|
-
|
|
21990
|
+
log8 = createLogger("cleanup");
|
|
21864
21991
|
SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
21865
21992
|
TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
|
|
21866
21993
|
CLEANUP_INTERVAL_MS = 60 * 60 * 1000;
|
|
21867
21994
|
});
|
|
21868
21995
|
|
|
21869
21996
|
// src/core/heartbeat.ts
|
|
21870
|
-
import { existsSync as
|
|
21871
|
-
import { join as
|
|
21997
|
+
import { existsSync as existsSync12 } from "fs";
|
|
21998
|
+
import { join as join12 } from "path";
|
|
21872
21999
|
|
|
21873
22000
|
class Heartbeat {
|
|
21874
22001
|
timer = null;
|
|
@@ -21887,10 +22014,10 @@ class Heartbeat {
|
|
|
21887
22014
|
if (this.timer)
|
|
21888
22015
|
return;
|
|
21889
22016
|
const minutes = this.intervalMs / 60000;
|
|
21890
|
-
|
|
22017
|
+
log9.info(`Heartbeat started (every ${minutes} minute${minutes === 1 ? "" : "s"})`);
|
|
21891
22018
|
this.timer = setInterval(() => {
|
|
21892
22019
|
this.fire().catch((err) => {
|
|
21893
|
-
|
|
22020
|
+
log9.error(`Heartbeat fire failed unexpectedly: ${err}`);
|
|
21894
22021
|
});
|
|
21895
22022
|
}, this.intervalMs);
|
|
21896
22023
|
}
|
|
@@ -21898,7 +22025,7 @@ class Heartbeat {
|
|
|
21898
22025
|
if (this.timer) {
|
|
21899
22026
|
clearInterval(this.timer);
|
|
21900
22027
|
this.timer = null;
|
|
21901
|
-
|
|
22028
|
+
log9.info("Heartbeat stopped");
|
|
21902
22029
|
}
|
|
21903
22030
|
}
|
|
21904
22031
|
get processing() {
|
|
@@ -21906,7 +22033,7 @@ class Heartbeat {
|
|
|
21906
22033
|
}
|
|
21907
22034
|
async fire() {
|
|
21908
22035
|
if (this.isProcessing) {
|
|
21909
|
-
|
|
22036
|
+
log9.info("Heartbeat tick skipped \u2014 previous fire still in progress");
|
|
21910
22037
|
return;
|
|
21911
22038
|
}
|
|
21912
22039
|
this.isProcessing = true;
|
|
@@ -21914,8 +22041,8 @@ class Heartbeat {
|
|
|
21914
22041
|
try {
|
|
21915
22042
|
const config2 = getConfig();
|
|
21916
22043
|
const workspaceDir = getWorkspaceDir2();
|
|
21917
|
-
const heartbeatPath =
|
|
21918
|
-
const heartbeatExists =
|
|
22044
|
+
const heartbeatPath = join12(workspaceDir, "HEARTBEAT.md");
|
|
22045
|
+
const heartbeatExists = existsSync12(heartbeatPath);
|
|
21919
22046
|
const systemPrompt = await assembleSystemPrompt();
|
|
21920
22047
|
const prompt = this.buildPrompt(heartbeatExists);
|
|
21921
22048
|
const userMcpServers = config2.mcp.servers;
|
|
@@ -21928,11 +22055,11 @@ class Heartbeat {
|
|
|
21928
22055
|
const onDone = ({ cost }) => {
|
|
21929
22056
|
completed = true;
|
|
21930
22057
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
21931
|
-
|
|
22058
|
+
log9.info(`Heartbeat fire complete (${elapsed}s, $${cost.toFixed(4)})`);
|
|
21932
22059
|
};
|
|
21933
22060
|
const onError = ({ message, details }) => {
|
|
21934
22061
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
21935
|
-
|
|
22062
|
+
log9.error(`Heartbeat fire error (${elapsed}s): ${message}` + (details ? ` \u2014 ${details}` : ""));
|
|
21936
22063
|
};
|
|
21937
22064
|
engine.on("done", onDone);
|
|
21938
22065
|
engine.on("error", onError);
|
|
@@ -21947,10 +22074,10 @@ class Heartbeat {
|
|
|
21947
22074
|
engine.removeListener("error", onError);
|
|
21948
22075
|
}
|
|
21949
22076
|
if (!completed) {
|
|
21950
|
-
|
|
22077
|
+
log9.warn("Heartbeat fire returned without a done event");
|
|
21951
22078
|
}
|
|
21952
22079
|
} catch (err) {
|
|
21953
|
-
|
|
22080
|
+
log9.error(`Heartbeat fire threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
21954
22081
|
} finally {
|
|
21955
22082
|
this.isProcessing = false;
|
|
21956
22083
|
}
|
|
@@ -21977,14 +22104,14 @@ class Heartbeat {
|
|
|
21977
22104
|
`);
|
|
21978
22105
|
}
|
|
21979
22106
|
}
|
|
21980
|
-
var
|
|
22107
|
+
var log9;
|
|
21981
22108
|
var init_heartbeat = __esm(() => {
|
|
21982
22109
|
init_config();
|
|
21983
22110
|
init_logger();
|
|
21984
22111
|
init_engine();
|
|
21985
22112
|
init_memory();
|
|
21986
22113
|
init_pair_mcp();
|
|
21987
|
-
|
|
22114
|
+
log9 = createLogger("heartbeat");
|
|
21988
22115
|
});
|
|
21989
22116
|
|
|
21990
22117
|
// src/core/health.ts
|
|
@@ -22011,26 +22138,26 @@ function startHealthCheck(port, adapters) {
|
|
|
22011
22138
|
return new Response("Not Found", { status: 404 });
|
|
22012
22139
|
}
|
|
22013
22140
|
});
|
|
22014
|
-
|
|
22141
|
+
log10.info(`Health check server listening on port ${port}`);
|
|
22015
22142
|
}
|
|
22016
22143
|
function stopHealthCheck() {
|
|
22017
22144
|
if (server) {
|
|
22018
22145
|
server.stop();
|
|
22019
22146
|
server = null;
|
|
22020
|
-
|
|
22147
|
+
log10.info("Health check server stopped");
|
|
22021
22148
|
}
|
|
22022
22149
|
}
|
|
22023
|
-
var
|
|
22150
|
+
var log10, server = null, startedAt;
|
|
22024
22151
|
var init_health = __esm(() => {
|
|
22025
22152
|
init_logger();
|
|
22026
|
-
|
|
22153
|
+
log10 = createLogger("health");
|
|
22027
22154
|
startedAt = Date.now();
|
|
22028
22155
|
});
|
|
22029
22156
|
|
|
22030
22157
|
// src/adapters/telegram.ts
|
|
22031
|
-
import { existsSync as
|
|
22032
|
-
import { mkdir, readFile as
|
|
22033
|
-
import { join as
|
|
22158
|
+
import { existsSync as existsSync13 } from "fs";
|
|
22159
|
+
import { mkdir as mkdir2, readFile as readFile9 } from "fs/promises";
|
|
22160
|
+
import { join as join13 } from "path";
|
|
22034
22161
|
function escapeHtml(text) {
|
|
22035
22162
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
22036
22163
|
}
|
|
@@ -22134,13 +22261,13 @@ class TelegramAdapter {
|
|
|
22134
22261
|
await ctx.reply("Hi! I'm Pair, your personal AI assistant. Send me a message and I'll help.");
|
|
22135
22262
|
});
|
|
22136
22263
|
this.bot.command("tasks", async (ctx) => {
|
|
22137
|
-
const tasksPath =
|
|
22264
|
+
const tasksPath = join13(this.workspaceDir, "tasks.md");
|
|
22138
22265
|
try {
|
|
22139
|
-
if (!
|
|
22266
|
+
if (!existsSync13(tasksPath)) {
|
|
22140
22267
|
await ctx.reply("No tasks yet.");
|
|
22141
22268
|
return;
|
|
22142
22269
|
}
|
|
22143
|
-
const content = await
|
|
22270
|
+
const content = await readFile9(tasksPath, "utf-8");
|
|
22144
22271
|
if (!content.trim()) {
|
|
22145
22272
|
await ctx.reply("No tasks yet.");
|
|
22146
22273
|
return;
|
|
@@ -22189,10 +22316,10 @@ class TelegramAdapter {
|
|
|
22189
22316
|
const url2 = `https://api.telegram.org/file/bot${this.token}/${file2.file_path}`;
|
|
22190
22317
|
const response = await fetch(url2);
|
|
22191
22318
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
22192
|
-
const uploadsDir =
|
|
22193
|
-
await
|
|
22319
|
+
const uploadsDir = join13(this.workspaceDir, "uploads");
|
|
22320
|
+
await mkdir2(uploadsDir, { recursive: true });
|
|
22194
22321
|
const filename = `photo_${Date.now()}.jpg`;
|
|
22195
|
-
const uploadPath =
|
|
22322
|
+
const uploadPath = join13(uploadsDir, filename);
|
|
22196
22323
|
await Bun.write(uploadPath, buffer);
|
|
22197
22324
|
this.log.info(`Photo saved: ${uploadPath} (${buffer.length} bytes)`);
|
|
22198
22325
|
const caption = ctx.message.caption ? `
|
|
@@ -22221,10 +22348,10 @@ Please analyze this image using the Read tool.`
|
|
|
22221
22348
|
const url2 = `https://api.telegram.org/file/bot${this.token}/${file2.file_path}`;
|
|
22222
22349
|
const response = await fetch(url2);
|
|
22223
22350
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
22224
|
-
const uploadsDir =
|
|
22225
|
-
await
|
|
22351
|
+
const uploadsDir = join13(this.workspaceDir, "uploads");
|
|
22352
|
+
await mkdir2(uploadsDir, { recursive: true });
|
|
22226
22353
|
const filename = doc2.file_name ?? `document_${Date.now()}`;
|
|
22227
|
-
const uploadPath =
|
|
22354
|
+
const uploadPath = join13(uploadsDir, filename);
|
|
22228
22355
|
await Bun.write(uploadPath, buffer);
|
|
22229
22356
|
this.log.info(`Document saved: ${uploadPath} (${buffer.length} bytes, ${doc2.mime_type ?? "unknown type"})`);
|
|
22230
22357
|
const caption = ctx.message.caption ? `
|
|
@@ -26366,22 +26493,22 @@ __export(exports_start, {
|
|
|
26366
26493
|
default: () => start_default
|
|
26367
26494
|
});
|
|
26368
26495
|
import {
|
|
26369
|
-
existsSync as
|
|
26496
|
+
existsSync as existsSync14,
|
|
26370
26497
|
mkdirSync as mkdirSync4,
|
|
26371
26498
|
readdirSync as readdirSync4,
|
|
26372
26499
|
statSync as statSync8,
|
|
26373
26500
|
unlinkSync as unlinkSync3,
|
|
26374
26501
|
writeFileSync
|
|
26375
26502
|
} from "fs";
|
|
26376
|
-
import { readFile as
|
|
26377
|
-
import { join as
|
|
26503
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
26504
|
+
import { join as join14 } from "path";
|
|
26378
26505
|
function getPidFile() {
|
|
26379
|
-
return
|
|
26506
|
+
return join14(getPairHome(), "pair.pid");
|
|
26380
26507
|
}
|
|
26381
26508
|
function getLogDir2() {
|
|
26382
|
-
return
|
|
26509
|
+
return join14(getPairHome(), "logs");
|
|
26383
26510
|
}
|
|
26384
|
-
async function validateOAuth(
|
|
26511
|
+
async function validateOAuth(log11) {
|
|
26385
26512
|
try {
|
|
26386
26513
|
const proc = Bun.spawn(["claude", "auth", "status"], {
|
|
26387
26514
|
stdout: "pipe",
|
|
@@ -26390,11 +26517,11 @@ async function validateOAuth(log10) {
|
|
|
26390
26517
|
const exitCode = await proc.exited;
|
|
26391
26518
|
const stdout = await new Response(proc.stdout).text();
|
|
26392
26519
|
if (exitCode === 0) {
|
|
26393
|
-
|
|
26520
|
+
log11.info(`OAuth: authenticated \u2014 ${stdout.trim().split(`
|
|
26394
26521
|
`)[0]}`);
|
|
26395
26522
|
return true;
|
|
26396
26523
|
}
|
|
26397
|
-
|
|
26524
|
+
log11.warn("OAuth: not authenticated \u2014 run `claude login`");
|
|
26398
26525
|
return false;
|
|
26399
26526
|
} catch {
|
|
26400
26527
|
try {
|
|
@@ -26404,11 +26531,11 @@ async function validateOAuth(log10) {
|
|
|
26404
26531
|
});
|
|
26405
26532
|
const exitCode = await proc.exited;
|
|
26406
26533
|
if (exitCode === 0) {
|
|
26407
|
-
|
|
26534
|
+
log11.info("OAuth: Claude CLI found (could not verify auth status)");
|
|
26408
26535
|
return true;
|
|
26409
26536
|
}
|
|
26410
26537
|
} catch {}
|
|
26411
|
-
|
|
26538
|
+
log11.warn("OAuth: Claude CLI not found \u2014 install and run `claude login`");
|
|
26412
26539
|
return false;
|
|
26413
26540
|
}
|
|
26414
26541
|
}
|
|
@@ -26416,7 +26543,7 @@ function spawnDaemon() {
|
|
|
26416
26543
|
const pairHome = getPairHome();
|
|
26417
26544
|
const logDir = getLogDir2();
|
|
26418
26545
|
const pidFile = getPidFile();
|
|
26419
|
-
if (!
|
|
26546
|
+
if (!existsSync14(logDir)) {
|
|
26420
26547
|
mkdirSync4(logDir, { recursive: true });
|
|
26421
26548
|
}
|
|
26422
26549
|
const args = process.argv.slice(1).filter((a) => a !== "-d" && a !== "--daemon");
|
|
@@ -26426,19 +26553,19 @@ function spawnDaemon() {
|
|
|
26426
26553
|
env: { ...process.env, PAIR_HOME: pairHome }
|
|
26427
26554
|
});
|
|
26428
26555
|
const pid = child.pid;
|
|
26429
|
-
if (!
|
|
26556
|
+
if (!existsSync14(pairHome)) {
|
|
26430
26557
|
mkdirSync4(pairHome, { recursive: true });
|
|
26431
26558
|
}
|
|
26432
26559
|
writeFileSync(pidFile, String(pid), "utf-8");
|
|
26433
26560
|
child.unref();
|
|
26434
26561
|
console.log(` Pair started in background (PID: ${pid})`);
|
|
26435
26562
|
console.log(` PID file: ${pidFile}`);
|
|
26436
|
-
console.log(` Logs: ${
|
|
26563
|
+
console.log(` Logs: ${join14(logDir, "pair.log")}
|
|
26437
26564
|
`);
|
|
26438
26565
|
}
|
|
26439
26566
|
function writePidFile() {
|
|
26440
26567
|
const pairHome = getPairHome();
|
|
26441
|
-
if (!
|
|
26568
|
+
if (!existsSync14(pairHome)) {
|
|
26442
26569
|
mkdirSync4(pairHome, { recursive: true });
|
|
26443
26570
|
}
|
|
26444
26571
|
writeFileSync(getPidFile(), String(process.pid), "utf-8");
|
|
@@ -26446,42 +26573,42 @@ function writePidFile() {
|
|
|
26446
26573
|
function removePidFile() {
|
|
26447
26574
|
try {
|
|
26448
26575
|
const pidFile = getPidFile();
|
|
26449
|
-
if (
|
|
26576
|
+
if (existsSync14(pidFile)) {
|
|
26450
26577
|
unlinkSync3(pidFile);
|
|
26451
26578
|
}
|
|
26452
26579
|
} catch {}
|
|
26453
26580
|
}
|
|
26454
|
-
async function ensureWorkspaceInitialized(workspaceDir,
|
|
26455
|
-
if (
|
|
26581
|
+
async function ensureWorkspaceInitialized(workspaceDir, log11) {
|
|
26582
|
+
if (existsSync14(workspaceDir))
|
|
26456
26583
|
return;
|
|
26457
|
-
const templatesDir =
|
|
26458
|
-
if (!
|
|
26459
|
-
|
|
26584
|
+
const templatesDir = join14(getPackageRoot(), "workspace-init");
|
|
26585
|
+
if (!existsSync14(templatesDir)) {
|
|
26586
|
+
log11.warn(`Workspace dir ${workspaceDir} does not exist and templates not found at ${templatesDir} \u2014 creating empty workspace`);
|
|
26460
26587
|
mkdirSync4(workspaceDir, { recursive: true });
|
|
26461
26588
|
return;
|
|
26462
26589
|
}
|
|
26463
|
-
|
|
26590
|
+
log11.info(`Initializing workspace at ${workspaceDir} from templates`);
|
|
26464
26591
|
mkdirSync4(workspaceDir, { recursive: true });
|
|
26465
|
-
mkdirSync4(
|
|
26466
|
-
mkdirSync4(
|
|
26467
|
-
mkdirSync4(
|
|
26592
|
+
mkdirSync4(join14(workspaceDir, "notes"), { recursive: true });
|
|
26593
|
+
mkdirSync4(join14(workspaceDir, "uploads"), { recursive: true });
|
|
26594
|
+
mkdirSync4(join14(workspaceDir, "sessions"), { recursive: true });
|
|
26468
26595
|
for (const file2 of readdirSync4(templatesDir)) {
|
|
26469
|
-
const src =
|
|
26596
|
+
const src = join14(templatesDir, file2);
|
|
26470
26597
|
try {
|
|
26471
26598
|
if (statSync8(src).isDirectory())
|
|
26472
26599
|
continue;
|
|
26473
26600
|
} catch {
|
|
26474
26601
|
continue;
|
|
26475
26602
|
}
|
|
26476
|
-
const dest =
|
|
26477
|
-
if (
|
|
26603
|
+
const dest = join14(workspaceDir, file2);
|
|
26604
|
+
if (existsSync14(dest))
|
|
26478
26605
|
continue;
|
|
26479
26606
|
try {
|
|
26480
|
-
const content = await
|
|
26607
|
+
const content = await readFile10(src, "utf-8");
|
|
26481
26608
|
await Bun.write(dest, content);
|
|
26482
|
-
|
|
26609
|
+
log11.info(` Copied ${file2}`);
|
|
26483
26610
|
} catch (err) {
|
|
26484
|
-
|
|
26611
|
+
log11.warn(` Failed to copy ${file2}: ${String(err)}`);
|
|
26485
26612
|
}
|
|
26486
26613
|
}
|
|
26487
26614
|
}
|
|
@@ -26525,22 +26652,22 @@ var init_start = __esm(() => {
|
|
|
26525
26652
|
level: config2.pair.logLevel,
|
|
26526
26653
|
stdout: !isDaemonChild
|
|
26527
26654
|
});
|
|
26528
|
-
const
|
|
26529
|
-
|
|
26530
|
-
|
|
26531
|
-
|
|
26532
|
-
|
|
26533
|
-
|
|
26534
|
-
await ensureWorkspaceInitialized(config2.pair.workspaceDir,
|
|
26655
|
+
const log11 = createLogger("main");
|
|
26656
|
+
log11.info("Pair is starting up...");
|
|
26657
|
+
log11.info(`Home: ${getPairHome()} (${getPairMode()} mode)`);
|
|
26658
|
+
log11.info(`Model: ${config2.pair.model}`);
|
|
26659
|
+
log11.info(`Auth: ${config2.pair.authMode}${config2.pair.apiKeyHelper ? ` (helper: ${config2.pair.apiKeyHelper})` : ""}`);
|
|
26660
|
+
log11.info(`Workspace: ${config2.pair.workspaceDir}`);
|
|
26661
|
+
await ensureWorkspaceInitialized(config2.pair.workspaceDir, log11);
|
|
26535
26662
|
if (config2.pair.authMode === "oauth") {
|
|
26536
|
-
const authOk = await validateOAuth(
|
|
26663
|
+
const authOk = await validateOAuth(log11);
|
|
26537
26664
|
if (!authOk && !isDaemonChild) {
|
|
26538
26665
|
console.log(`
|
|
26539
26666
|
Warning: Could not verify OAuth credentials.
|
|
26540
26667
|
` + " Run `claude login` to authenticate with your Claude Max/Pro subscription.\n");
|
|
26541
26668
|
}
|
|
26542
26669
|
} else if (!process.env.ANTHROPIC_API_KEY) {
|
|
26543
|
-
|
|
26670
|
+
log11.warn("ANTHROPIC_API_KEY is not set \u2014 queries will fail");
|
|
26544
26671
|
if (!isDaemonChild) {
|
|
26545
26672
|
console.log(`
|
|
26546
26673
|
Warning: ANTHROPIC_API_KEY is not set. Run 'pair setup' to configure.
|
|
@@ -26549,7 +26676,7 @@ var init_start = __esm(() => {
|
|
|
26549
26676
|
}
|
|
26550
26677
|
if (isDaemonChild) {
|
|
26551
26678
|
writePidFile();
|
|
26552
|
-
|
|
26679
|
+
log11.info(`Daemon started (PID: ${process.pid})`);
|
|
26553
26680
|
}
|
|
26554
26681
|
const sessionManager = new SessionManager;
|
|
26555
26682
|
sessionManager.startSweep();
|
|
@@ -26558,32 +26685,32 @@ var init_start = __esm(() => {
|
|
|
26558
26685
|
const stopCleanup = startCleanupJobs(config2.pair.workspaceDir);
|
|
26559
26686
|
const telegramToken = config2.telegram.botToken;
|
|
26560
26687
|
if (telegramToken) {
|
|
26561
|
-
|
|
26688
|
+
log11.info("Telegram adapter enabled \u2014 connecting...");
|
|
26562
26689
|
const telegram = new TelegramAdapter(telegramToken, config2.pair.workspaceDir);
|
|
26563
26690
|
router.registerAdapter(telegram);
|
|
26564
26691
|
adapters.push(telegram);
|
|
26565
26692
|
try {
|
|
26566
26693
|
await telegram.start();
|
|
26567
26694
|
} catch (err) {
|
|
26568
|
-
|
|
26695
|
+
log11.error(`Failed to start Telegram adapter: ${String(err)}`);
|
|
26569
26696
|
}
|
|
26570
26697
|
} else {
|
|
26571
|
-
|
|
26698
|
+
log11.warn("Telegram adapter disabled \u2014 no bot token configured");
|
|
26572
26699
|
}
|
|
26573
26700
|
const slackBotToken = config2.slack.botToken;
|
|
26574
26701
|
const slackAppToken = config2.slack.appToken;
|
|
26575
26702
|
if (slackBotToken && slackAppToken) {
|
|
26576
|
-
|
|
26703
|
+
log11.info("Slack adapter enabled \u2014 connecting...");
|
|
26577
26704
|
const slack = new SlackAdapter(slackBotToken, slackAppToken);
|
|
26578
26705
|
router.registerAdapter(slack);
|
|
26579
26706
|
adapters.push(slack);
|
|
26580
26707
|
try {
|
|
26581
26708
|
await slack.start();
|
|
26582
26709
|
} catch (err) {
|
|
26583
|
-
|
|
26710
|
+
log11.error(`Failed to start Slack adapter: ${String(err)}`);
|
|
26584
26711
|
}
|
|
26585
26712
|
} else {
|
|
26586
|
-
|
|
26713
|
+
log11.warn("Slack adapter disabled \u2014 no bot/app tokens configured");
|
|
26587
26714
|
}
|
|
26588
26715
|
if (config2.pair.healthCheckPort) {
|
|
26589
26716
|
startHealthCheck(config2.pair.healthCheckPort, adapters);
|
|
@@ -26592,19 +26719,19 @@ var init_start = __esm(() => {
|
|
|
26592
26719
|
const heartbeatInterval = config2.pair.heartbeatIntervalMinutes;
|
|
26593
26720
|
if (heartbeatInterval && heartbeatInterval > 0) {
|
|
26594
26721
|
if (adapters.length === 0) {
|
|
26595
|
-
|
|
26722
|
+
log11.warn("Heartbeat is configured but no adapters are running \u2014 " + "the agent will wake up but `send_message` calls will fail.");
|
|
26596
26723
|
}
|
|
26597
26724
|
if (!config2.pair.defaultChannel || !config2.pair.defaultChannelId) {
|
|
26598
|
-
|
|
26725
|
+
log11.warn("Heartbeat is configured but pair.defaultChannel/defaultChannelId are not set \u2014 " + "the agent must pass channel + channelId explicitly to every send_message call.");
|
|
26599
26726
|
}
|
|
26600
26727
|
heartbeat = new Heartbeat({
|
|
26601
26728
|
adapters: router.getAdapters(),
|
|
26602
26729
|
intervalMinutes: heartbeatInterval
|
|
26603
26730
|
});
|
|
26604
26731
|
heartbeat.start();
|
|
26605
|
-
|
|
26732
|
+
log11.info(`Heartbeat enabled: every ${heartbeatInterval} min` + (config2.pair.defaultChannel ? `, default target ${config2.pair.defaultChannel}:${config2.pair.defaultChannelId}` : ""));
|
|
26606
26733
|
} else {
|
|
26607
|
-
|
|
26734
|
+
log11.info("Heartbeat disabled (pair.heartbeatIntervalMinutes not set)");
|
|
26608
26735
|
}
|
|
26609
26736
|
if (!isDaemonChild) {
|
|
26610
26737
|
const channels = adapters.map((a) => a.name);
|
|
@@ -26614,7 +26741,7 @@ var init_start = __esm(() => {
|
|
|
26614
26741
|
console.log(` Channels: ${channels.length > 0 ? channels.join(", ") : "(none)"}`);
|
|
26615
26742
|
console.log();
|
|
26616
26743
|
if (channels.length === 0) {
|
|
26617
|
-
console.log(` No adapters are enabled. Configure a bot token in ${
|
|
26744
|
+
console.log(` No adapters are enabled. Configure a bot token in ${join14(getPairHome(), ".env")}`);
|
|
26618
26745
|
console.log(` and run 'pair setup' to get started.
|
|
26619
26746
|
`);
|
|
26620
26747
|
}
|
|
@@ -26626,7 +26753,7 @@ var init_start = __esm(() => {
|
|
|
26626
26753
|
if (shuttingDown)
|
|
26627
26754
|
return;
|
|
26628
26755
|
shuttingDown = true;
|
|
26629
|
-
|
|
26756
|
+
log11.info("Shutdown signal received \u2014 shutting down...");
|
|
26630
26757
|
if (!isDaemonChild) {
|
|
26631
26758
|
console.log(`
|
|
26632
26759
|
Shutting down...`);
|
|
@@ -26639,17 +26766,17 @@ var init_start = __esm(() => {
|
|
|
26639
26766
|
sessionManager.stopSweep();
|
|
26640
26767
|
for (const adapter of adapters) {
|
|
26641
26768
|
try {
|
|
26642
|
-
|
|
26769
|
+
log11.info(`Stopping ${adapter.name} adapter...`);
|
|
26643
26770
|
await adapter.stop();
|
|
26644
|
-
|
|
26771
|
+
log11.info(`${adapter.name} adapter stopped`);
|
|
26645
26772
|
} catch (err) {
|
|
26646
|
-
|
|
26773
|
+
log11.error(`Error stopping ${adapter.name}: ${String(err)}`);
|
|
26647
26774
|
}
|
|
26648
26775
|
}
|
|
26649
26776
|
if (isDaemonChild) {
|
|
26650
26777
|
removePidFile();
|
|
26651
26778
|
}
|
|
26652
|
-
|
|
26779
|
+
log11.info("Pair stopped");
|
|
26653
26780
|
if (!isDaemonChild) {
|
|
26654
26781
|
console.log(` Pair stopped.
|
|
26655
26782
|
`);
|
|
@@ -26671,10 +26798,10 @@ var exports_stop = {};
|
|
|
26671
26798
|
__export(exports_stop, {
|
|
26672
26799
|
default: () => stop_default
|
|
26673
26800
|
});
|
|
26674
|
-
import { existsSync as
|
|
26675
|
-
import { join as
|
|
26801
|
+
import { existsSync as existsSync15, readFileSync, unlinkSync as unlinkSync4 } from "fs";
|
|
26802
|
+
import { join as join15 } from "path";
|
|
26676
26803
|
function getPidFile2() {
|
|
26677
|
-
return
|
|
26804
|
+
return join15(getPairHome(), "pair.pid");
|
|
26678
26805
|
}
|
|
26679
26806
|
function isProcessRunning(pid) {
|
|
26680
26807
|
try {
|
|
@@ -26697,7 +26824,7 @@ async function waitForExit(pid, timeoutMs) {
|
|
|
26697
26824
|
function removePidFile2() {
|
|
26698
26825
|
try {
|
|
26699
26826
|
const pidFile = getPidFile2();
|
|
26700
|
-
if (
|
|
26827
|
+
if (existsSync15(pidFile)) {
|
|
26701
26828
|
unlinkSync4(pidFile);
|
|
26702
26829
|
}
|
|
26703
26830
|
} catch {}
|
|
@@ -26713,7 +26840,7 @@ var init_stop = __esm(() => {
|
|
|
26713
26840
|
},
|
|
26714
26841
|
async run() {
|
|
26715
26842
|
const pidFile = getPidFile2();
|
|
26716
|
-
if (!
|
|
26843
|
+
if (!existsSync15(pidFile)) {
|
|
26717
26844
|
console.log(` Pair is not running (no PID file found).
|
|
26718
26845
|
`);
|
|
26719
26846
|
return;
|
|
@@ -26763,8 +26890,8 @@ var exports_status = {};
|
|
|
26763
26890
|
__export(exports_status, {
|
|
26764
26891
|
default: () => status_default
|
|
26765
26892
|
});
|
|
26766
|
-
import { existsSync as
|
|
26767
|
-
import { join as
|
|
26893
|
+
import { existsSync as existsSync16, readFileSync as readFileSync2, readdirSync as readdirSync5, statSync as statSync9 } from "fs";
|
|
26894
|
+
import { join as join16 } from "path";
|
|
26768
26895
|
function isProcessRunning2(pid) {
|
|
26769
26896
|
try {
|
|
26770
26897
|
process.kill(pid, 0);
|
|
@@ -26790,12 +26917,12 @@ function formatUptime(seconds) {
|
|
|
26790
26917
|
return parts.join(" ");
|
|
26791
26918
|
}
|
|
26792
26919
|
function countFiles(dir) {
|
|
26793
|
-
if (!
|
|
26920
|
+
if (!existsSync16(dir))
|
|
26794
26921
|
return 0;
|
|
26795
26922
|
try {
|
|
26796
26923
|
return readdirSync5(dir).filter((entry) => {
|
|
26797
26924
|
try {
|
|
26798
|
-
return statSync9(
|
|
26925
|
+
return statSync9(join16(dir, entry)).isFile();
|
|
26799
26926
|
} catch {
|
|
26800
26927
|
return false;
|
|
26801
26928
|
}
|
|
@@ -26805,7 +26932,7 @@ function countFiles(dir) {
|
|
|
26805
26932
|
}
|
|
26806
26933
|
}
|
|
26807
26934
|
function countLines(filePath) {
|
|
26808
|
-
if (!
|
|
26935
|
+
if (!existsSync16(filePath))
|
|
26809
26936
|
return 0;
|
|
26810
26937
|
try {
|
|
26811
26938
|
const content = readFileSync2(filePath, "utf-8");
|
|
@@ -26816,7 +26943,7 @@ function countLines(filePath) {
|
|
|
26816
26943
|
}
|
|
26817
26944
|
}
|
|
26818
26945
|
function readFilePreview(filePath, maxLines = 10) {
|
|
26819
|
-
if (!
|
|
26946
|
+
if (!existsSync16(filePath))
|
|
26820
26947
|
return null;
|
|
26821
26948
|
try {
|
|
26822
26949
|
const content = readFileSync2(filePath, "utf-8").trim();
|
|
@@ -26848,13 +26975,13 @@ var init_status = __esm(() => {
|
|
|
26848
26975
|
`);
|
|
26849
26976
|
const pairHome = getPairHome();
|
|
26850
26977
|
const pairMode = getPairMode();
|
|
26851
|
-
const pidFile =
|
|
26852
|
-
const workspaceDir =
|
|
26978
|
+
const pidFile = join16(pairHome, "pair.pid");
|
|
26979
|
+
const workspaceDir = join16(pairHome, "workspace");
|
|
26853
26980
|
console.log(` Home: ${pairHome} (${pairMode} mode)`);
|
|
26854
26981
|
let pid = null;
|
|
26855
26982
|
let running = false;
|
|
26856
26983
|
let uptime = null;
|
|
26857
|
-
if (
|
|
26984
|
+
if (existsSync16(pidFile)) {
|
|
26858
26985
|
const pidStr = readFileSync2(pidFile, "utf-8").trim();
|
|
26859
26986
|
pid = Number(pidStr);
|
|
26860
26987
|
if (pid && !Number.isNaN(pid) && isProcessRunning2(pid)) {
|
|
@@ -26877,11 +27004,11 @@ var init_status = __esm(() => {
|
|
|
26877
27004
|
}
|
|
26878
27005
|
console.log();
|
|
26879
27006
|
console.log(" Workspace");
|
|
26880
|
-
const notesDir =
|
|
26881
|
-
const uploadsDir =
|
|
26882
|
-
const sessionsDir =
|
|
26883
|
-
const memoryPath =
|
|
26884
|
-
const tasksPath =
|
|
27007
|
+
const notesDir = join16(workspaceDir, "notes");
|
|
27008
|
+
const uploadsDir = join16(workspaceDir, "uploads");
|
|
27009
|
+
const sessionsDir = join16(workspaceDir, "sessions");
|
|
27010
|
+
const memoryPath = join16(workspaceDir, "MEMORY.md");
|
|
27011
|
+
const tasksPath = join16(workspaceDir, "tasks.md");
|
|
26885
27012
|
const noteCount = countFiles(notesDir);
|
|
26886
27013
|
const uploadCount = countFiles(uploadsDir);
|
|
26887
27014
|
const sessionCount = countFiles(sessionsDir);
|
|
@@ -26927,7 +27054,7 @@ init_dist();
|
|
|
26927
27054
|
// package.json
|
|
26928
27055
|
var package_default = {
|
|
26929
27056
|
name: "@sneub/pair",
|
|
26930
|
-
version: "0.0.
|
|
27057
|
+
version: "0.0.7",
|
|
26931
27058
|
type: "module",
|
|
26932
27059
|
description: "A personal AI assistant powered by Claude Code \u2014 connects Telegram and Slack to Claude with persistent memory and custom tools",
|
|
26933
27060
|
bin: {
|
package/package.json
CHANGED