clearclaw 0.5.0 β†’ 0.6.1

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,19 @@
1
+ /**
2
+ * Per-tool custom handlers for permission prompts and display.
3
+ *
4
+ * Permission handlers replace the default Allow/Deny prompt with tool-specific
5
+ * UI (e.g. option buttons for AskUserQuestion, approve/reject for ExitPlanMode).
6
+ *
7
+ * Display handlers suppress the default rolling tool status line for tools that
8
+ * have their own display logic (or should be invisible to the user).
9
+ */
10
+ import type { Button, ButtonResponse, PermissionResponse } from "./types.js";
11
+ export interface ToolPromptResult {
12
+ text: string;
13
+ buttons: Button[][];
14
+ mapResponse: (resp: ButtonResponse) => PermissionResponse;
15
+ }
16
+ export type ToolPromptHandler = (input: Record<string, unknown>, description: string) => ToolPromptResult | null;
17
+ export declare const permissionHandlers: Map<string, ToolPromptHandler>;
18
+ /** Tools that suppress the default rolling status line in routeEngineEvent. */
19
+ export declare const displayHandledTools: Set<string>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Per-tool custom handlers for permission prompts and display.
3
+ *
4
+ * Permission handlers replace the default Allow/Deny prompt with tool-specific
5
+ * UI (e.g. option buttons for AskUserQuestion, approve/reject for ExitPlanMode).
6
+ *
7
+ * Display handlers suppress the default rolling tool status line for tools that
8
+ * have their own display logic (or should be invisible to the user).
9
+ */
10
+ import { formatAskUserQuestion, formatExitPlanMode, truncateButtonLabel, } from "./format.js";
11
+ // --- Permission Handlers ---
12
+ function handleAskUserQuestion(input) {
13
+ const { text, options } = formatAskUserQuestion(input);
14
+ const buttons = options.map((opt, i) => [
15
+ { label: truncateButtonLabel(i, opt.label), value: opt.label },
16
+ ]);
17
+ buttons.push([{ label: "Other…", value: "__other__", requestText: true }]);
18
+ return {
19
+ text,
20
+ buttons,
21
+ mapResponse(resp) {
22
+ const answer = resp.value === "__other__"
23
+ ? (resp.text ?? "")
24
+ : resp.value;
25
+ const questions = input.questions;
26
+ const questionText = questions[0].question;
27
+ return {
28
+ decision: "allow",
29
+ updatedInput: {
30
+ ...input,
31
+ answers: { [questionText]: answer },
32
+ },
33
+ };
34
+ },
35
+ };
36
+ }
37
+ function handleExitPlanMode(input, description) {
38
+ const text = formatExitPlanMode(input, description);
39
+ return {
40
+ text,
41
+ buttons: [
42
+ [
43
+ { label: "πŸ‘ Approve", value: "approve" },
44
+ { label: "πŸ‘Ž Reject", value: "reject" },
45
+ ],
46
+ [
47
+ { label: "πŸ“ Reject + Note", value: "reject", requestText: true },
48
+ ],
49
+ ],
50
+ mapResponse(resp) {
51
+ if (resp.value === "approve") {
52
+ return { decision: "allow", updatedInput: input };
53
+ }
54
+ return {
55
+ decision: "deny",
56
+ message: resp.text ?? "Plan rejected",
57
+ };
58
+ },
59
+ };
60
+ }
61
+ // --- Exported maps ---
62
+ export const permissionHandlers = new Map([
63
+ ["AskUserQuestion", handleAskUserQuestion],
64
+ ["ExitPlanMode", handleExitPlanMode],
65
+ ]);
66
+ /** Tools that suppress the default rolling status line in routeEngineEvent. */
67
+ export const displayHandledTools = new Set([
68
+ "AskUserQuestion",
69
+ "ExitPlanMode",
70
+ "EnterPlanMode",
71
+ "TodoWrite",
72
+ ]);
73
+ //# sourceMappingURL=tool-handlers.js.map
package/dist/types.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface Channel {
16
16
  updateStatus(chatId: string, text: string): Promise<void>;
17
17
  setTyping(chatId: string, isTyping: boolean): Promise<void>;
18
18
  sendFile(chatId: string, buffer: Buffer, filename: string, opts?: SendFileOpts): Promise<void>;
19
+ reactToMessage(chatId: string, messageId: string, emoji: string): Promise<void>;
19
20
  on<K extends keyof ChannelEvents>(event: K, listener: (...args: ChannelEvents[K]) => void): this;
20
21
  off<K extends keyof ChannelEvents>(event: K, listener: (...args: ChannelEvents[K]) => void): this;
21
22
  emit<K extends keyof ChannelEvents>(event: K, ...args: ChannelEvents[K]): boolean;
@@ -30,9 +31,10 @@ export interface ButtonResponse {
30
31
  text?: string;
31
32
  }
32
33
  export interface SendMessageOpts {
33
- parseMode?: "MarkdownV2" | "HTML";
34
34
  /** When false, skip consuming the typing placeholder (e.g. for tool status messages). */
35
35
  consumeTyping?: boolean;
36
+ /** Reply/thread to a specific platform message ID (Telegram reply, Slack thread). */
37
+ replyToMessageId?: string;
36
38
  }
37
39
  export interface SendFileOpts {
38
40
  caption?: string;
@@ -70,6 +72,7 @@ export interface PermissionRequest {
70
72
  export interface PermissionResponse {
71
73
  decision: "allow" | "deny";
72
74
  message?: string;
75
+ updatedInput?: Record<string, unknown>;
73
76
  }
74
77
  export interface TurnStats {
75
78
  model: string;
@@ -106,6 +109,7 @@ export interface Workspace {
106
109
  cwd: string;
107
110
  chat_id: string;
108
111
  current_session_id: string | null;
112
+ behavior?: "assistant" | "relay";
109
113
  }
110
114
  export interface UserInfo {
111
115
  id: string;
@@ -118,10 +122,21 @@ export interface Attachment {
118
122
  mimeType: string;
119
123
  filename?: string;
120
124
  }
125
+ /** Context from a replied-to / quoted message. */
126
+ export interface ReplyContext {
127
+ messageId: string;
128
+ senderName?: string;
129
+ text?: string;
130
+ mediaType?: string;
131
+ }
132
+ export type ChatType = "dm" | "group";
121
133
  export interface InboundMessage {
122
134
  chatId: string;
135
+ chatType: ChatType;
123
136
  user: UserInfo;
124
137
  text: string;
138
+ messageId?: string;
139
+ replyTo?: ReplyContext;
125
140
  attachments?: Attachment[];
126
141
  }
127
142
  export type { PermissionMode };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clearclaw",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Your coding agent, from your phone β€” personal assistant + project workspaces on your machine, built on the CLI's permissions, config, and memory instead of replacing them.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,21 +29,23 @@
29
29
  "dist/**/*.d.ts"
30
30
  ],
31
31
  "scripts": {
32
- "start": "tsx src/index.ts",
33
- "dev": "tsx --watch src/index.ts",
34
- "dev:relay": "nodemon --watch dist --exec 'node dist/index.js'",
32
+ "start": "tsx src/index.ts daemon",
33
+ "dev": "tsx --watch src/index.ts daemon",
34
+ "dev:relay": "nodemon --watch dist --exec 'node dist/index.js daemon'",
35
35
  "build": "tsc",
36
36
  "check": "tsc --noEmit"
37
37
  },
38
38
  "dependencies": {
39
- "@anthropic-ai/claude-agent-sdk": "^0.2.50",
39
+ "@anthropic-ai/claude-agent-sdk": "^0.2.89",
40
40
  "@slack/bolt": "^4.6.0",
41
41
  "diff": "^8.0.3",
42
42
  "grammy": "^1.30.0",
43
43
  "mime": "^4.1.0",
44
44
  "pino": "^10.3.1",
45
45
  "pino-pretty": "^13.1.3",
46
- "pino-roll": "^4.0.0"
46
+ "pino-roll": "^4.0.0",
47
+ "slackify-markdown": "^5.0.0",
48
+ "telegramify-markdown": "^1.3.3"
47
49
  },
48
50
  "devDependencies": {
49
51
  "@anthropic-ai/sdk": "^0.78.0",
package/dist/db.d.ts DELETED
@@ -1,6 +0,0 @@
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 DELETED
@@ -1,42 +0,0 @@
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
@@ -1,11 +0,0 @@
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
- }
@@ -1,45 +0,0 @@
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