clauderipple 0.9.5 → 0.11.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/CHANGELOG.md +118 -0
- package/README.es.md +45 -0
- package/README.ja.md +17 -0
- package/README.ko.md +31 -0
- package/README.md +70 -0
- package/README.pt-BR.md +45 -0
- package/README.zh-CN.md +17 -0
- package/dist/app/assets/icon.icns +0 -0
- package/dist/app/assets/icon.ico +0 -0
- package/dist/app/assets/icon.png +0 -0
- package/dist/app/assets/trayDirectTemplate.png +0 -0
- package/dist/app/assets/trayDirectTemplate@2x.png +0 -0
- package/dist/app/assets/trayDownTemplate.png +0 -0
- package/dist/app/assets/trayDownTemplate@2x.png +0 -0
- package/dist/app/assets/trayTemplate.png +0 -0
- package/dist/app/assets/trayTemplate@2x.png +0 -0
- package/dist/app/assets/trayWarnTemplate.png +0 -0
- package/dist/app/assets/trayWarnTemplate@2x.png +0 -0
- package/dist/app/assets/trayWin.png +0 -0
- package/dist/app/assets/trayWin@2x.png +0 -0
- package/dist/app/assets/trayWinDirect.png +0 -0
- package/dist/app/assets/trayWinDirect@2x.png +0 -0
- package/dist/app/assets/trayWinDown.png +0 -0
- package/dist/app/assets/trayWinDown@2x.png +0 -0
- package/dist/app/assets/trayWinWarn.png +0 -0
- package/dist/app/assets/trayWinWarn@2x.png +0 -0
- package/dist/app/dist/claude-desktop.js +67 -0
- package/dist/app/dist/main.js +423 -110
- package/dist/app/dist/preload.js +21 -0
- package/dist/app/dist/protocol.js +121 -0
- package/dist/app/dist/status-model.js +87 -0
- package/dist/app/dist/window.js +133 -0
- package/dist/app/package.json +1 -1
- package/dist/cli/src/awake-setup.js +314 -0
- package/dist/cli/src/bundle.js +9 -5
- package/dist/cli/src/direct.js +159 -0
- package/dist/cli/src/doctor.js +205 -0
- package/dist/cli/src/hooks/agent-title.js +7 -4
- package/dist/cli/src/index.js +227 -31
- package/dist/cli/src/launchd.js +29 -20
- package/dist/cli/src/launcher.js +215 -0
- package/dist/cli/src/power.js +182 -0
- package/dist/cli/src/registration.js +157 -0
- package/dist/cli/src/remote.js +263 -0
- package/dist/cli/src/schtasks.js +50 -40
- package/dist/cli/src/settings.js +26 -1
- package/dist/cli/src/tray.js +5 -2
- package/dist/mod/hooks/i18n.ts +473 -227
- package/dist/mod/hooks/judge.ts +35 -17
- package/dist/mod/hooks/modes.ts +56 -1
- package/dist/mod/hooks/register.tsx +237 -32
- package/dist/mod/hooks/session.ts +102 -1
- package/dist/mod/hooks/settings.js +7 -3
- package/dist/mod/hooks/settings.ts +9 -4
- package/dist/mod/hooks/stats.ts +25 -1
- package/dist/mod/hooks/ui/agents.tsx +903 -79
- package/dist/mod/hooks/ui/alerts.tsx +20 -17
- package/dist/mod/hooks/ui/art.ts +33 -7
- package/dist/mod/hooks/ui/band.tsx +262 -81
- package/dist/mod/hooks/ui/flat.ts +265 -0
- package/dist/mod/hooks/ui/kit.tsx +70 -55
- package/dist/mod/hooks/ui/look.ts +129 -0
- package/dist/mod/hooks/ui/settings.tsx +71 -46
- package/dist/mod/hooks/ui/setup.tsx +15 -13
- package/dist/mod/hooks/ui/theme.ts +3 -0
- package/dist/mod/hooks/ui/tools.ts +124 -0
- package/dist/mod/hooks/ui/view.ts +120 -4
- package/dist/mod/hooks/workers.ts +33 -6
- package/dist/mod/tests/register.test.tsx +155 -11
- package/dist/mod/tests/ui.test.tsx +408 -27
- package/dist/mod/types/index.d.ts +66 -2
- package/dist/remote/package.json +28 -0
- package/dist/remote/src/away.js +85 -0
- package/dist/remote/src/bridge-client.js +336 -0
- package/dist/remote/src/composer-menu.js +11 -0
- package/dist/remote/src/config.js +102 -0
- package/dist/remote/src/desktop.js +1018 -0
- package/dist/remote/src/e2ee.js +126 -0
- package/dist/remote/src/e2ee.ts +126 -0
- package/dist/remote/src/eventlog.js +298 -0
- package/dist/remote/src/expo-push.js +127 -0
- package/dist/remote/src/folders.js +79 -0
- package/dist/remote/src/fsutil.js +39 -0
- package/dist/remote/src/handoff.js +38 -0
- package/dist/remote/src/host-service.js +168 -0
- package/dist/remote/src/images.js +215 -0
- package/dist/remote/src/local-control.js +138 -0
- package/dist/remote/src/main.js +264 -0
- package/dist/remote/src/permissions.js +124 -0
- package/dist/remote/src/power.js +76 -0
- package/dist/remote/src/processes.js +24 -0
- package/dist/remote/src/protocol.js +32 -0
- package/dist/remote/src/protocol.ts +284 -0
- package/dist/remote/src/push-content.js +32 -0
- package/dist/remote/src/push.js +134 -0
- package/dist/remote/src/receipts.js +111 -0
- package/dist/remote/src/relay.js +438 -0
- package/dist/remote/src/router.js +208 -0
- package/dist/remote/src/sdk-launcher.js +18 -0
- package/dist/remote/src/server.js +706 -0
- package/dist/remote/src/service.js +157 -0
- package/dist/remote/src/sessions.js +1800 -0
- package/dist/remote/src/sidebar-storage.js +211 -0
- package/dist/remote/src/sidebar.js +173 -0
- package/dist/remote/src/tool-details.js +205 -0
- package/dist/remote/src/tool-details.ts +153 -0
- package/dist/remote/src/worktree.js +99 -0
- package/dist/remote/web/app.ts +746 -0
- package/dist/remote/web/conn.ts +193 -0
- package/dist/remote/web/demo.ts +134 -0
- package/dist/remote/web/dom.ts +124 -0
- package/dist/remote/web/esc.ts +4 -0
- package/dist/remote/web/i18n.ts +374 -0
- package/dist/remote/web/icon.svg +1 -0
- package/dist/remote/web/index.html +24 -0
- package/dist/remote/web/manifest.webmanifest +10 -0
- package/dist/remote/web/mascot.ts +60 -0
- package/dist/remote/web/md.ts +78 -0
- package/dist/remote/web/notify.ts +55 -0
- package/dist/remote/web/pushstate.ts +27 -0
- package/dist/remote/web/secure.ts +109 -0
- package/dist/remote/web/sidebar.ts +106 -0
- package/dist/remote/web/style.css +311 -0
- package/dist/remote/web/sw.ts +48 -0
- package/dist/remote/web/timeline.ts +224 -0
- package/dist/remote/web/views.ts +323 -0
- package/dist/router/src/admin.js +331 -5
- package/dist/router/src/awake-win.js +590 -0
- package/dist/router/src/awake.js +432 -0
- package/dist/router/src/config.js +94 -0
- package/dist/router/src/health.js +86 -3
- package/dist/router/src/hold.js +107 -0
- package/dist/router/src/index.js +108 -6
- package/dist/router/src/ingress/server.js +26 -4
- package/dist/router/src/ingress/translate.js +6 -2
- package/dist/router/src/phone.js +82 -0
- package/dist/router/src/power.js +107 -0
- package/dist/router/src/providers/anthropic-usage.js +33 -1
- package/dist/router/src/providers/google/antigravity.js +73 -10
- package/dist/router/src/providers/google/index.js +191 -14
- package/dist/router/src/providers/google/prompt-refusal.js +29 -0
- package/dist/router/src/providers/google/thoughtSignatures.js +13 -0
- package/dist/router/src/providers/google/transport.js +1 -1
- package/dist/router/src/providers/google/usage.js +302 -0
- package/dist/router/src/providers/grok/index.js +18 -0
- package/dist/router/src/providers/grok/usage.js +77 -0
- package/dist/router/src/providers/openai/translate.js +13 -2
- package/dist/router/src/providers/opencode-usage.js +116 -0
- package/dist/router/src/providers/retry.js +4 -1
- package/dist/router/src/proxy.js +371 -261
- package/dist/router/src/remote-host.js +320 -0
- package/dist/router/src/remote-tunnel.js +320 -0
- package/dist/router/src/requestlog.js +48 -39
- package/dist/router/src/tether-win.js +211 -0
- package/dist/router/src/tether.js +712 -0
- package/dist/router/src/usage-history.js +227 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +1083 -59
- package/dist/ui/i18n.js +670 -69
- package/dist/ui/index.html +33 -6
- package/dist/ui/style.css +177 -0
- package/docs/ARCHITECTURE.md +402 -14
- package/package.json +8 -2
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Read Chromium Local Storage without opening LevelDB (opening it takes a lock and writes).
|
|
2
|
+
// Only the requested key escapes this boundary. SST blocks use LevelDB's raw Snappy format.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
function varint(b, cursor) {
|
|
6
|
+
let n = 0, scale = 1;
|
|
7
|
+
for (let i = 0; i < 8; i++) {
|
|
8
|
+
if (cursor.at >= b.length)
|
|
9
|
+
throw new Error("Truncated LevelDB varint");
|
|
10
|
+
const v = b[cursor.at++];
|
|
11
|
+
n += (v & 127) * scale;
|
|
12
|
+
if (!(v & 128))
|
|
13
|
+
return n;
|
|
14
|
+
scale *= 128;
|
|
15
|
+
}
|
|
16
|
+
throw new Error("Invalid LevelDB varint");
|
|
17
|
+
}
|
|
18
|
+
function slice(b, c) {
|
|
19
|
+
const size = varint(b, c), end = c.at + size;
|
|
20
|
+
if (end > b.length)
|
|
21
|
+
throw new Error("Truncated LevelDB value");
|
|
22
|
+
const value = b.subarray(c.at, end);
|
|
23
|
+
c.at = end;
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function snappy(b) {
|
|
27
|
+
const c = { at: 0 }, size = varint(b, c);
|
|
28
|
+
if (size > 16 * 1024 * 1024)
|
|
29
|
+
throw new Error("Oversized LevelDB block");
|
|
30
|
+
const out = Buffer.alloc(size);
|
|
31
|
+
let at = 0;
|
|
32
|
+
while (c.at < b.length) {
|
|
33
|
+
const tag = b[c.at++], kind = tag & 3;
|
|
34
|
+
let length = (tag >>> 2) + 1, offset = 0;
|
|
35
|
+
if (kind === 0) {
|
|
36
|
+
if (length >= 61) {
|
|
37
|
+
const bytes = length - 60;
|
|
38
|
+
if (c.at + bytes > b.length)
|
|
39
|
+
throw new Error("Truncated Snappy literal");
|
|
40
|
+
length = b.readUIntLE(c.at, bytes) + 1;
|
|
41
|
+
c.at += bytes;
|
|
42
|
+
}
|
|
43
|
+
if (c.at + length > b.length || at + length > size)
|
|
44
|
+
throw new Error("Invalid Snappy literal");
|
|
45
|
+
b.copy(out, at, c.at, c.at + length);
|
|
46
|
+
c.at += length;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const bytes = kind === 1 ? 1 : kind === 2 ? 2 : 4;
|
|
50
|
+
if (c.at + bytes > b.length)
|
|
51
|
+
throw new Error("Truncated Snappy copy");
|
|
52
|
+
offset = b.readUIntLE(c.at, bytes);
|
|
53
|
+
c.at += bytes;
|
|
54
|
+
if (kind === 1) {
|
|
55
|
+
offset += (tag & 224) << 3;
|
|
56
|
+
length = 4 + ((tag >>> 2) & 7);
|
|
57
|
+
}
|
|
58
|
+
if (!offset || offset > at || at + length > size)
|
|
59
|
+
throw new Error("Invalid Snappy copy");
|
|
60
|
+
for (let i = 0; i < length; i++)
|
|
61
|
+
out[at + i] = out[at + i - offset];
|
|
62
|
+
}
|
|
63
|
+
at += length;
|
|
64
|
+
}
|
|
65
|
+
if (at !== size)
|
|
66
|
+
throw new Error("Incomplete Snappy block");
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function entries(b, visit) {
|
|
70
|
+
if (b.length < 4)
|
|
71
|
+
throw new Error("Invalid LevelDB block");
|
|
72
|
+
const end = b.length - 4 - b.readUInt32LE(b.length - 4) * 4;
|
|
73
|
+
let key = Buffer.alloc(0);
|
|
74
|
+
const c = { at: 0 };
|
|
75
|
+
while (c.at < end) {
|
|
76
|
+
const shared = varint(b, c), suffix = varint(b, c), size = varint(b, c);
|
|
77
|
+
if (shared > key.length || c.at + suffix + size > end)
|
|
78
|
+
throw new Error("Invalid LevelDB entry");
|
|
79
|
+
key = Buffer.concat([key.subarray(0, shared), b.subarray(c.at, c.at + suffix)]);
|
|
80
|
+
c.at += suffix;
|
|
81
|
+
const value = b.subarray(c.at, c.at + size);
|
|
82
|
+
c.at += size;
|
|
83
|
+
visit(key, value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function table(b, visit) {
|
|
87
|
+
if (b.length < 48 || b.readBigUInt64LE(b.length - 8) !== 0xdb4775248b80fb57n)
|
|
88
|
+
throw new Error("Invalid LevelDB table");
|
|
89
|
+
const block = (handle) => {
|
|
90
|
+
const c = { at: 0 }, offset = varint(handle, c), size = varint(handle, c);
|
|
91
|
+
if (offset + size + 5 > b.length)
|
|
92
|
+
throw new Error("Truncated LevelDB block");
|
|
93
|
+
const data = b.subarray(offset, offset + size), compression = b[offset + size];
|
|
94
|
+
if (compression === 0)
|
|
95
|
+
return data;
|
|
96
|
+
if (compression === 1)
|
|
97
|
+
return snappy(data);
|
|
98
|
+
throw new Error("Unsupported LevelDB compression");
|
|
99
|
+
};
|
|
100
|
+
const footer = { at: b.length - 48 };
|
|
101
|
+
varint(b, footer);
|
|
102
|
+
varint(b, footer); // metaindex handle
|
|
103
|
+
entries(block(b.subarray(footer.at)), (_key, handle) => entries(block(handle), (key, value) => {
|
|
104
|
+
if (key.length < 8)
|
|
105
|
+
throw new Error("Invalid LevelDB internal key");
|
|
106
|
+
const tag = key.readBigUInt64LE(key.length - 8);
|
|
107
|
+
visit(key.subarray(0, -8), (tag & 255n) === 0n ? Buffer.alloc(0) : value, tag >> 8n);
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
function wal(b, visit) {
|
|
111
|
+
let chunks = [];
|
|
112
|
+
const batch = (data) => {
|
|
113
|
+
if (data.length < 12)
|
|
114
|
+
throw new Error("Invalid LevelDB batch");
|
|
115
|
+
const sequence = data.readBigUInt64LE(), count = data.readUInt32LE(8), c = { at: 12 };
|
|
116
|
+
for (let i = 0; i < count; i++) {
|
|
117
|
+
const type = data[c.at++];
|
|
118
|
+
if (type !== 0 && type !== 1)
|
|
119
|
+
throw new Error("Invalid LevelDB batch type");
|
|
120
|
+
const key = slice(data, c), value = type === 1 ? slice(data, c) : Buffer.alloc(0);
|
|
121
|
+
visit(key, value, sequence + BigInt(i));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
for (let at = 0; at + 7 <= b.length;) {
|
|
125
|
+
const remaining = 32768 - (at % 32768);
|
|
126
|
+
if (remaining < 7) {
|
|
127
|
+
at += remaining;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const size = b.readUInt16LE(at + 4), type = b[at + 6];
|
|
131
|
+
if (!size && !type) {
|
|
132
|
+
at += remaining;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (size + 7 > remaining)
|
|
136
|
+
throw new Error("Invalid LevelDB WAL record");
|
|
137
|
+
if (at + 7 + size > b.length)
|
|
138
|
+
break; // An in-flight append is retried on the next poll.
|
|
139
|
+
const data = b.subarray(at + 7, at + 7 + size);
|
|
140
|
+
at += 7 + size;
|
|
141
|
+
if (type === 1) {
|
|
142
|
+
batch(data);
|
|
143
|
+
chunks = [];
|
|
144
|
+
}
|
|
145
|
+
else if (type === 2)
|
|
146
|
+
chunks = [data];
|
|
147
|
+
else if (type === 3 && chunks.length)
|
|
148
|
+
chunks.push(data);
|
|
149
|
+
else if (type === 4 && chunks.length) {
|
|
150
|
+
chunks.push(data);
|
|
151
|
+
batch(Buffer.concat(chunks));
|
|
152
|
+
chunks = [];
|
|
153
|
+
}
|
|
154
|
+
else
|
|
155
|
+
throw new Error("Invalid LevelDB WAL fragment");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export class SidebarStorage {
|
|
159
|
+
files = new Map();
|
|
160
|
+
dir;
|
|
161
|
+
constructor(dir) { this.dir = dir; }
|
|
162
|
+
read(keyName) {
|
|
163
|
+
const wanted = Buffer.from(`_https://claude.ai\0\x01${keyName}`);
|
|
164
|
+
let names;
|
|
165
|
+
try {
|
|
166
|
+
names = fs.readdirSync(this.dir).filter(n => /^\d+\.(ldb|log)$/.test(n));
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error.code === "ENOENT")
|
|
170
|
+
return null;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
for (const name of this.files.keys())
|
|
174
|
+
if (!names.includes(name))
|
|
175
|
+
this.files.delete(name);
|
|
176
|
+
for (const name of names) {
|
|
177
|
+
const file = path.join(this.dir, name);
|
|
178
|
+
try {
|
|
179
|
+
const stat = fs.lstatSync(file), stamp = `${stat.mtimeMs}:${stat.size}`;
|
|
180
|
+
if (!stat.isFile() || this.files.get(name)?.stamp === stamp)
|
|
181
|
+
continue;
|
|
182
|
+
let sequence = -1n;
|
|
183
|
+
let value = Buffer.alloc(0);
|
|
184
|
+
const visit = (key, data, seq) => { if (key.equals(wanted) && seq > sequence) {
|
|
185
|
+
sequence = seq;
|
|
186
|
+
value = data;
|
|
187
|
+
} };
|
|
188
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
189
|
+
try {
|
|
190
|
+
const data = fs.readFileSync(fd);
|
|
191
|
+
name.endsWith(".log") ? wal(data, visit) : table(data, visit);
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
fs.closeSync(fd);
|
|
195
|
+
}
|
|
196
|
+
this.files.set(name, { stamp, sequence, value });
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (error.code !== "ENOENT")
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const latest = [...this.files.values()].sort((a, b) => a.sequence > b.sequence ? -1 : a.sequence < b.sequence ? 1 : 0)[0];
|
|
204
|
+
if (!latest || latest.sequence < 0n || !latest.value.length)
|
|
205
|
+
return null;
|
|
206
|
+
const encoding = latest.value[0];
|
|
207
|
+
if (encoding !== 0 && encoding !== 1)
|
|
208
|
+
throw new Error("Unsupported Chromium storage encoding");
|
|
209
|
+
return JSON.parse(latest.value.subarray(1).toString(encoding === 0 ? "utf16le" : "latin1"));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Claude's files are read-only. Group IDs refer to app sessionId, not cliSessionId.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { object } from "./bridge-client.js";
|
|
6
|
+
import { SidebarStorage } from "./sidebar-storage.js";
|
|
7
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
|
+
const number = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
9
|
+
const string = (v) => typeof v === "string" ? v : "";
|
|
10
|
+
const array = (v) => Array.isArray(v) ? v : [];
|
|
11
|
+
export const UNGROUPED = "sidebar:ungrouped";
|
|
12
|
+
export const PHONE_GROUP = "sidebar:phone";
|
|
13
|
+
export class DesktopSidebar {
|
|
14
|
+
root;
|
|
15
|
+
storage;
|
|
16
|
+
cache = new Map();
|
|
17
|
+
at = -Infinity;
|
|
18
|
+
current = { groups: [], sessions: new Map(), superseded: new Set() };
|
|
19
|
+
opts;
|
|
20
|
+
constructor(opts = {}) {
|
|
21
|
+
this.opts = opts;
|
|
22
|
+
this.root = opts.root ?? path.join(os.homedir(), "Library", "Application Support", "Claude");
|
|
23
|
+
this.storage = new SidebarStorage(path.join(this.root, "Local Storage", "leveldb"));
|
|
24
|
+
}
|
|
25
|
+
json(file) {
|
|
26
|
+
try {
|
|
27
|
+
const stat = fs.lstatSync(file);
|
|
28
|
+
if (!stat.isFile())
|
|
29
|
+
throw new Error("Non-regular desktop sidebar file");
|
|
30
|
+
const stamp = `${stat.mtimeMs}:${stat.size}`, cached = this.cache.get(file);
|
|
31
|
+
if (cached?.stamp === stamp)
|
|
32
|
+
return cached.value;
|
|
33
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
34
|
+
let value;
|
|
35
|
+
try {
|
|
36
|
+
value = JSON.parse(fs.readFileSync(fd, "utf8"));
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
fs.closeSync(fd);
|
|
40
|
+
}
|
|
41
|
+
this.cache.set(file, { stamp, value });
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code !== "ENOENT")
|
|
46
|
+
throw error;
|
|
47
|
+
this.cache.delete(file);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
read() {
|
|
52
|
+
const now = (this.opts.now ?? Date.now)();
|
|
53
|
+
if (now - this.at < 3000)
|
|
54
|
+
return this.current;
|
|
55
|
+
try {
|
|
56
|
+
return this.refresh(now);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
this.at = now;
|
|
60
|
+
if (!this.opts.onError)
|
|
61
|
+
throw error;
|
|
62
|
+
this.opts.onError(error);
|
|
63
|
+
return this.current;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
refresh(now) {
|
|
67
|
+
// Publish atomically; a racing/invalid external file never replaces a good snapshot.
|
|
68
|
+
const prefs = object(object(this.json(path.join(this.root, "claude_desktop_config.json")))?.preferences);
|
|
69
|
+
const epitaxy = object(prefs?.epitaxyPrefs);
|
|
70
|
+
const scopes = object(epitaxy?.["dframe-group-scopes"]);
|
|
71
|
+
const sections = object(epitaxy?.["dframe-code-sections"]);
|
|
72
|
+
const config = object(this.json(path.join(this.root, "config.json")));
|
|
73
|
+
const account = string(config?.lastKnownAccountUuid);
|
|
74
|
+
const scopeKeys = [...new Set([...Object.keys(scopes ?? {}), ...Object.keys(sections ?? {})])]
|
|
75
|
+
.filter(k => k.split("/").every(v => UUID.test(v)) && k.split("/").length === 2 && (!account || k.startsWith(account + "/")));
|
|
76
|
+
const unreadState = object(object(this.storage.read("epitaxy-unread-v1"))?.state);
|
|
77
|
+
const unread = new Set(array(unreadState?.unreadIds).filter((v) => typeof v === "string"));
|
|
78
|
+
const result = { groups: [], sessions: new Map(), superseded: new Set() };
|
|
79
|
+
for (const scope of scopeKeys)
|
|
80
|
+
this.scope(scope, object(scopes?.[scope]), object(sections?.[scope]), epitaxy, unread, result);
|
|
81
|
+
this.current = result;
|
|
82
|
+
this.at = now;
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
scope(scope, groups, layout, prefs, unread, result) {
|
|
86
|
+
const dir = path.join(this.root, "claude-code-sessions", scope);
|
|
87
|
+
let names;
|
|
88
|
+
try {
|
|
89
|
+
names = fs.readdirSync(dir);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error.code === "ENOENT")
|
|
93
|
+
return;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
const archiveIndex = object(this.json(path.join(dir, "archived-sessions.idx")));
|
|
97
|
+
const archived = new Set(array(archiveIndex?.archived).filter((v) => typeof v === "string"));
|
|
98
|
+
const assignments = object(groups?.assignments), orders = object(groups?.order);
|
|
99
|
+
const manual = array(groups?.groups).map(object).filter(g => g && typeof g.id === "string");
|
|
100
|
+
const rows = [];
|
|
101
|
+
for (const name of names.filter(n => /^local_[0-9a-f-]+\.json$/i.test(n))) {
|
|
102
|
+
const v = object(this.json(path.join(dir, name)));
|
|
103
|
+
if (!v || !UUID.test(string(v.cliSessionId)))
|
|
104
|
+
continue;
|
|
105
|
+
const sessionId = string(v.sessionId) || name.slice(0, -5), group = string(assignments?.[`code:${sessionId}`]);
|
|
106
|
+
for (const prior of array(v.priorCliSessionIds))
|
|
107
|
+
if (typeof prior === "string" && UUID.test(prior))
|
|
108
|
+
result.superseded.add(prior);
|
|
109
|
+
const routine = typeof v.scheduledTaskId === "string" && v.scheduledTaskId.length > 0;
|
|
110
|
+
// The app persists its unread set to Local Storage late; a session focused in the app after
|
|
111
|
+
// its last activity is read even while the stored set still lists it (2026-10-13: 데이리플
|
|
112
|
+
// showed yellow on the phone while the Mac, which had it open, showed it working).
|
|
113
|
+
const focused = number(v.lastFocusedAt), active = number(v.lastActivityAt);
|
|
114
|
+
const seen = focused > 0 && active > 0 && focused >= active;
|
|
115
|
+
rows.push({ cliSessionId: string(v.cliSessionId), sessionId, title: string(v.title), unread: unread.has(sessionId) && !seen,
|
|
116
|
+
archived: v.isArchived === true || archived.has(sessionId), group: routine ? "routines" : manual.some(g => g?.id === group) ? group : "sessions",
|
|
117
|
+
order: 0, activity: number(v.lastActivityAt) || number(v.createdAt), created: number(v.createdAt) });
|
|
118
|
+
}
|
|
119
|
+
const pinned = new Set(array(prefs?.["starred-local-code-sessions"]).filter((v) => typeof v === "string"));
|
|
120
|
+
const configured = array(layout?.sections).map(object).filter((v) => v !== null);
|
|
121
|
+
const sectionList = configured.length ? configured.sort((a, b) => number(a.order) - number(b.order)) :
|
|
122
|
+
[...manual.map(g => ({ id: g?.id, kind: "manual", name: g?.name })), { id: "sessions", kind: "sessions", name: "" }];
|
|
123
|
+
const consumed = new Set();
|
|
124
|
+
for (const section of sectionList) {
|
|
125
|
+
if (section.shown === false)
|
|
126
|
+
continue;
|
|
127
|
+
const id = string(section.id), groupName = section.kind === "pinned" ? "sidebar:pinned" : section.kind === "routines" ? "sidebar:routines" : section.kind === "sessions" ? UNGROUPED : string(section.name);
|
|
128
|
+
if (!groupName)
|
|
129
|
+
continue;
|
|
130
|
+
const members = new Set(array(section.members).filter((v) => typeof v === "string"));
|
|
131
|
+
const selected = rows.filter(r => !r.archived && !consumed.has(r.cliSessionId) && (section.kind === "pinned" ? pinned.has(r.sessionId) : r.group === id || members.has(`code:${r.sessionId}`)));
|
|
132
|
+
const order = array(orders?.[id]);
|
|
133
|
+
selected.sort((a, b) => {
|
|
134
|
+
let delta;
|
|
135
|
+
if (section.sortBy === "manual") {
|
|
136
|
+
const rank = (r) => { const i = order.indexOf(`code:${r.sessionId}`); return i < 0 ? Number.MAX_SAFE_INTEGER : i; };
|
|
137
|
+
delta = rank(a) - rank(b);
|
|
138
|
+
}
|
|
139
|
+
else if (section.sortBy === "title" || section.sortBy === "name")
|
|
140
|
+
delta = a.title.localeCompare(b.title);
|
|
141
|
+
else if (section.sortBy === "created")
|
|
142
|
+
delta = b.created - a.created;
|
|
143
|
+
else
|
|
144
|
+
delta = b.activity - a.activity;
|
|
145
|
+
return (section.ascending === true ? -delta : delta) || a.sessionId.localeCompare(b.sessionId);
|
|
146
|
+
});
|
|
147
|
+
const group = { name: groupName, sessions: selected.map(r => r.cliSessionId) };
|
|
148
|
+
result.groups.push(group);
|
|
149
|
+
selected.forEach((r, order) => { consumed.add(r.cliSessionId); result.sessions.set(r.cliSessionId, { ...r, group: groupName, order }); });
|
|
150
|
+
}
|
|
151
|
+
// Archived and hidden rows remain in the index so joins can exclude them.
|
|
152
|
+
for (const r of rows)
|
|
153
|
+
if (!result.sessions.has(r.cliSessionId))
|
|
154
|
+
result.sessions.set(r.cliSessionId, { ...r, archived: true });
|
|
155
|
+
}
|
|
156
|
+
join(sessions, cliId) {
|
|
157
|
+
const sidebar = this.read();
|
|
158
|
+
if (!sidebar.groups.length)
|
|
159
|
+
return sessions;
|
|
160
|
+
return sessions.flatMap(s => {
|
|
161
|
+
const row = sidebar.sessions.get(cliId(s.id));
|
|
162
|
+
if (row?.archived)
|
|
163
|
+
return [];
|
|
164
|
+
// The host keeps one record per CLI session id; an app session that has moved on from one
|
|
165
|
+
// (2026-10-13: 클로드리플 and 클로드리플2 each listed twice, the old half stuck at "42 min").
|
|
166
|
+
if (!row && sidebar.superseded.has(cliId(s.id)))
|
|
167
|
+
return [];
|
|
168
|
+
if (!row)
|
|
169
|
+
return [{ ...s, group: s.origin === "desktop" ? UNGROUPED : PHONE_GROUP }];
|
|
170
|
+
return [{ ...s, title: row.title || s.title, unread: row.unread, group: row.group, order: row.order, updatedAt: row.activity }];
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export function record(value) {
|
|
2
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3
|
+
}
|
|
4
|
+
export function toolInput(value) {
|
|
5
|
+
if (typeof value !== "string")
|
|
6
|
+
return null;
|
|
7
|
+
try {
|
|
8
|
+
return record(JSON.parse(value));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
} // Legacy tools may store a plain prompt, not JSON.
|
|
13
|
+
}
|
|
14
|
+
export const stringField = (input, key) => typeof input?.[key] === "string" ? input[key] : "";
|
|
15
|
+
function lines(text) {
|
|
16
|
+
if (!text)
|
|
17
|
+
return [];
|
|
18
|
+
const list = text.replace(/\r\n/g, "\n").split("\n");
|
|
19
|
+
if (list.at(-1) === "")
|
|
20
|
+
list.pop();
|
|
21
|
+
return list;
|
|
22
|
+
}
|
|
23
|
+
/** A linear-space line diff. Common edges are peeled before finding the next shared line,
|
|
24
|
+
* avoiding an N×M matrix for large writes. Equal-distance anchors prefer removals. */
|
|
25
|
+
export function diffLines(before, after) {
|
|
26
|
+
const a = lines(before), b = lines(after);
|
|
27
|
+
const positions = new Map();
|
|
28
|
+
b.forEach((line, i) => { const at = positions.get(line) ?? []; at.push(i); positions.set(line, at); });
|
|
29
|
+
const out = [];
|
|
30
|
+
let i = 0, j = 0;
|
|
31
|
+
const remove = () => out.push({ kind: "remove", text: a[i], old: ++i, next: null });
|
|
32
|
+
const add = () => out.push({ kind: "add", text: b[j], old: null, next: ++j });
|
|
33
|
+
const context = () => out.push({ kind: "context", text: a[i], old: ++i, next: ++j });
|
|
34
|
+
while (i < a.length && j < b.length) {
|
|
35
|
+
if (a[i] === b[j]) {
|
|
36
|
+
context();
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
let anchor = null;
|
|
40
|
+
for (let k = i; k < a.length; k++) {
|
|
41
|
+
if (anchor && k - i > anchor.distance)
|
|
42
|
+
break;
|
|
43
|
+
const candidates = positions.get(a[k]);
|
|
44
|
+
if (!candidates)
|
|
45
|
+
continue;
|
|
46
|
+
// Binary search: the same repeated line can occur thousands of times.
|
|
47
|
+
let lo = 0, hi = candidates.length;
|
|
48
|
+
while (lo < hi) {
|
|
49
|
+
const mid = (lo + hi) >>> 1;
|
|
50
|
+
if (candidates[mid] < j)
|
|
51
|
+
lo = mid + 1;
|
|
52
|
+
else
|
|
53
|
+
hi = mid;
|
|
54
|
+
}
|
|
55
|
+
const next = candidates[lo];
|
|
56
|
+
if (next === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
const distance = k - i + next - j;
|
|
59
|
+
if (!anchor || distance < anchor.distance)
|
|
60
|
+
anchor = { i: k, j: next, distance };
|
|
61
|
+
}
|
|
62
|
+
if (!anchor) {
|
|
63
|
+
while (i < a.length)
|
|
64
|
+
remove();
|
|
65
|
+
while (j < b.length)
|
|
66
|
+
add();
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
while (i < anchor.i)
|
|
70
|
+
remove();
|
|
71
|
+
while (j < anchor.j)
|
|
72
|
+
add();
|
|
73
|
+
}
|
|
74
|
+
while (i < a.length)
|
|
75
|
+
remove();
|
|
76
|
+
while (j < b.length)
|
|
77
|
+
add();
|
|
78
|
+
// A changed final newline is a real edit, even if every visible line is equal.
|
|
79
|
+
if (before !== after && before.endsWith("\n") !== after.endsWith("\n") && a.length && b.length && a.at(-1) === b.at(-1)) {
|
|
80
|
+
const last = out.at(-1);
|
|
81
|
+
if (last?.kind === "context")
|
|
82
|
+
out.splice(out.length - 1, 1, { kind: "remove", text: last.text + (before.endsWith("\n") ? "" : "\n\"), old: last.old, next: null }, { kind: "add", text: last.text + (after.endsWith("\n") ? "" : "\n\"), old: null, next: last.next });
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
export function toolDiffs(name, input) {
|
|
87
|
+
if (!input || !["Edit", "MultiEdit", "Write"].includes(name))
|
|
88
|
+
return [];
|
|
89
|
+
const path = stringField(input, "file_path") || stringField(input, "path") || "file";
|
|
90
|
+
const edits = name === "MultiEdit" && Array.isArray(input.edits) ? input.edits.map(record).filter(e => e !== null) : [input];
|
|
91
|
+
return edits.map(edit => {
|
|
92
|
+
const before = name === "Write" ? stringField(edit, "old_content") : stringField(edit, "old_string");
|
|
93
|
+
const after = name === "Write" ? stringField(edit, "content") : stringField(edit, "new_string");
|
|
94
|
+
const diff = diffLines(before, after);
|
|
95
|
+
return { path, header: `@@ -${before ? 1 : 0},${lines(before).length} +${after ? 1 : 0},${lines(after).length} @@`, lines: diff };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
export function changeCount(name, input) {
|
|
99
|
+
return toolDiffs(name, input).flatMap(d => d.lines).reduce((sum, line) => ({
|
|
100
|
+
added: sum.added + Number(line.kind === "add"), removed: sum.removed + Number(line.kind === "remove"),
|
|
101
|
+
}), { added: 0, removed: 0 });
|
|
102
|
+
}
|
|
103
|
+
function category(name) {
|
|
104
|
+
if (["Bash", "BashOutput", "KillShell"].includes(name))
|
|
105
|
+
return "command";
|
|
106
|
+
if (name === "Read")
|
|
107
|
+
return "read";
|
|
108
|
+
if (["Edit", "MultiEdit", "Write", "NotebookEdit"].includes(name))
|
|
109
|
+
return "edit";
|
|
110
|
+
if (["Grep", "Glob", "LS"].includes(name))
|
|
111
|
+
return "search";
|
|
112
|
+
if (["WebSearch", "WebFetch"].includes(name))
|
|
113
|
+
return "web";
|
|
114
|
+
if (["Task", "Agent"].includes(name))
|
|
115
|
+
return "agent";
|
|
116
|
+
return "other";
|
|
117
|
+
}
|
|
118
|
+
const completed = {
|
|
119
|
+
ko: { command: (n) => `명령어 ${n}개 실행함`, read: (n) => `파일 ${n}개 읽음`, edit: (n) => `파일 ${n}개를 편집함`, search: (n) => `검색 ${n}회`, web: (n) => `웹 ${n}회`, agent: (n) => `에이전트 ${n}개`, other: (n) => `도구 ${n}개 사용함` },
|
|
120
|
+
en: { command: (n) => `Ran ${n} command${n === 1 ? "" : "s"}`, read: (n) => `Read ${n} file${n === 1 ? "" : "s"}`, edit: (n) => `Edited ${n} file${n === 1 ? "" : "s"}`, search: (n) => `${n} search${n === 1 ? "" : "es"}`, web: (n) => `${n} web request${n === 1 ? "" : "s"}`, agent: (n) => `${n} agent${n === 1 ? "" : "s"}`, other: (n) => `Used ${n} tool${n === 1 ? "" : "s"}` },
|
|
121
|
+
};
|
|
122
|
+
const active = {
|
|
123
|
+
ko: { command: "명령어 실행 중…", read: "파일 읽는 중…", edit: "파일 편집 중…", search: "검색 중…", web: "웹 검색 중…", agent: "에이전트 실행 중…", other: "도구 실행 중…" },
|
|
124
|
+
en: { command: "Running command…", read: "Reading file…", edit: "Editing file…", search: "Searching…", web: "Searching the web…", agent: "Running agent…", other: "Running tool…" },
|
|
125
|
+
};
|
|
126
|
+
const more = { ko: (n) => `그 외 작업 ${n}개`, en: (n) => `and ${n} more action${n === 1 ? "" : "s"}` };
|
|
127
|
+
/** Like the Claude app: kinds in the order they first appear, files counted once per path, five or more kinds cut to three. */
|
|
128
|
+
export function activitySummary(rows, running, language) {
|
|
129
|
+
const counts = new Map();
|
|
130
|
+
const paths = new Set();
|
|
131
|
+
let added = 0, removed = 0, failed = false, pending = null;
|
|
132
|
+
for (const row of rows) {
|
|
133
|
+
const kind = row.type === "subagent" ? "agent" : category(row.tool.name);
|
|
134
|
+
const input = row.type === "tool" ? toolInput(row.tool.input) : null;
|
|
135
|
+
const path = stringField(input, "file_path") || stringField(input, "path") || (row.type === "tool" ? row.tool.summary : "");
|
|
136
|
+
const file = (kind === "edit" || kind === "read") && path ? `${kind}:${path}` : "";
|
|
137
|
+
if (!file || !paths.has(file))
|
|
138
|
+
counts.set(kind, (counts.get(kind) ?? 0) + 1);
|
|
139
|
+
if (file)
|
|
140
|
+
paths.add(file);
|
|
141
|
+
if (row.type === "subagent") {
|
|
142
|
+
failed ||= row.status === "failed";
|
|
143
|
+
if (row.status === "running")
|
|
144
|
+
pending = kind;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
failed ||= row.tool.result?.ok === false;
|
|
148
|
+
if (!row.tool.result)
|
|
149
|
+
pending = kind;
|
|
150
|
+
const changes = row.tool.changes ?? changeCount(row.tool.name, input);
|
|
151
|
+
added += changes.added;
|
|
152
|
+
removed += changes.removed;
|
|
153
|
+
}
|
|
154
|
+
const parts = [...counts];
|
|
155
|
+
const shown = parts.length >= 5 ? parts.slice(0, 3) : parts;
|
|
156
|
+
const rest = parts.slice(shown.length).reduce((sum, [, n]) => sum + n, 0);
|
|
157
|
+
const text = running && pending ? active[language][pending] : [...shown.map(([kind, n]) => completed[language][kind](n)), ...(rest ? [more[language](rest)] : [])].join(", ");
|
|
158
|
+
return { text, added, removed, failed };
|
|
159
|
+
}
|
|
160
|
+
export function numberedRead(text, input) {
|
|
161
|
+
const offset = typeof input?.offset === "number" && input.offset > 0 ? Math.floor(input.offset) : 1;
|
|
162
|
+
return lines(text).map((line, i) => {
|
|
163
|
+
const match = /^\s*(\d+)[→\t](.*)$/.exec(line);
|
|
164
|
+
return match ? { number: Number(match[1]), text: match[2] } : { number: offset + i, text: line };
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
export function pretty(text) {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return text;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export function webLinks(text) {
|
|
176
|
+
const out = new Map();
|
|
177
|
+
const add = (url, title) => { if (/^https?:\/\//.test(url))
|
|
178
|
+
out.set(url, title || url); };
|
|
179
|
+
function walk(value) {
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
value.forEach(walk);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const obj = record(value);
|
|
185
|
+
if (!obj)
|
|
186
|
+
return;
|
|
187
|
+
const url = stringField(obj, "url") || stringField(obj, "link");
|
|
188
|
+
if (url)
|
|
189
|
+
add(url, stringField(obj, "title") || stringField(obj, "name"));
|
|
190
|
+
Object.values(obj).forEach(v => { if (typeof v === "object")
|
|
191
|
+
walk(v); });
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
walk(JSON.parse(text));
|
|
195
|
+
}
|
|
196
|
+
catch { /* Text/Markdown output is normal. */ }
|
|
197
|
+
for (const match of text.matchAll(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g))
|
|
198
|
+
add(match[2], match[1]);
|
|
199
|
+
for (const match of text.matchAll(/https?:\/\/[^\s<>"\])]+/g)) {
|
|
200
|
+
const url = match[0].replace(/[.,;]+$/, "");
|
|
201
|
+
if (!out.has(url))
|
|
202
|
+
add(url, url);
|
|
203
|
+
}
|
|
204
|
+
return [...out].map(([url, title]) => ({ title, url }));
|
|
205
|
+
}
|