clearclaw 0.7.0 → 0.8.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/README.md +53 -7
- package/dist/channel/slack.d.ts +18 -1
- package/dist/channel/slack.js +227 -27
- package/dist/channel/telegram.d.ts +19 -0
- package/dist/channel/telegram.js +136 -30
- package/dist/config.d.ts +37 -1
- package/dist/config.js +118 -0
- package/dist/engine/acp.d.ts +9 -4
- package/dist/engine/acp.js +301 -29
- package/dist/engine/claude-code.d.ts +19 -2
- package/dist/engine/claude-code.js +121 -33
- package/dist/engine/mcp-http.d.ts +9 -0
- package/dist/engine/mcp-http.js +59 -0
- package/dist/engine/registry.d.ts +10 -7
- package/dist/engine/registry.js +99 -16
- package/dist/engine/session-transcript.d.ts +14 -0
- package/dist/engine/session-transcript.js +38 -0
- package/dist/format.d.ts +1 -1
- package/dist/format.js +42 -7
- package/dist/index.js +18 -23
- package/dist/orchestrator.d.ts +31 -4
- package/dist/orchestrator.js +822 -167
- package/dist/plan-usage.d.ts +6 -0
- package/dist/plan-usage.js +31 -0
- package/dist/prompt.d.ts +6 -2
- package/dist/prompt.js +62 -19
- package/dist/scheduler.d.ts +15 -0
- package/dist/scheduler.js +82 -0
- package/dist/types.d.ts +77 -5
- package/package.json +11 -4
- package/patches/README.md +13 -0
- package/patches/codex-acp-1.11.0.patch +38 -0
- package/prompts/SYSTEM.md +66 -16
- package/scripts/patch-codex-acp.mjs +32 -0
- package/prompts/ONBOARDING.md +0 -56
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EngineEvent, PlanUsageState } from "./types.js";
|
|
2
|
+
/** Latest normalized readings shared by workspaces on the same engine/account. */
|
|
3
|
+
export declare function recordPlanUsage(state: PlanUsageState, event: Extract<EngineEvent, {
|
|
4
|
+
type: "plan_usage";
|
|
5
|
+
}>): void;
|
|
6
|
+
export declare function formatPlanUsage(state?: PlanUsageState, maxLength?: number): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Latest normalized readings shared by workspaces on the same engine/account. */
|
|
2
|
+
export function recordPlanUsage(state, event) {
|
|
3
|
+
for (const window of event.windows)
|
|
4
|
+
state.windows.set(window.id, window);
|
|
5
|
+
if (event.isUsingOverage !== undefined)
|
|
6
|
+
state.isUsingOverage = event.isUsingOverage;
|
|
7
|
+
}
|
|
8
|
+
export function formatPlanUsage(state, maxLength = 4096) {
|
|
9
|
+
// A window with nothing to report is omitted: "5h ?" tells the reader less than
|
|
10
|
+
// its absence does, and the seeded windows start out unreported.
|
|
11
|
+
const parts = [];
|
|
12
|
+
for (const window of state?.windows.values() ?? []) {
|
|
13
|
+
if (window.usedPercent !== undefined)
|
|
14
|
+
parts.push(`${window.label} ${Math.round(window.usedPercent)}%`);
|
|
15
|
+
else if (window.limited)
|
|
16
|
+
parts.push(`${window.label} limited`);
|
|
17
|
+
}
|
|
18
|
+
if (state?.isUsingOverage)
|
|
19
|
+
parts.push("overage");
|
|
20
|
+
const kept = [];
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
const remaining = parts.length - kept.length - 1;
|
|
23
|
+
const candidate = [...kept, part].join(", ") + (remaining ? ` +${remaining} more` : "");
|
|
24
|
+
if (candidate.length > maxLength)
|
|
25
|
+
break;
|
|
26
|
+
kept.push(part);
|
|
27
|
+
}
|
|
28
|
+
const omitted = parts.length - kept.length;
|
|
29
|
+
return kept.join(", ") + (omitted ? `${kept.length ? " " : ""}+${omitted} more` : "");
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=plan-usage.js.map
|
package/dist/prompt.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Assemble the system prompt from framework and user instruction files.
|
|
3
3
|
* Framework content comes first, user content appended after.
|
|
4
|
-
*
|
|
4
|
+
* `prompt` is undefined if no content was found in either directory;
|
|
5
|
+
* `skipped` names files that existed but could not be read.
|
|
5
6
|
*/
|
|
6
|
-
export declare function assemblePrompt(frameworkDir: string, instructionsDir: string):
|
|
7
|
+
export declare function assemblePrompt(frameworkDir: string, instructionsDir: string): Promise<{
|
|
8
|
+
prompt: string | undefined;
|
|
9
|
+
skipped: string[];
|
|
10
|
+
}>;
|
package/dist/prompt.js
CHANGED
|
@@ -1,35 +1,78 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import log from "./logger.js";
|
|
5
|
+
// A cloud-synced instructions dir (OneDrive/iCloud "dataless" placeholder) can
|
|
6
|
+
// block a read indefinitely. Reading it synchronously froze the whole relay.
|
|
7
|
+
const READ_TIMEOUT_MS = 5000;
|
|
8
|
+
/** Read a file, giving up after READ_TIMEOUT_MS. Returns null on timeout or error. */
|
|
9
|
+
async function readFileOrSkip(file) {
|
|
10
|
+
// ponytail: Promise.race, not AbortSignal — an abort can't interrupt a read
|
|
11
|
+
// already blocked in libuv's threadpool, which is exactly what a
|
|
12
|
+
// cloud-placeholder file does.
|
|
13
|
+
let timer;
|
|
14
|
+
const timeout = new Promise((resolve) => {
|
|
15
|
+
timer = setTimeout(() => resolve(null), READ_TIMEOUT_MS);
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
const content = await Promise.race([fsp.readFile(file, "utf-8"), timeout]);
|
|
19
|
+
if (content === null) {
|
|
20
|
+
log.warn("[prompt] timed out reading %s after %dms — skipping", file, READ_TIMEOUT_MS);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return content.trim();
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
log.warn({ err }, "[prompt] could not read %s — skipping", file);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
3
33
|
/**
|
|
4
34
|
* Read all .md files from a directory, sorted alphabetically.
|
|
5
|
-
*
|
|
35
|
+
* Unreadable files are reported by name instead of silently dropped.
|
|
6
36
|
*/
|
|
7
|
-
function readMarkdownFiles(dir) {
|
|
37
|
+
async function readMarkdownFiles(dir) {
|
|
8
38
|
if (!fs.existsSync(dir))
|
|
9
|
-
return [];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
39
|
+
return { contents: [], skipped: [] };
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = await fsp.readdir(dir);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
// Cloud-synced dirs (Google Drive File Provider) can transiently refuse a listing with EPERM.
|
|
46
|
+
log.warn({ err }, "[prompt] could not list %s — skipping", dir);
|
|
47
|
+
return { contents: [], skipped: [dir] };
|
|
48
|
+
}
|
|
49
|
+
const names = entries.filter((f) => f.endsWith(".md")).sort();
|
|
50
|
+
const results = await Promise.all(names.map(async (f) => ({ name: f, text: await readFileOrSkip(path.join(dir, f)) })));
|
|
51
|
+
return {
|
|
52
|
+
contents: results.map((r) => r.text).filter((text) => Boolean(text)),
|
|
53
|
+
skipped: results.filter((r) => r.text === null).map((r) => r.name),
|
|
54
|
+
};
|
|
15
55
|
}
|
|
16
56
|
/**
|
|
17
57
|
* Assemble the system prompt from framework and user instruction files.
|
|
18
58
|
* Framework content comes first, user content appended after.
|
|
19
|
-
*
|
|
59
|
+
* `prompt` is undefined if no content was found in either directory;
|
|
60
|
+
* `skipped` names files that existed but could not be read.
|
|
20
61
|
*/
|
|
21
|
-
export function assemblePrompt(frameworkDir, instructionsDir) {
|
|
22
|
-
const framework = readMarkdownFiles(frameworkDir);
|
|
23
|
-
const user = readMarkdownFiles(instructionsDir);
|
|
24
|
-
|
|
25
|
-
|
|
62
|
+
export async function assemblePrompt(frameworkDir, instructionsDir) {
|
|
63
|
+
const framework = await readMarkdownFiles(frameworkDir);
|
|
64
|
+
const user = await readMarkdownFiles(instructionsDir);
|
|
65
|
+
const skipped = [...framework.skipped, ...user.skipped];
|
|
66
|
+
if (framework.contents.length === 0 && user.contents.length === 0) {
|
|
67
|
+
return { prompt: undefined, skipped };
|
|
68
|
+
}
|
|
26
69
|
const parts = [];
|
|
27
|
-
if (framework.length > 0) {
|
|
28
|
-
parts.push(framework.join("\n\n---\n\n"));
|
|
70
|
+
if (framework.contents.length > 0) {
|
|
71
|
+
parts.push(framework.contents.join("\n\n---\n\n"));
|
|
29
72
|
}
|
|
30
|
-
if (user.length > 0) {
|
|
31
|
-
parts.push(user.join("\n\n---\n\n"));
|
|
73
|
+
if (user.contents.length > 0) {
|
|
74
|
+
parts.push(user.contents.join("\n\n---\n\n"));
|
|
32
75
|
}
|
|
33
|
-
return parts.join("\n\n");
|
|
76
|
+
return { prompt: parts.join("\n\n"), skipped };
|
|
34
77
|
}
|
|
35
78
|
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Config, ScheduleEntry } from "./config.js";
|
|
2
|
+
import type { InboundMessage } from "./types.js";
|
|
3
|
+
export declare class Scheduler {
|
|
4
|
+
private config;
|
|
5
|
+
private inject;
|
|
6
|
+
private jobs;
|
|
7
|
+
constructor(config: Config, inject: (msg: Pick<InboundMessage, "origin" | "text">) => void);
|
|
8
|
+
start(): void;
|
|
9
|
+
stop(): void;
|
|
10
|
+
add(entry: ScheduleEntry): void;
|
|
11
|
+
remove(id: string): void;
|
|
12
|
+
toggle(id: string, enabled: boolean): void;
|
|
13
|
+
list(): ScheduleEntry[];
|
|
14
|
+
private createJob;
|
|
15
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Cron } from "croner";
|
|
2
|
+
import log from "./logger.js";
|
|
3
|
+
export class Scheduler {
|
|
4
|
+
config;
|
|
5
|
+
inject;
|
|
6
|
+
jobs = new Map();
|
|
7
|
+
constructor(config, inject) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.inject = inject;
|
|
10
|
+
}
|
|
11
|
+
start() {
|
|
12
|
+
const schedules = this.config.listSchedules();
|
|
13
|
+
let started = 0;
|
|
14
|
+
for (const entry of schedules) {
|
|
15
|
+
if (entry.enabled) {
|
|
16
|
+
this.createJob(entry);
|
|
17
|
+
started++;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (started > 0) {
|
|
21
|
+
log.info("[scheduler] started %d schedule(s)", started);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
stop() {
|
|
25
|
+
for (const [id, job] of this.jobs) {
|
|
26
|
+
job.stop();
|
|
27
|
+
log.info("[scheduler] stopped job %s", id);
|
|
28
|
+
}
|
|
29
|
+
this.jobs.clear();
|
|
30
|
+
}
|
|
31
|
+
add(entry) {
|
|
32
|
+
this.config.addSchedule(entry);
|
|
33
|
+
if (entry.enabled) {
|
|
34
|
+
this.createJob(entry);
|
|
35
|
+
}
|
|
36
|
+
log.info("[scheduler] added %s: %s → %s", entry.id, entry.cron, entry.prompt.slice(0, 60));
|
|
37
|
+
}
|
|
38
|
+
remove(id) {
|
|
39
|
+
const job = this.jobs.get(id);
|
|
40
|
+
if (job) {
|
|
41
|
+
job.stop();
|
|
42
|
+
this.jobs.delete(id);
|
|
43
|
+
}
|
|
44
|
+
this.config.removeSchedule(id);
|
|
45
|
+
log.info("[scheduler] removed %s", id);
|
|
46
|
+
}
|
|
47
|
+
toggle(id, enabled) {
|
|
48
|
+
this.config.updateSchedule(id, { enabled });
|
|
49
|
+
if (enabled) {
|
|
50
|
+
const entry = this.config.listSchedules().find((s) => s.id === id);
|
|
51
|
+
if (entry && !this.jobs.has(id)) {
|
|
52
|
+
this.createJob(entry);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const job = this.jobs.get(id);
|
|
57
|
+
if (job) {
|
|
58
|
+
job.stop();
|
|
59
|
+
this.jobs.delete(id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
log.info("[scheduler] %s → %s", id, enabled ? "enabled" : "disabled");
|
|
63
|
+
}
|
|
64
|
+
list() {
|
|
65
|
+
return this.config.listSchedules();
|
|
66
|
+
}
|
|
67
|
+
createJob(entry) {
|
|
68
|
+
const opts = {};
|
|
69
|
+
if (entry.timezone)
|
|
70
|
+
opts.timezone = entry.timezone;
|
|
71
|
+
const job = new Cron(entry.cron, opts, () => {
|
|
72
|
+
log.info("[scheduler] firing %s: %s", entry.id, entry.prompt.slice(0, 60));
|
|
73
|
+
this.inject({ origin: { kind: "scheduler", scheduleId: entry.id }, text: entry.prompt });
|
|
74
|
+
if (!job.nextRun()) {
|
|
75
|
+
this.remove(entry.id);
|
|
76
|
+
log.info("[scheduler] one-shot %s auto-deleted", entry.id);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
this.jobs.set(entry.id, job);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=scheduler.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -4,9 +4,13 @@ export interface ChannelEvents {
|
|
|
4
4
|
}
|
|
5
5
|
export interface Channel {
|
|
6
6
|
name: string;
|
|
7
|
+
/** Character budget for the chat status surface, e.g. Slack channel topic. */
|
|
8
|
+
statusMaxLength?: number;
|
|
7
9
|
connect(): Promise<void>;
|
|
8
10
|
disconnect(): Promise<void>;
|
|
9
11
|
ownsId(chatId: string): boolean;
|
|
12
|
+
/** This user's own top-level DM with the bot — home's only valid chat. */
|
|
13
|
+
isRootDM(chatId: string, userId: string): boolean;
|
|
10
14
|
sendMessage(chatId: string, text: string, opts?: MessageOpts): Promise<string[]>;
|
|
11
15
|
sendInteractive(chatId: string, text: string, buttons: Button[][]): Promise<ButtonResponse>;
|
|
12
16
|
editMessage(chatId: string, handle: string, text: string, opts?: MessageOpts): Promise<void>;
|
|
@@ -17,6 +21,15 @@ export interface Channel {
|
|
|
17
21
|
setTyping(chatId: string, isTyping: boolean): Promise<void>;
|
|
18
22
|
sendFile(chatId: string, buffer: Buffer, filename: string, opts?: SendFileOpts): Promise<void>;
|
|
19
23
|
reactToMessage(chatId: string, messageId: string, emoji: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Group an existing chat under its Project, creating the grouping if it does not exist yet.
|
|
26
|
+
* Returns a note when grouping did not happen, so the caller can tell the user.
|
|
27
|
+
*/
|
|
28
|
+
groupProjectChat?(projectName: string, chatId: string): Promise<string | undefined>;
|
|
29
|
+
/** Create a peer chat. Returns an opaque chat ID; grouping is a separate step. */
|
|
30
|
+
createProjectChat?(projectName: string, anchor: string, title: string): Promise<string>;
|
|
31
|
+
/** Close a workspace's chat and remove its Project grouping, regardless of origin. */
|
|
32
|
+
closeProjectChat?(chatId: string, projectName?: string): Promise<void>;
|
|
20
33
|
on<K extends keyof ChannelEvents>(event: K, listener: (...args: ChannelEvents[K]) => void): this;
|
|
21
34
|
off<K extends keyof ChannelEvents>(event: K, listener: (...args: ChannelEvents[K]) => void): this;
|
|
22
35
|
emit<K extends keyof ChannelEvents>(event: K, ...args: ChannelEvents[K]): boolean;
|
|
@@ -49,10 +62,24 @@ export interface SessionInfo {
|
|
|
49
62
|
lastModified: number;
|
|
50
63
|
gitBranch?: string;
|
|
51
64
|
}
|
|
65
|
+
/** Chronological conversation text, excluding tool payloads and internal reasoning. */
|
|
66
|
+
export interface SessionMessage {
|
|
67
|
+
role: "user" | "assistant";
|
|
68
|
+
text: string;
|
|
69
|
+
}
|
|
70
|
+
export interface SessionHistoryOpts {
|
|
71
|
+
sessionId: string;
|
|
72
|
+
cwd: string;
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
}
|
|
52
75
|
export interface Engine {
|
|
53
76
|
name: string;
|
|
77
|
+
/** Known account windows to show as unknown before the first observation. */
|
|
78
|
+
planUsageWindows?: readonly PlanUsageWindow[];
|
|
54
79
|
runTurn(opts: RunTurnOpts): AsyncIterable<EngineEvent>;
|
|
55
80
|
listSessions(cwd: string): Promise<SessionInfo[]>;
|
|
81
|
+
/** Read history without submitting a new prompt. Reject on unsupported/failed retrieval. */
|
|
82
|
+
getSessionMessages(opts: SessionHistoryOpts): Promise<SessionMessage[]>;
|
|
56
83
|
}
|
|
57
84
|
export interface RunTurnOpts {
|
|
58
85
|
sessionId: string | null;
|
|
@@ -64,6 +91,7 @@ export interface RunTurnOpts {
|
|
|
64
91
|
appendSystemPrompt?: string;
|
|
65
92
|
mcpServers?: Record<string, McpServerConfig>;
|
|
66
93
|
signal?: AbortSignal;
|
|
94
|
+
model?: string;
|
|
67
95
|
}
|
|
68
96
|
interface ToolCallBase {
|
|
69
97
|
toolName: string;
|
|
@@ -112,11 +140,24 @@ export interface PermissionResponse {
|
|
|
112
140
|
updatedInput?: Record<string, unknown>;
|
|
113
141
|
}
|
|
114
142
|
export interface TurnStats {
|
|
143
|
+
modelLabel?: string;
|
|
115
144
|
model: string | null;
|
|
116
145
|
contextUsed: number;
|
|
117
146
|
contextWindow: number;
|
|
118
147
|
toolCalls: Record<string, number>;
|
|
119
148
|
}
|
|
149
|
+
/** Account quota, distinct from a session's context/token usage. Reset times are Unix seconds. */
|
|
150
|
+
export interface PlanUsageWindow {
|
|
151
|
+
id: string;
|
|
152
|
+
label: string;
|
|
153
|
+
usedPercent?: number;
|
|
154
|
+
resetsAt?: number;
|
|
155
|
+
limited?: boolean;
|
|
156
|
+
}
|
|
157
|
+
export interface PlanUsageState {
|
|
158
|
+
windows: Map<string, PlanUsageWindow>;
|
|
159
|
+
isUsingOverage?: boolean;
|
|
160
|
+
}
|
|
120
161
|
export type EngineEvent = {
|
|
121
162
|
type: "text";
|
|
122
163
|
text: string;
|
|
@@ -131,9 +172,13 @@ export type EngineEvent = {
|
|
|
131
172
|
toolName: string;
|
|
132
173
|
output: string;
|
|
133
174
|
} | {
|
|
134
|
-
type: "
|
|
135
|
-
|
|
136
|
-
|
|
175
|
+
type: "plan_usage";
|
|
176
|
+
windows: PlanUsageWindow[];
|
|
177
|
+
isUsingOverage?: boolean;
|
|
178
|
+
} | {
|
|
179
|
+
type: "session";
|
|
180
|
+
sessionId: string;
|
|
181
|
+
model?: string;
|
|
137
182
|
} | {
|
|
138
183
|
type: "done";
|
|
139
184
|
sessionId: string;
|
|
@@ -142,13 +187,27 @@ export type EngineEvent = {
|
|
|
142
187
|
type: "error";
|
|
143
188
|
message: string;
|
|
144
189
|
};
|
|
190
|
+
export interface EngineHandoff {
|
|
191
|
+
engine: string;
|
|
192
|
+
sessionId: string;
|
|
193
|
+
cwd: string;
|
|
194
|
+
}
|
|
145
195
|
export interface Workspace {
|
|
146
196
|
name: string;
|
|
147
197
|
cwd: string;
|
|
148
|
-
chat_id: string;
|
|
198
|
+
chat_id: string | null;
|
|
149
199
|
current_session_id: string | null;
|
|
200
|
+
engine_handoff?: EngineHandoff;
|
|
150
201
|
behavior?: "assistant" | "relay";
|
|
151
202
|
engine?: string;
|
|
203
|
+
model?: string;
|
|
204
|
+
project?: string;
|
|
205
|
+
description?: string;
|
|
206
|
+
spawnedFrom?: string;
|
|
207
|
+
pending_brief?: {
|
|
208
|
+
fromWorkspace: string;
|
|
209
|
+
text: string;
|
|
210
|
+
};
|
|
152
211
|
}
|
|
153
212
|
export interface UserInfo {
|
|
154
213
|
id: string;
|
|
@@ -160,6 +219,8 @@ export interface Attachment {
|
|
|
160
219
|
buffer: Buffer;
|
|
161
220
|
mimeType: string;
|
|
162
221
|
filename?: string;
|
|
222
|
+
/** Absolute path where the orchestrator saved this file to disk. */
|
|
223
|
+
savedAs?: string;
|
|
163
224
|
}
|
|
164
225
|
/** Context from a replied-to / quoted message. */
|
|
165
226
|
export interface ReplyContext {
|
|
@@ -169,10 +230,21 @@ export interface ReplyContext {
|
|
|
169
230
|
mediaType?: string;
|
|
170
231
|
}
|
|
171
232
|
export type ChatType = "dm" | "group";
|
|
233
|
+
/** Where an inbound message came from. */
|
|
234
|
+
export type MessageOrigin = {
|
|
235
|
+
kind: "user";
|
|
236
|
+
user: UserInfo;
|
|
237
|
+
} | {
|
|
238
|
+
kind: "scheduler";
|
|
239
|
+
scheduleId: string;
|
|
240
|
+
} | {
|
|
241
|
+
kind: "peer";
|
|
242
|
+
workspaceName: string;
|
|
243
|
+
};
|
|
172
244
|
export interface InboundMessage {
|
|
173
245
|
chatId: string;
|
|
174
246
|
chatType: ChatType;
|
|
175
|
-
|
|
247
|
+
origin: MessageOrigin;
|
|
176
248
|
text: string;
|
|
177
249
|
messageId?: string;
|
|
178
250
|
replyTo?: ReplyContext;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clearclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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",
|
|
@@ -27,26 +27,32 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist/**/*.js",
|
|
29
29
|
"dist/**/*.d.ts",
|
|
30
|
-
"prompts/**/*.md"
|
|
30
|
+
"prompts/**/*.md",
|
|
31
|
+
"scripts/patch-codex-acp.mjs",
|
|
32
|
+
"patches/**"
|
|
31
33
|
],
|
|
32
34
|
"scripts": {
|
|
33
35
|
"start": "tsx src/index.ts daemon",
|
|
34
36
|
"dev": "tsx --watch src/index.ts daemon",
|
|
35
37
|
"dev:relay": "nodemon --watch dist --exec 'node dist/index.js daemon'",
|
|
36
38
|
"build": "tsc",
|
|
37
|
-
"check": "tsc --noEmit"
|
|
39
|
+
"check": "tsc --noEmit",
|
|
40
|
+
"test": "node --import tsx --test test/*.test.ts",
|
|
41
|
+
"postinstall": "node scripts/patch-codex-acp.mjs"
|
|
38
42
|
},
|
|
39
43
|
"dependencies": {
|
|
44
|
+
"@agentclientprotocol/codex-acp": "1.11.0",
|
|
40
45
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
41
46
|
"@anthropic-ai/claude-agent-sdk": "^0.2.89",
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
48
|
"@slack/bolt": "^4.6.0",
|
|
49
|
+
"croner": "^10.0.1",
|
|
43
50
|
"diff": "^8.0.3",
|
|
44
51
|
"grammy": "^1.30.0",
|
|
45
52
|
"mime": "^4.1.0",
|
|
46
53
|
"pino": "^10.3.1",
|
|
47
54
|
"pino-pretty": "^13.1.3",
|
|
48
55
|
"pino-roll": "^4.0.0",
|
|
49
|
-
"slackify-markdown": "^5.0.0",
|
|
50
56
|
"telegramify-markdown": "^1.3.3"
|
|
51
57
|
},
|
|
52
58
|
"devDependencies": {
|
|
@@ -54,6 +60,7 @@
|
|
|
54
60
|
"@types/diff": "^7.0.2",
|
|
55
61
|
"@types/node": "^22.0.0",
|
|
56
62
|
"nodemon": "^3.1.14",
|
|
63
|
+
"telegram": "^2.26.22",
|
|
57
64
|
"tsx": "^4.19.0",
|
|
58
65
|
"typescript": "^5.7.0"
|
|
59
66
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Codex ACP usage metadata backport
|
|
2
|
+
|
|
3
|
+
ClearClaw temporarily pins `@agentclientprotocol/codex-acp` to npm version `1.11.0` and backports the usage notification changes from [codex-acp PR #334](https://github.com/agentclientprotocol/codex-acp/pull/334), head [`b670a4e4914fbd5ffe2d008091e64921dc2684d4`](https://github.com/agentclientprotocol/codex-acp/commit/b670a4e4914fbd5ffe2d008091e64921dc2684d4). The upstream adapter and these adapted changes are Apache-2.0 licensed.
|
|
4
|
+
|
|
5
|
+
The npm release distributes a bundled `dist/index.js`, so the patch targets that bundle. It forwards account limit updates as ACP `usage_update` notifications and includes all cached quota buckets in `_meta["_codex/rateLimits"]` on context usage updates. Notifications retain the upstream requirement that context usage and a positive context window are known. This does not fetch quotas on demand.
|
|
6
|
+
|
|
7
|
+
Only the emission changes are backported. Version 1.11.0's existing `mergeRateLimitSnapshot` logic remains intact, including preservation of account-level fields on sparse updates.
|
|
8
|
+
|
|
9
|
+
`scripts/patch-codex-acp.mjs` runs after installation and uses ClearClaw's existing `diff` dependency. It resolves the installed adapter, checks its exact version and original SHA-256 hash, applies this patch, and verifies the resulting hash. An already patched bundle is accepted, making repeat installation idempotent. Unexpected source fails installation instead of silently dropping quota data.
|
|
10
|
+
|
|
11
|
+
The installer modifies the resolved dependency in place. In an installation where npm hoists and shares that exact adapter version, other consumers also receive these additive usage metadata notifications. Both the script and patch are included in ClearClaw's published package. Installations using `--ignore-scripts` must explicitly run `node scripts/patch-codex-acp.mjs` from the ClearClaw package directory before relying on Codex quota reporting.
|
|
12
|
+
|
|
13
|
+
Remove the installer and patch when an upstream release includes this metadata contract, then update the pinned dependency and verify usage notifications.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
--- a/dist/index.js
|
|
2
|
+
+++ b/dist/index.js
|
|
3
|
+
@@ -24471,7 +24471,7 @@
|
|
4
|
+
return this.createMcpToolProgressEvent(notification.params);
|
|
5
|
+
case "account/rateLimits/updated":
|
|
6
|
+
this.handleRateLimitsUpdated(notification.params);
|
|
7
|
+
- return null;
|
|
8
|
+
+ return this.createUsageUpdateFromState();
|
|
9
|
+
case "account/updated":
|
|
10
|
+
this.onAccountUpdated?.(notification.params);
|
|
11
|
+
return null;
|
|
12
|
+
@@ -25155,6 +25155,9 @@
|
|
13
|
+
}
|
|
14
|
+
createUsageUpdate(params) {
|
|
15
|
+
this.handleTokenUsageUpdated(params);
|
|
16
|
+
+ return this.createUsageUpdateFromState();
|
|
17
|
+
+ }
|
|
18
|
+
+ createUsageUpdateFromState() {
|
|
19
|
+
const used = this.sessionState.lastTokenUsage?.totalTokens;
|
|
20
|
+
const size = this.sessionState.modelContextWindow;
|
|
21
|
+
if (used == null || size == null || size <= 0) {
|
|
22
|
+
@@ -25163,7 +25166,15 @@
|
|
23
|
+
return {
|
|
24
|
+
sessionUpdate: "usage_update",
|
|
25
|
+
used,
|
|
26
|
+
- size
|
|
27
|
+
+ size,
|
|
28
|
+
+ ...this.sessionState.rateLimits ? {
|
|
29
|
+
+ _meta: {
|
|
30
|
+
+ "_codex/rateLimits": Array.from(this.sessionState.rateLimits.values(), (entry) => ({
|
|
31
|
+
+ ...entry.snapshot,
|
|
32
|
+
+ limitId: entry.limitId
|
|
33
|
+
+ }))
|
|
34
|
+
+ }
|
|
35
|
+
+ } : {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
handleRateLimitsUpdated(params) {
|
package/prompts/SYSTEM.md
CHANGED
|
@@ -6,16 +6,60 @@ Framework behavior for ClearClaw agents. This file ships with ClearClaw and upda
|
|
|
6
6
|
|
|
7
7
|
## Session Startup
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Before doing anything else — these files live in **ClearClaw home** (`$CLEARCLAW_HOME/workspace`, default `~/.clearclaw/workspace`). From a project cwd, Read them there, not from the project directory.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
3. If a topic comes up that memory mentions, check `knowledge/` before starting from scratch
|
|
11
|
+
1. Read `memory/MEMORY.md` — index with pointers
|
|
12
|
+
2. Read `memory/YYYY-MM-DD.md` (today + yesterday) for recent daily logs. If a pointer or topic needs another memory file, Read that `memory/*.md`.
|
|
13
|
+
3. `IDENTITY.md` and `USER.md` are the always-on main identity and user files. Additional role/identity and per-person markdown lives in `instructions/identity/` and `instructions/user/` — Read a file there only when the topic or the speaker needs that extra context.
|
|
14
|
+
4. If a topic comes up that memory mentions, check `knowledge/` before starting from scratch
|
|
16
15
|
|
|
17
16
|
Don't ask permission. Just do it.
|
|
18
17
|
|
|
18
|
+
## People
|
|
19
|
+
|
|
20
|
+
More than one person may talk to you. Each message names its speaker as `[user <id>] Name (@handle)`.
|
|
21
|
+
|
|
22
|
+
- Identify people by the stable `<id>`, not the display name. Names can collide or change.
|
|
23
|
+
- Don't assume the speaker is the person `USER.md` describes. Check the ID.
|
|
24
|
+
- Keep one person's preferences in `instructions/user/<id>.md` (use the ID with `:` replaced by `-`). Read it when that person speaks and the topic needs it.
|
|
25
|
+
- When a preference, request, or decision is someone's, say whose in memory and in replies.
|
|
26
|
+
- Changes to `IDENTITY.md`, `USER.md`, or `TOOLS.md` affect everyone you talk to. When someone asks for one, say so in the chat where they asked.
|
|
27
|
+
|
|
28
|
+
## Workspaces and Projects
|
|
29
|
+
|
|
30
|
+
Home is created automatically when the service starts and connects through approved DM pairing. There is no onboarding interview or task-completion step.
|
|
31
|
+
|
|
32
|
+
### Delegating work
|
|
33
|
+
|
|
34
|
+
- Use the project's main workspace to coordinate the overall effort, track dependencies, and integrate outcomes. Give substantial, distinct tasks to dedicated peer workspaces with their own chat and context.
|
|
35
|
+
- Keep small tasks, quick follow-ups, and continuations whose context is difficult to carry over in their current workspace. Reuse an existing peer when the work fits its scope.
|
|
36
|
+
- Use a subagent for bounded help that reports back within the current task. Use a peer for work that needs its own ongoing conversation with the user.
|
|
37
|
+
- Hand peers the goal, agreed decisions, constraints, and expected outcome. Leave unstated implementation choices open. Let peers work independently.
|
|
38
|
+
|
|
39
|
+
### Messaging other workspaces
|
|
40
|
+
|
|
41
|
+
- The user's attention decides where a conversation lives. When the user is talking with a peer directly, keep the discussion, decisions, and questions in that peer's chat; the main workspace does not need a play-by-play.
|
|
42
|
+
- When the user drives the work from the main workspace and has handed a peer a self-contained goal, the peer reports back to the main workspace.
|
|
43
|
+
- Message another workspace only when it changes what the recipient does: a question you are blocked on, a concrete dependency, or a logical checkpoint (ready for review, ready for handoff, done). Fold progress into that checkpoint message instead of sending updates along the way.
|
|
44
|
+
- Don't reply only to acknowledge. A message that needs no action needs no answer.
|
|
45
|
+
- Make each message self-contained and give it a one-line `summary`. Both chats show only the summary; the full message goes to the recipient.
|
|
46
|
+
|
|
47
|
+
### Choosing an engine and model
|
|
48
|
+
|
|
49
|
+
- Choose the engine and model together for the task. Consult the applicable user model-selection criteria, ratings, and task preferences in shared instructions such as `CLAUDE.md` or `AGENTS.md`; keep personal rankings there.
|
|
50
|
+
- Consider which available engine can run the chosen model and provide the tools and workflow the task needs. Select the pair deliberately rather than assuming the coordinator's engine is the best fit for every peer.
|
|
51
|
+
- Pass `engine` and `model` to `workspace_create` when choosing a specific pair. Omitted settings inherit from the destination project's main workspace, or from the source workspace for a new project. The model inherits only when the engine stays the same; switching engines without specifying a model uses the selected engine's default.
|
|
52
|
+
|
|
53
|
+
### Creating and managing workspaces
|
|
54
|
+
|
|
55
|
+
- Use `workspace_create` to hand a strand of work to a new peer. The peer joins your own project by default; pass `project` to put it elsewhere — an existing project name joins it, any new name starts that project with this peer as its main. Keep the description to one line; put the detailed handoff in `brief`.
|
|
56
|
+
- Propose a spin-out by calling `workspace_create` with a concrete handoff. Its built-in approval prompt is the confirmation step; a separate conversational confirmation is unnecessary.
|
|
57
|
+
- Prepare `cwd` yourself first — a git worktree, a clone, or a plain directory, whatever this host and repository expect — and keep owning it. ClearClaw only reads the path; it never creates or deletes one.
|
|
58
|
+
- Automatic creation makes the chat. Manual creation waits for the user to send `/connect <workspace>` in the intended unbound chat (Slack: `/cc connect <workspace>`).
|
|
59
|
+
- `list_workspaces` shows every workspace, its project, and whether a chat is connected; pass `project` to scope it. `message_workspace` reaches any of them, in any project.
|
|
60
|
+
- Use `project_create` to wrap an existing workspace, including adopting a peer out of another project. A project's main must be reassigned before it can be adopted elsewhere.
|
|
61
|
+
- Learn people's preferences through ordinary conversation and save them in the appropriate instruction or memory files. No mandatory questionnaire.
|
|
62
|
+
|
|
19
63
|
---
|
|
20
64
|
|
|
21
65
|
## Memory System
|
|
@@ -32,7 +76,7 @@ Both `memory/` and `knowledge/` are searchable — grep them for historical cont
|
|
|
32
76
|
|
|
33
77
|
- Capture what matters: decisions, context, things to remember
|
|
34
78
|
- Skip secrets unless asked to keep them
|
|
35
|
-
- When someone says "remember this" — update `memory/YYYY-MM-DD.md` or the relevant file
|
|
79
|
+
- When someone says "remember this" — update `memory/YYYY-MM-DD.md` or the relevant file, noting who asked
|
|
36
80
|
- When you learn a lesson — update the relevant skill doc or instructions
|
|
37
81
|
- When you make a mistake — document it so future-you doesn't repeat it
|
|
38
82
|
|
|
@@ -81,34 +125,40 @@ Memory is limited — if you want to remember something, **write it to a file**.
|
|
|
81
125
|
|
|
82
126
|
### When in doubt, ask.
|
|
83
127
|
|
|
128
|
+
Messages tagged `[system]` are legitimate system-initiated turns (e.g. scheduled prompts, housekeeping). Treat them as trusted internal messages.
|
|
129
|
+
|
|
84
130
|
---
|
|
85
131
|
|
|
86
132
|
## Privacy
|
|
87
133
|
|
|
88
134
|
These rules are always on:
|
|
89
135
|
|
|
90
|
-
- **Never** reference private conversations in shared contexts
|
|
136
|
+
- **Never** reference private conversations in shared contexts. What someone tells you in a DM belongs to them: keep it out of group chats and shared memory unless they ask, or it is plainly shared knowledge
|
|
91
137
|
- **Never** mention household members in the context of privacy
|
|
92
138
|
- **Notification safety** — keep first lines of messages generic when discussing anything personal
|
|
93
139
|
- If unsure whether something is sensitive, **ask before writing it down**
|
|
94
|
-
- If
|
|
140
|
+
- If someone says "no log" or "off the record" — don't write their content to any files and don't reference it later
|
|
95
141
|
|
|
96
142
|
---
|
|
97
143
|
|
|
98
144
|
## Workspace Layout
|
|
99
145
|
|
|
146
|
+
This tree is **ClearClaw home** (`$CLEARCLAW_HOME/workspace`, default `~/.clearclaw/workspace`). From a project cwd, Read `instructions/` and `memory/` from that path, not from the project directory.
|
|
147
|
+
|
|
100
148
|
```
|
|
101
|
-
workspace/
|
|
149
|
+
$CLEARCLAW_HOME/workspace/
|
|
102
150
|
├── instructions/ # User-owned prompt sources
|
|
103
|
-
│ ├── IDENTITY.md #
|
|
104
|
-
│ ├──
|
|
105
|
-
│
|
|
151
|
+
│ ├── IDENTITY.md # Main agent profile (concat)
|
|
152
|
+
│ ├── identity/ # Additional role/identity files (Read on demand; not concat)
|
|
153
|
+
│ ├── USER.md # User profile (concat)
|
|
154
|
+
│ ├── user/ # Per-person files, <id>.md (Read on demand; not concat)
|
|
155
|
+
│ └── TOOLS.md # Tools (concat)
|
|
106
156
|
├── knowledge/ # Structured notes (zk-indexed)
|
|
107
157
|
│ ├── notes/
|
|
108
158
|
│ └── saves/
|
|
109
159
|
├── memory/ # Session logs + curated memory
|
|
110
160
|
│ ├── YYYY-MM-DD.md # Daily session logs
|
|
111
|
-
│ └── MEMORY.md #
|
|
161
|
+
│ └── MEMORY.md # Index with pointers (Read on demand; never concat)
|
|
112
162
|
├── scratch/ # WIP, temp files
|
|
113
163
|
└── .claude/
|
|
114
164
|
└── skills/ # User skills (if any)
|
|
@@ -134,7 +184,7 @@ workspace/
|
|
|
134
184
|
|
|
135
185
|
## Persistence Routing
|
|
136
186
|
|
|
137
|
-
As you learn about the
|
|
187
|
+
As you learn about the people you work with, their preferences, and their environment, capture what you learn in the right place:
|
|
138
188
|
|
|
139
189
|
- **`instructions/`** — behavioral directives that shape every turn. "Always reply-all." "My timezone is Pacific." If it changes how you act, it's an instruction. Update these files as you learn.
|
|
140
190
|
- **`memory/MEMORY.md`** — curated context and decisions. "We chose zk over SQLite." "User is exploring a startup idea." Facts and decisions that might be relevant later.
|
|
@@ -161,4 +211,4 @@ When recommending or using tools:
|
|
|
161
211
|
|
|
162
212
|
This is a living document. Add conventions, style, and rules as you figure out what works. If you change this file, inform the user — it's your operating manual, and they should know.
|
|
163
213
|
|
|
164
|
-
*Evolving as you learn who you are and how you work together.*
|
|
214
|
+
*Evolving as you learn who you are and how you work together.*
|