clearclaw 0.1.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/dist/channel/telegram.d.ts +17 -0
- package/dist/channel/telegram.js +162 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +38 -0
- package/dist/db.d.ts +6 -0
- package/dist/db.js +42 -0
- package/dist/engine/claude-code.d.ts +5 -0
- package/dist/engine/claude-code.js +137 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.js +106 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +26 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.js +28 -0
- package/dist/orchestrator.d.ts +20 -0
- package/dist/orchestrator.js +135 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.js +2 -0
- package/dist/workspace-store.d.ts +11 -0
- package/dist/workspace-store.js +45 -0
- package/package.json +33 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { Channel, Button, ButtonResponse, SendMessageOpts } from "../types.js";
|
|
3
|
+
export declare class TelegramChannel extends EventEmitter implements Channel {
|
|
4
|
+
name: string;
|
|
5
|
+
private bot;
|
|
6
|
+
private allowedUserId;
|
|
7
|
+
private typingIntervals;
|
|
8
|
+
private pendingCallbacks;
|
|
9
|
+
constructor(botToken: string, allowedUserId: number);
|
|
10
|
+
connect(): Promise<void>;
|
|
11
|
+
disconnect(): Promise<void>;
|
|
12
|
+
ownsId(chatId: string): boolean;
|
|
13
|
+
sendMessage(chatId: string, text: string, opts?: SendMessageOpts): Promise<void>;
|
|
14
|
+
sendInteractive(chatId: string, text: string, buttons: Button[]): Promise<ButtonResponse>;
|
|
15
|
+
setTyping(chatId: string, isTyping: boolean): Promise<void>;
|
|
16
|
+
private numericId;
|
|
17
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { Bot, InlineKeyboard } from "grammy";
|
|
3
|
+
import log from "../logger.js";
|
|
4
|
+
export class TelegramChannel extends EventEmitter {
|
|
5
|
+
name = "telegram";
|
|
6
|
+
bot;
|
|
7
|
+
allowedUserId;
|
|
8
|
+
typingIntervals = new Map();
|
|
9
|
+
// Pending interactive responses: callbackId → resolve function
|
|
10
|
+
pendingCallbacks = new Map();
|
|
11
|
+
constructor(botToken, allowedUserId) {
|
|
12
|
+
super();
|
|
13
|
+
this.allowedUserId = allowedUserId;
|
|
14
|
+
this.bot = new Bot(botToken);
|
|
15
|
+
}
|
|
16
|
+
async connect() {
|
|
17
|
+
// Auth check: only respond to allowed user
|
|
18
|
+
this.bot.on("message:text", (ctx) => {
|
|
19
|
+
if (ctx.from?.id !== this.allowedUserId)
|
|
20
|
+
return;
|
|
21
|
+
const chatId = `tg:${ctx.chat.id}`;
|
|
22
|
+
this.emit("message", { chatId, text: ctx.message.text });
|
|
23
|
+
});
|
|
24
|
+
// Handle inline keyboard button presses
|
|
25
|
+
this.bot.on("callback_query:data", async (ctx) => {
|
|
26
|
+
const data = ctx.callbackQuery.data;
|
|
27
|
+
const resolve = this.pendingCallbacks.get(data);
|
|
28
|
+
if (resolve) {
|
|
29
|
+
this.pendingCallbacks.delete(data);
|
|
30
|
+
resolve(data);
|
|
31
|
+
}
|
|
32
|
+
await ctx.answerCallbackQuery();
|
|
33
|
+
});
|
|
34
|
+
this.bot.catch((err) => {
|
|
35
|
+
log.error("[channel] Telegram bot error: %s", err.message);
|
|
36
|
+
});
|
|
37
|
+
await new Promise((resolve) => {
|
|
38
|
+
this.bot.start({
|
|
39
|
+
onStart: (botInfo) => {
|
|
40
|
+
log.info(`[channel] Telegram bot: @${botInfo.username}`);
|
|
41
|
+
resolve();
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async disconnect() {
|
|
47
|
+
for (const interval of this.typingIntervals.values()) {
|
|
48
|
+
clearInterval(interval);
|
|
49
|
+
}
|
|
50
|
+
this.typingIntervals.clear();
|
|
51
|
+
await this.bot.stop();
|
|
52
|
+
}
|
|
53
|
+
ownsId(chatId) {
|
|
54
|
+
return chatId.startsWith("tg:");
|
|
55
|
+
}
|
|
56
|
+
async sendMessage(chatId, text, opts) {
|
|
57
|
+
const numId = this.numericId(chatId);
|
|
58
|
+
const chunks = splitMessage(text);
|
|
59
|
+
for (const chunk of chunks) {
|
|
60
|
+
try {
|
|
61
|
+
await this.bot.api.sendMessage(numId, chunk, {
|
|
62
|
+
parse_mode: opts?.parseMode,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (opts?.parseMode) {
|
|
67
|
+
log.warn("[channel] sendMessage failed with %s, retrying as plain text", opts.parseMode);
|
|
68
|
+
await this.bot.api.sendMessage(numId, chunk);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async sendInteractive(chatId, text, buttons) {
|
|
77
|
+
const numId = this.numericId(chatId);
|
|
78
|
+
const callbackId = crypto.randomUUID().slice(0, 8);
|
|
79
|
+
// Pre-compute callback data keys
|
|
80
|
+
const cbEntries = buttons.map((btn) => ({
|
|
81
|
+
btn,
|
|
82
|
+
cbData: `${callbackId}:${btn.value}`,
|
|
83
|
+
}));
|
|
84
|
+
// Build inline keyboard
|
|
85
|
+
const keyboard = new InlineKeyboard();
|
|
86
|
+
for (const { btn, cbData } of cbEntries) {
|
|
87
|
+
keyboard.text(btn.label, cbData);
|
|
88
|
+
}
|
|
89
|
+
await this.bot.api.sendMessage(numId, text, {
|
|
90
|
+
reply_markup: keyboard,
|
|
91
|
+
});
|
|
92
|
+
const cleanupAll = () => {
|
|
93
|
+
for (const { cbData } of cbEntries) {
|
|
94
|
+
this.pendingCallbacks.delete(cbData);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
// Wait for button press or timeout (30 minutes)
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const timeout = setTimeout(() => {
|
|
100
|
+
cleanupAll();
|
|
101
|
+
resolve({ value: "" });
|
|
102
|
+
}, 30 * 60 * 1000);
|
|
103
|
+
for (const { btn, cbData } of cbEntries) {
|
|
104
|
+
this.pendingCallbacks.set(cbData, () => {
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
cleanupAll();
|
|
107
|
+
resolve({ value: btn.value });
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async setTyping(chatId, isTyping) {
|
|
113
|
+
if (isTyping) {
|
|
114
|
+
if (this.typingIntervals.has(chatId))
|
|
115
|
+
return;
|
|
116
|
+
const numId = this.numericId(chatId);
|
|
117
|
+
// Send immediately, then repeat every 4s
|
|
118
|
+
await this.bot.api
|
|
119
|
+
.sendChatAction(numId, "typing")
|
|
120
|
+
.catch(() => { });
|
|
121
|
+
const interval = setInterval(() => {
|
|
122
|
+
this.bot.api.sendChatAction(numId, "typing").catch(() => { });
|
|
123
|
+
}, 4000);
|
|
124
|
+
this.typingIntervals.set(chatId, interval);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const interval = this.typingIntervals.get(chatId);
|
|
128
|
+
if (interval) {
|
|
129
|
+
clearInterval(interval);
|
|
130
|
+
this.typingIntervals.delete(chatId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
numericId(chatId) {
|
|
135
|
+
return Number(chatId.replace(/^tg:/, ""));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const MAX_MSG_LEN = 4096;
|
|
139
|
+
/** Split text into ≤4096-char chunks, preferring newline boundaries. */
|
|
140
|
+
function splitMessage(text) {
|
|
141
|
+
if (text.length <= MAX_MSG_LEN)
|
|
142
|
+
return [text];
|
|
143
|
+
const chunks = [];
|
|
144
|
+
let remaining = text;
|
|
145
|
+
while (remaining.length > MAX_MSG_LEN) {
|
|
146
|
+
// Find last newline within the limit
|
|
147
|
+
const newlineAt = remaining.lastIndexOf("\n", MAX_MSG_LEN);
|
|
148
|
+
if (newlineAt > 0) {
|
|
149
|
+
chunks.push(remaining.slice(0, newlineAt));
|
|
150
|
+
remaining = remaining.slice(newlineAt + 1); // skip the newline
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// No newline found — hard split, no char skipped
|
|
154
|
+
chunks.push(remaining.slice(0, MAX_MSG_LEN));
|
|
155
|
+
remaining = remaining.slice(MAX_MSG_LEN);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (remaining)
|
|
159
|
+
chunks.push(remaining);
|
|
160
|
+
return chunks;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=telegram.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PermissionMode } from "./types.js";
|
|
2
|
+
export interface Config {
|
|
3
|
+
botToken: string;
|
|
4
|
+
allowedUserId: number;
|
|
5
|
+
permissionMode: PermissionMode;
|
|
6
|
+
dataDir: string;
|
|
7
|
+
workspacesPath: string;
|
|
8
|
+
logPath: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function loadConfig(): Config;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function loadConfig() {
|
|
5
|
+
const dataDir = process.env.CLEARCLAW_HOME ?? path.join(os.homedir(), ".clearclaw");
|
|
6
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
7
|
+
const botToken = requireEnv("TELEGRAM_BOT_TOKEN");
|
|
8
|
+
const allowedUserId = Number(requireEnv("ALLOWED_USER_ID"));
|
|
9
|
+
if (Number.isNaN(allowedUserId)) {
|
|
10
|
+
throw new Error("ALLOWED_USER_ID must be a number");
|
|
11
|
+
}
|
|
12
|
+
const permissionMode = (process.env.PERMISSION_MODE ?? "default");
|
|
13
|
+
const valid = [
|
|
14
|
+
"default",
|
|
15
|
+
"acceptEdits",
|
|
16
|
+
"bypassPermissions",
|
|
17
|
+
"plan",
|
|
18
|
+
"dontAsk",
|
|
19
|
+
];
|
|
20
|
+
if (!valid.includes(permissionMode)) {
|
|
21
|
+
throw new Error(`PERMISSION_MODE must be one of: ${valid.join(", ")}`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
botToken,
|
|
25
|
+
allowedUserId,
|
|
26
|
+
permissionMode,
|
|
27
|
+
dataDir,
|
|
28
|
+
workspacesPath: path.join(dataDir, "workspaces.json"),
|
|
29
|
+
logPath: path.join(dataDir, "clearclaw.log"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function requireEnv(name) {
|
|
33
|
+
const val = process.env[name];
|
|
34
|
+
if (!val)
|
|
35
|
+
throw new Error(`Missing required env var: ${name}`);
|
|
36
|
+
return val;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=config.js.map
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Workspace } from "./types.js";
|
|
2
|
+
export declare function initDb(): void;
|
|
3
|
+
export declare function getWorkspaceByChat(chatId: string): Workspace | undefined;
|
|
4
|
+
export declare function upsertWorkspace(ws: Workspace): void;
|
|
5
|
+
export declare function updateSessionId(name: string, sessionId: string): void;
|
|
6
|
+
export declare function clearSession(name: string): void;
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { dbPath } from "./config.js";
|
|
3
|
+
let db;
|
|
4
|
+
// Cached prepared statements (initialized in initDb)
|
|
5
|
+
let stmtGetByChat;
|
|
6
|
+
let stmtUpsert;
|
|
7
|
+
let stmtUpdateSession;
|
|
8
|
+
let stmtClearSession;
|
|
9
|
+
export function initDb() {
|
|
10
|
+
db = new Database(dbPath());
|
|
11
|
+
db.pragma("journal_mode = WAL");
|
|
12
|
+
db.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
14
|
+
name TEXT PRIMARY KEY,
|
|
15
|
+
cwd TEXT NOT NULL,
|
|
16
|
+
chat_id TEXT UNIQUE,
|
|
17
|
+
current_session_id TEXT
|
|
18
|
+
);
|
|
19
|
+
`);
|
|
20
|
+
stmtGetByChat = db.prepare("SELECT name, cwd, chat_id, current_session_id FROM workspaces WHERE chat_id = ?");
|
|
21
|
+
stmtUpsert = db.prepare(`INSERT INTO workspaces (name, cwd, chat_id, current_session_id)
|
|
22
|
+
VALUES (@name, @cwd, @chat_id, @current_session_id)
|
|
23
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
24
|
+
cwd = excluded.cwd,
|
|
25
|
+
chat_id = excluded.chat_id,
|
|
26
|
+
current_session_id = excluded.current_session_id`);
|
|
27
|
+
stmtUpdateSession = db.prepare("UPDATE workspaces SET current_session_id = ? WHERE name = ?");
|
|
28
|
+
stmtClearSession = db.prepare("UPDATE workspaces SET current_session_id = NULL WHERE name = ?");
|
|
29
|
+
}
|
|
30
|
+
export function getWorkspaceByChat(chatId) {
|
|
31
|
+
return stmtGetByChat.get(chatId);
|
|
32
|
+
}
|
|
33
|
+
export function upsertWorkspace(ws) {
|
|
34
|
+
stmtUpsert.run(ws);
|
|
35
|
+
}
|
|
36
|
+
export function updateSessionId(name, sessionId) {
|
|
37
|
+
stmtUpdateSession.run(sessionId, name);
|
|
38
|
+
}
|
|
39
|
+
export function clearSession(name) {
|
|
40
|
+
stmtClearSession.run(name);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import log from "../logger.js";
|
|
3
|
+
import { formatToolDescription } from "../format.js";
|
|
4
|
+
export class ClaudeCodeEngine {
|
|
5
|
+
name = "claude-code";
|
|
6
|
+
async *runTurn(opts) {
|
|
7
|
+
const { sessionId, cwd, prompt, permissionMode, onPermissionRequest, signal, } = opts;
|
|
8
|
+
const abortController = new AbortController();
|
|
9
|
+
// Wire external signal to our abort controller
|
|
10
|
+
if (signal) {
|
|
11
|
+
signal.addEventListener("abort", () => abortController.abort(), {
|
|
12
|
+
once: true,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const sessionOpts = sessionId
|
|
16
|
+
? { resume: sessionId }
|
|
17
|
+
: {};
|
|
18
|
+
const canUseTool = async (toolName, input, options) => {
|
|
19
|
+
const description = formatToolDescription(toolName, input);
|
|
20
|
+
const resp = await onPermissionRequest({
|
|
21
|
+
toolName,
|
|
22
|
+
input,
|
|
23
|
+
description,
|
|
24
|
+
reason: options.decisionReason,
|
|
25
|
+
});
|
|
26
|
+
if (resp.decision === "allow") {
|
|
27
|
+
return { behavior: "allow", updatedInput: input };
|
|
28
|
+
}
|
|
29
|
+
return { behavior: "deny", message: "User denied" };
|
|
30
|
+
};
|
|
31
|
+
const q = query({
|
|
32
|
+
prompt,
|
|
33
|
+
options: {
|
|
34
|
+
...sessionOpts,
|
|
35
|
+
cwd,
|
|
36
|
+
permissionMode,
|
|
37
|
+
allowDangerouslySkipPermissions: permissionMode === "bypassPermissions" ? true : undefined,
|
|
38
|
+
canUseTool,
|
|
39
|
+
abortController,
|
|
40
|
+
settingSources: ["user", "project", "local"],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
let resultSessionId;
|
|
44
|
+
const toolUseIdToName = new Map();
|
|
45
|
+
try {
|
|
46
|
+
for await (const msg of q) {
|
|
47
|
+
if (msg.type !== "assistant") {
|
|
48
|
+
const sub = msg.type === "result" ? ` (${msg.subtype})` : "";
|
|
49
|
+
log.info(`[sdk] ${msg.type}${sub}`);
|
|
50
|
+
}
|
|
51
|
+
// Extract text and tool_use from assistant messages
|
|
52
|
+
if (msg.type === "assistant") {
|
|
53
|
+
const { content } = msg.message;
|
|
54
|
+
for (const block of content) {
|
|
55
|
+
if (block.type === "text" && block.text) {
|
|
56
|
+
yield { type: "text", text: block.text };
|
|
57
|
+
}
|
|
58
|
+
if (block.type === "tool_use") {
|
|
59
|
+
toolUseIdToName.set(block.id, block.name);
|
|
60
|
+
yield {
|
|
61
|
+
type: "tool_use",
|
|
62
|
+
toolName: block.name,
|
|
63
|
+
input: block.input,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Extract tool results from user messages
|
|
69
|
+
if (msg.type === "user") {
|
|
70
|
+
const content = msg.message.content;
|
|
71
|
+
if (Array.isArray(content)) {
|
|
72
|
+
for (const block of content) {
|
|
73
|
+
const b = block;
|
|
74
|
+
if (b.type === "tool_result" &&
|
|
75
|
+
typeof b.tool_use_id === "string") {
|
|
76
|
+
const toolName = toolUseIdToName.get(b.tool_use_id) ?? "unknown";
|
|
77
|
+
const output = extractToolResultText(b.content);
|
|
78
|
+
if (output) {
|
|
79
|
+
yield { type: "tool_result", toolName, output };
|
|
80
|
+
}
|
|
81
|
+
toolUseIdToName.delete(b.tool_use_id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Relay rate limit events
|
|
87
|
+
if (msg.type === "rate_limit_event") {
|
|
88
|
+
const { rate_limit_info } = msg;
|
|
89
|
+
if (rate_limit_info.status !== "allowed") {
|
|
90
|
+
yield {
|
|
91
|
+
type: "rate_limit",
|
|
92
|
+
status: rate_limit_info.status,
|
|
93
|
+
resetsAt: rate_limit_info.resetsAt,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Relay local command output (e.g. /compact, /cost, /model)
|
|
98
|
+
if (msg.type === "system" && msg.subtype === "local_command_output") {
|
|
99
|
+
const cmdOut = msg;
|
|
100
|
+
if (cmdOut.content) {
|
|
101
|
+
yield { type: "text", text: cmdOut.content };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Capture result
|
|
105
|
+
if (msg.type === "result") {
|
|
106
|
+
const result = msg;
|
|
107
|
+
resultSessionId = result.session_id;
|
|
108
|
+
if (result.subtype !== "success" && result.errors.length) {
|
|
109
|
+
yield { type: "error", message: result.errors.join("\n") };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
yield {
|
|
116
|
+
type: "error",
|
|
117
|
+
message: err instanceof Error ? err.message : String(err),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (resultSessionId) {
|
|
121
|
+
yield { type: "done", sessionId: resultSessionId };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Pull plain text out of a tool_result content block. */
|
|
126
|
+
function extractToolResultText(content) {
|
|
127
|
+
if (typeof content === "string")
|
|
128
|
+
return content;
|
|
129
|
+
if (Array.isArray(content)) {
|
|
130
|
+
return content
|
|
131
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
132
|
+
.map((b) => b.text)
|
|
133
|
+
.join("\n");
|
|
134
|
+
}
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=claude-code.js.map
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff formatting utilities for tool_use display in Telegram.
|
|
3
|
+
*
|
|
4
|
+
* Uses the `diff` library for proper unified diffs with context lines.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Format a tool_use event for display. Returns a MarkdownV2 string.
|
|
8
|
+
* Edit/Write get rich formatting; all others get a one-line status.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatToolUse(toolName: string, input: Record<string, unknown>): string;
|
|
11
|
+
/**
|
|
12
|
+
* Format a tool_result event for display. Returns a MarkdownV2 code block
|
|
13
|
+
* with input + output, or null if suppressed/empty.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatToolResult(toolName: string, output: string): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Human-readable one-liner for a tool invocation (used in permission prompts).
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatToolDescription(toolName: string, input: Record<string, unknown>): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff formatting utilities for tool_use display in Telegram.
|
|
3
|
+
*
|
|
4
|
+
* Uses the `diff` library for proper unified diffs with context lines.
|
|
5
|
+
*/
|
|
6
|
+
import { createTwoFilesPatch } from "diff";
|
|
7
|
+
const MAX_LINES = 200;
|
|
8
|
+
/**
|
|
9
|
+
* Format a tool_use event for display. Returns a MarkdownV2 string.
|
|
10
|
+
* Edit/Write get rich formatting; all others get a one-line status.
|
|
11
|
+
*/
|
|
12
|
+
export function formatToolUse(toolName, input) {
|
|
13
|
+
if (toolName === "Edit") {
|
|
14
|
+
return formatEditDiff(input);
|
|
15
|
+
}
|
|
16
|
+
if (toolName === "Write") {
|
|
17
|
+
return formatWritePreview(input);
|
|
18
|
+
}
|
|
19
|
+
return escapeMarkdownV2(`🔧 ${formatToolStatus(toolName, input)}`);
|
|
20
|
+
}
|
|
21
|
+
function formatToolStatus(toolName, input) {
|
|
22
|
+
switch (toolName) {
|
|
23
|
+
case "Bash":
|
|
24
|
+
return `Bash: ${input.command ?? "(unknown)"}`;
|
|
25
|
+
case "Read":
|
|
26
|
+
return `Read: ${input.file_path ?? "(unknown)"}`;
|
|
27
|
+
case "Glob":
|
|
28
|
+
return `Glob: ${input.pattern ?? "(unknown)"}`;
|
|
29
|
+
case "Grep":
|
|
30
|
+
return `Grep: ${input.pattern ?? "(unknown)"}`;
|
|
31
|
+
case "WebFetch":
|
|
32
|
+
return `WebFetch: ${input.url ?? "(unknown)"}`;
|
|
33
|
+
case "WebSearch":
|
|
34
|
+
return `WebSearch: ${input.query ?? "(unknown)"}`;
|
|
35
|
+
case "Agent":
|
|
36
|
+
return `Agent: ${input.description ?? "(unknown)"}`;
|
|
37
|
+
default:
|
|
38
|
+
return toolName;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function formatEditDiff(input) {
|
|
42
|
+
const patch = createTwoFilesPatch(`a/${input.file_path}`, `b/${input.file_path}`, input.old_string, input.new_string, "", "", { context: 3 });
|
|
43
|
+
// Strip the "Index:" / "===" preamble, keep --- / +++ / @@ lines onward.
|
|
44
|
+
const lines = patch.split("\n");
|
|
45
|
+
const start = lines.findIndex((l) => l.startsWith("---"));
|
|
46
|
+
const diffLines = start >= 0 ? lines.slice(start) : lines;
|
|
47
|
+
const body = truncateLines(diffLines, MAX_LINES);
|
|
48
|
+
const header = escapeMarkdownV2(`\u270f\ufe0f Edit: ${input.file_path}`);
|
|
49
|
+
return `${header}\n\`\`\`diff\n${body}\n\`\`\``;
|
|
50
|
+
}
|
|
51
|
+
function formatWritePreview(input) {
|
|
52
|
+
const lines = input.content.split("\n");
|
|
53
|
+
const totalLines = lines.length;
|
|
54
|
+
const body = truncateLines(lines, MAX_LINES);
|
|
55
|
+
const header = escapeMarkdownV2(`\ud83d\udcc4 Write: ${input.file_path} (${totalLines} lines)`);
|
|
56
|
+
return `${header}\n\`\`\`\n${body}\n\`\`\``;
|
|
57
|
+
}
|
|
58
|
+
function truncateLines(lines, max) {
|
|
59
|
+
if (lines.length <= max) {
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
const shown = lines.slice(0, max).join("\n");
|
|
63
|
+
const remaining = lines.length - max;
|
|
64
|
+
return `${shown}\n... (${remaining} more lines)`;
|
|
65
|
+
}
|
|
66
|
+
// Tools whose output is noisy or redundant (tool_use already shows input).
|
|
67
|
+
// New/unknown tools show by default.
|
|
68
|
+
const SUPPRESSED_RESULTS = new Set([
|
|
69
|
+
"Read", "Edit", "Write", "Glob", "Agent",
|
|
70
|
+
"TodoWrite", "NotebookEdit", "EnterPlanMode", "ExitPlanMode",
|
|
71
|
+
]);
|
|
72
|
+
/**
|
|
73
|
+
* Format a tool_result event for display. Returns a MarkdownV2 code block
|
|
74
|
+
* with input + output, or null if suppressed/empty.
|
|
75
|
+
*/
|
|
76
|
+
export function formatToolResult(toolName, output) {
|
|
77
|
+
if (SUPPRESSED_RESULTS.has(toolName))
|
|
78
|
+
return null;
|
|
79
|
+
if (!output.trim())
|
|
80
|
+
return null;
|
|
81
|
+
const body = truncateLines(output.split("\n"), MAX_LINES);
|
|
82
|
+
return `\`\`\`\n${body}\n\`\`\``;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Escape special characters for MarkdownV2 (outside code blocks).
|
|
86
|
+
* Inside ``` blocks, no escaping is needed.
|
|
87
|
+
*/
|
|
88
|
+
function escapeMarkdownV2(text) {
|
|
89
|
+
return text.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Human-readable one-liner for a tool invocation (used in permission prompts).
|
|
93
|
+
*/
|
|
94
|
+
export function formatToolDescription(toolName, input) {
|
|
95
|
+
switch (toolName) {
|
|
96
|
+
case "Bash":
|
|
97
|
+
return `Bash: ${input.command ?? "(unknown command)"}`;
|
|
98
|
+
case "Edit":
|
|
99
|
+
case "Write":
|
|
100
|
+
case "Read":
|
|
101
|
+
return `${toolName}: ${input.file_path ?? "(unknown file)"}`;
|
|
102
|
+
default:
|
|
103
|
+
return `Allow ${toolName}?`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=format.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
import log, { initLogger } from "./logger.js";
|
|
4
|
+
import { WorkspaceStore } from "./workspace-store.js";
|
|
5
|
+
import { ClaudeCodeEngine } from "./engine/claude-code.js";
|
|
6
|
+
import { TelegramChannel } from "./channel/telegram.js";
|
|
7
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
8
|
+
async function main() {
|
|
9
|
+
const config = loadConfig();
|
|
10
|
+
initLogger(config.logPath);
|
|
11
|
+
const workspaceStore = new WorkspaceStore(config.workspacesPath);
|
|
12
|
+
const channel = new TelegramChannel(config.botToken, config.allowedUserId);
|
|
13
|
+
const engine = new ClaudeCodeEngine();
|
|
14
|
+
const orchestrator = new Orchestrator({
|
|
15
|
+
channel,
|
|
16
|
+
engine,
|
|
17
|
+
workspaceStore,
|
|
18
|
+
permissionMode: config.permissionMode,
|
|
19
|
+
});
|
|
20
|
+
await orchestrator.start();
|
|
21
|
+
}
|
|
22
|
+
main().catch((err) => {
|
|
23
|
+
log.fatal({ err }, "Fatal");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
let log = pino({ level: "silent" });
|
|
3
|
+
export function initLogger(logPath) {
|
|
4
|
+
log = pino({
|
|
5
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
6
|
+
transport: {
|
|
7
|
+
targets: [
|
|
8
|
+
{
|
|
9
|
+
target: "pino-pretty",
|
|
10
|
+
options: { destination: 1 },
|
|
11
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
target: "pino-roll",
|
|
15
|
+
options: {
|
|
16
|
+
file: logPath,
|
|
17
|
+
frequency: "daily",
|
|
18
|
+
size: "10m",
|
|
19
|
+
limit: { count: 5 },
|
|
20
|
+
},
|
|
21
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export { log as default };
|
|
28
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WorkspaceStore } from "./workspace-store.js";
|
|
2
|
+
import type { Channel, Engine, PermissionMode } from "./types.js";
|
|
3
|
+
export interface OrchestratorOpts {
|
|
4
|
+
channel: Channel;
|
|
5
|
+
engine: Engine;
|
|
6
|
+
workspaceStore: WorkspaceStore;
|
|
7
|
+
permissionMode: PermissionMode;
|
|
8
|
+
}
|
|
9
|
+
export declare class Orchestrator {
|
|
10
|
+
private channel;
|
|
11
|
+
private engine;
|
|
12
|
+
private workspaceStore;
|
|
13
|
+
private permissionMode;
|
|
14
|
+
private busyChats;
|
|
15
|
+
constructor(opts: OrchestratorOpts);
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
private handleMessage;
|
|
19
|
+
private routeEngineEvent;
|
|
20
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import log from "./logger.js";
|
|
2
|
+
import { formatToolUse, formatToolResult } from "./format.js";
|
|
3
|
+
export class Orchestrator {
|
|
4
|
+
channel;
|
|
5
|
+
engine;
|
|
6
|
+
workspaceStore;
|
|
7
|
+
permissionMode;
|
|
8
|
+
busyChats = new Set();
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.channel = opts.channel;
|
|
11
|
+
this.engine = opts.engine;
|
|
12
|
+
this.workspaceStore = opts.workspaceStore;
|
|
13
|
+
this.permissionMode = opts.permissionMode;
|
|
14
|
+
}
|
|
15
|
+
async start() {
|
|
16
|
+
this.channel.on("message", (msg) => {
|
|
17
|
+
this.handleMessage(msg).catch((err) => {
|
|
18
|
+
log.error({ err }, "[orchestrator] unhandled message error");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
await this.channel.connect();
|
|
22
|
+
log.info("ClearClaw ready.");
|
|
23
|
+
const shutdown = async () => {
|
|
24
|
+
await this.stop();
|
|
25
|
+
process.exit(0);
|
|
26
|
+
};
|
|
27
|
+
process.on("SIGINT", shutdown);
|
|
28
|
+
process.on("SIGTERM", shutdown);
|
|
29
|
+
}
|
|
30
|
+
async stop() {
|
|
31
|
+
await this.channel.disconnect();
|
|
32
|
+
}
|
|
33
|
+
async handleMessage(msg) {
|
|
34
|
+
try {
|
|
35
|
+
log.info(`[msg] ${msg.text.slice(0, 80)}`);
|
|
36
|
+
// /new — reset session
|
|
37
|
+
if (msg.text === "/new") {
|
|
38
|
+
const ws = this.workspaceStore.byChat(msg.chatId);
|
|
39
|
+
if (!ws) {
|
|
40
|
+
await this.channel.sendMessage(msg.chatId, "No workspace linked to this group.");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.workspaceStore.clearSession(ws.name);
|
|
44
|
+
log.info("[cmd] session cleared for workspace %s", ws.name);
|
|
45
|
+
await this.channel.sendMessage(msg.chatId, "Session cleared.");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Reject during active turn (per-workspace)
|
|
49
|
+
if (this.busyChats.has(msg.chatId)) {
|
|
50
|
+
log.info("[msg] rejected (busy)");
|
|
51
|
+
await this.channel.sendMessage(msg.chatId, "Still working on the previous message...");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.busyChats.add(msg.chatId);
|
|
55
|
+
const ws = this.workspaceStore.byChat(msg.chatId);
|
|
56
|
+
if (!ws) {
|
|
57
|
+
log.info("[msg] no workspace for chat %s", msg.chatId);
|
|
58
|
+
this.busyChats.delete(msg.chatId);
|
|
59
|
+
await this.channel.sendMessage(msg.chatId, "No workspace linked to this group.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
log.info(`[turn] start session=${ws.current_session_id ?? "new"} cwd=${ws.cwd}`);
|
|
63
|
+
await this.channel.setTyping(msg.chatId, true);
|
|
64
|
+
try {
|
|
65
|
+
for await (const event of this.engine.runTurn({
|
|
66
|
+
sessionId: ws.current_session_id,
|
|
67
|
+
cwd: ws.cwd,
|
|
68
|
+
prompt: msg.text,
|
|
69
|
+
permissionMode: this.permissionMode,
|
|
70
|
+
onPermissionRequest: async (req) => {
|
|
71
|
+
log.info(`[perm] ${req.toolName}`);
|
|
72
|
+
const resp = await this.channel.sendInteractive(msg.chatId, `Allow ${req.toolName}?`, [
|
|
73
|
+
{ label: "Allow", value: "allow" },
|
|
74
|
+
{ label: "Deny", value: "deny" },
|
|
75
|
+
]);
|
|
76
|
+
log.info(`[perm] ${req.toolName} → ${resp.value || "timeout"}`);
|
|
77
|
+
return {
|
|
78
|
+
decision: resp.value === "allow" ? "allow" : "deny",
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
})) {
|
|
82
|
+
await this.routeEngineEvent(msg.chatId, event, ws.name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
this.busyChats.delete(msg.chatId);
|
|
87
|
+
await this.channel.setTyping(msg.chatId, false);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
log.error({ err }, "[fatal]");
|
|
92
|
+
await this.channel.sendMessage(msg.chatId, `Internal error: ${err instanceof Error ? err.message : String(err)}`).catch(() => { });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async routeEngineEvent(chatId, event, workspaceName) {
|
|
96
|
+
switch (event.type) {
|
|
97
|
+
case "text":
|
|
98
|
+
await this.channel.sendMessage(chatId, event.text);
|
|
99
|
+
break;
|
|
100
|
+
case "tool_use": {
|
|
101
|
+
const formatted = formatToolUse(event.toolName, event.input);
|
|
102
|
+
await this.channel.sendMessage(chatId, formatted, {
|
|
103
|
+
parseMode: "MarkdownV2",
|
|
104
|
+
});
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "tool_result": {
|
|
108
|
+
const formatted = formatToolResult(event.toolName, event.output);
|
|
109
|
+
if (formatted) {
|
|
110
|
+
await this.channel.sendMessage(chatId, formatted, {
|
|
111
|
+
parseMode: "MarkdownV2",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "rate_limit": {
|
|
117
|
+
const resetMsg = event.resetsAt
|
|
118
|
+
? ` Resets at ${new Date(event.resetsAt).toLocaleTimeString()}.`
|
|
119
|
+
: "";
|
|
120
|
+
log.warn(`[turn] rate limit: ${event.status}${resetMsg}`);
|
|
121
|
+
await this.channel.sendMessage(chatId, `⚠️ Rate limited (${event.status}).${resetMsg}`);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "done":
|
|
125
|
+
log.info(`[turn] done session=${event.sessionId}`);
|
|
126
|
+
this.workspaceStore.setSession(workspaceName, event.sessionId);
|
|
127
|
+
break;
|
|
128
|
+
case "error":
|
|
129
|
+
log.error(`[turn] error: ${event.message}`);
|
|
130
|
+
await this.channel.sendMessage(chatId, `Error: ${event.message}`);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=orchestrator.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { PermissionMode } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
export interface ChannelEvents {
|
|
3
|
+
message: [msg: InboundMessage];
|
|
4
|
+
}
|
|
5
|
+
export interface Channel {
|
|
6
|
+
name: string;
|
|
7
|
+
connect(): Promise<void>;
|
|
8
|
+
disconnect(): Promise<void>;
|
|
9
|
+
ownsId(chatId: string): boolean;
|
|
10
|
+
sendMessage(chatId: string, text: string, opts?: SendMessageOpts): Promise<void>;
|
|
11
|
+
sendInteractive(chatId: string, text: string, buttons: Button[]): Promise<ButtonResponse>;
|
|
12
|
+
setTyping(chatId: string, isTyping: boolean): Promise<void>;
|
|
13
|
+
on<K extends keyof ChannelEvents>(event: K, listener: (...args: ChannelEvents[K]) => void): this;
|
|
14
|
+
off<K extends keyof ChannelEvents>(event: K, listener: (...args: ChannelEvents[K]) => void): this;
|
|
15
|
+
emit<K extends keyof ChannelEvents>(event: K, ...args: ChannelEvents[K]): boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface Button {
|
|
18
|
+
label: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ButtonResponse {
|
|
22
|
+
value: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SendMessageOpts {
|
|
25
|
+
parseMode?: "MarkdownV2" | "HTML";
|
|
26
|
+
}
|
|
27
|
+
export interface Engine {
|
|
28
|
+
name: string;
|
|
29
|
+
runTurn(opts: RunTurnOpts): AsyncIterable<EngineEvent>;
|
|
30
|
+
}
|
|
31
|
+
export interface RunTurnOpts {
|
|
32
|
+
sessionId: string | null;
|
|
33
|
+
cwd: string;
|
|
34
|
+
prompt: string;
|
|
35
|
+
permissionMode: PermissionMode;
|
|
36
|
+
onPermissionRequest: (req: PermissionRequest) => Promise<PermissionResponse>;
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
}
|
|
39
|
+
export interface PermissionRequest {
|
|
40
|
+
toolName: string;
|
|
41
|
+
input: Record<string, unknown>;
|
|
42
|
+
description: string;
|
|
43
|
+
reason?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface PermissionResponse {
|
|
46
|
+
decision: "allow" | "deny";
|
|
47
|
+
}
|
|
48
|
+
export type EngineEvent = {
|
|
49
|
+
type: "text";
|
|
50
|
+
text: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: "tool_use";
|
|
53
|
+
toolName: string;
|
|
54
|
+
input: Record<string, unknown>;
|
|
55
|
+
} | {
|
|
56
|
+
type: "tool_result";
|
|
57
|
+
toolName: string;
|
|
58
|
+
output: string;
|
|
59
|
+
} | {
|
|
60
|
+
type: "rate_limit";
|
|
61
|
+
status: string;
|
|
62
|
+
resetsAt?: number;
|
|
63
|
+
} | {
|
|
64
|
+
type: "done";
|
|
65
|
+
sessionId: string;
|
|
66
|
+
} | {
|
|
67
|
+
type: "error";
|
|
68
|
+
message: string;
|
|
69
|
+
};
|
|
70
|
+
export interface Workspace {
|
|
71
|
+
name: string;
|
|
72
|
+
cwd: string;
|
|
73
|
+
chat_id: string;
|
|
74
|
+
current_session_id: string | null;
|
|
75
|
+
}
|
|
76
|
+
export interface InboundMessage {
|
|
77
|
+
chatId: string;
|
|
78
|
+
text: string;
|
|
79
|
+
}
|
|
80
|
+
export type { PermissionMode };
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Workspace } from "./types.js";
|
|
2
|
+
export declare class WorkspaceStore {
|
|
3
|
+
private readonly path;
|
|
4
|
+
constructor(path: string);
|
|
5
|
+
private read;
|
|
6
|
+
private write;
|
|
7
|
+
byChat(chatId: string): Workspace | undefined;
|
|
8
|
+
upsert(ws: Workspace): void;
|
|
9
|
+
setSession(name: string, sessionId: string): void;
|
|
10
|
+
clearSession(name: string): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
export class WorkspaceStore {
|
|
3
|
+
path;
|
|
4
|
+
constructor(path) {
|
|
5
|
+
this.path = path;
|
|
6
|
+
if (!fs.existsSync(path)) {
|
|
7
|
+
fs.writeFileSync(path, "[]\n");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
read() {
|
|
11
|
+
return JSON.parse(fs.readFileSync(this.path, "utf-8"));
|
|
12
|
+
}
|
|
13
|
+
write(workspaces) {
|
|
14
|
+
fs.writeFileSync(this.path, JSON.stringify(workspaces, null, 2) + "\n");
|
|
15
|
+
}
|
|
16
|
+
byChat(chatId) {
|
|
17
|
+
return this.read().find((ws) => ws.chat_id === chatId);
|
|
18
|
+
}
|
|
19
|
+
upsert(ws) {
|
|
20
|
+
const all = this.read();
|
|
21
|
+
const idx = all.findIndex((w) => w.name === ws.name);
|
|
22
|
+
if (idx >= 0)
|
|
23
|
+
all[idx] = ws;
|
|
24
|
+
else
|
|
25
|
+
all.push(ws);
|
|
26
|
+
this.write(all);
|
|
27
|
+
}
|
|
28
|
+
setSession(name, sessionId) {
|
|
29
|
+
const all = this.read();
|
|
30
|
+
const ws = all.find((w) => w.name === name);
|
|
31
|
+
if (ws) {
|
|
32
|
+
ws.current_session_id = sessionId;
|
|
33
|
+
this.write(all);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
clearSession(name) {
|
|
37
|
+
const all = this.read();
|
|
38
|
+
const ws = all.find((w) => w.name === name);
|
|
39
|
+
if (ws) {
|
|
40
|
+
ws.current_session_id = null;
|
|
41
|
+
this.write(all);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=workspace-store.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clearclaw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"clearclaw": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/**/*.js",
|
|
10
|
+
"dist/**/*.d.ts"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "tsx src/index.ts",
|
|
14
|
+
"dev": "tsc --watch & nodemon --watch dist --exec 'node dist/index.js'",
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"check": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.50",
|
|
20
|
+
"diff": "^8.0.3",
|
|
21
|
+
"grammy": "^1.30.0",
|
|
22
|
+
"pino": "^10.3.1",
|
|
23
|
+
"pino-pretty": "^13.1.3",
|
|
24
|
+
"pino-roll": "^4.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/diff": "^7.0.2",
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"nodemon": "^3.1.14",
|
|
30
|
+
"tsx": "^4.19.0",
|
|
31
|
+
"typescript": "^5.7.0"
|
|
32
|
+
}
|
|
33
|
+
}
|