@chenacc/agentbridge 0.0.0-stage → 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/.env.example +44 -0
- package/LICENSE +21 -0
- package/README.md +297 -2
- package/bin/agentbridge.js +7 -0
- package/docs/RELEASING.md +47 -0
- package/package.json +65 -4
- package/public/adapter-capabilities.js +29 -0
- package/public/app.js +2211 -0
- package/public/background.jpg +0 -0
- package/public/command-id.js +16 -0
- package/public/highlight.js +125 -0
- package/public/index.html +339 -0
- package/public/markdown.js +291 -0
- package/public/styles.css +1073 -0
- package/public/transcript.js +368 -0
- package/public/workspace-navigation.js +241 -0
- package/src/adapter-registry.js +23 -0
- package/src/adapters/capabilities.js +36 -0
- package/src/adapters/claude.js +493 -0
- package/src/adapters/codex.js +529 -0
- package/src/adapters/deepseek-harness.js +413 -0
- package/src/adapters/dsh-desktop.js +322 -0
- package/src/adapters/echo.js +57 -0
- package/src/adapters/process-utils.js +125 -0
- package/src/adapters/unavailable.js +30 -0
- package/src/adapters/zcode-native.js +40 -0
- package/src/attachments.js +187 -0
- package/src/cc-switch-models.js +86 -0
- package/src/cli.js +199 -0
- package/src/config.js +127 -0
- package/src/conversations/conversation-catalog.js +283 -0
- package/src/dsh-desktop-channels.js +460 -0
- package/src/event-store.js +330 -0
- package/src/notifier.js +266 -0
- package/src/pairing.js +167 -0
- package/src/server.js +957 -0
- package/src/session-manager.js +1355 -0
- package/src/workspaces/workspace-catalog.js +196 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, realpath, rename, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const SCHEMA_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
function pathKey(value, platform) {
|
|
8
|
+
return platform === "win32" ? value.toLowerCase() : value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function generatedAlias(canonicalPath) {
|
|
12
|
+
const slug = path.basename(canonicalPath)
|
|
13
|
+
.replace(/[^a-zA-Z0-9_-]+/g, "-")
|
|
14
|
+
.replace(/^-+|-+$/g, "")
|
|
15
|
+
.slice(0, 22) || "project";
|
|
16
|
+
const suffix = createHash("sha256").update(canonicalPath).digest("hex").slice(0, 8);
|
|
17
|
+
return `local-${slug}-${suffix}`.slice(0, 40);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateAlias(value) {
|
|
21
|
+
return typeof value === "string" && /^[a-zA-Z0-9_-]{1,40}$/.test(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function copy(record) {
|
|
25
|
+
return record ? { ...record } : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class WorkspaceCatalog {
|
|
29
|
+
#byId = new Map();
|
|
30
|
+
#byAlias = new Map();
|
|
31
|
+
#byPath = new Map();
|
|
32
|
+
#writes = Promise.resolve();
|
|
33
|
+
#revision = 0;
|
|
34
|
+
|
|
35
|
+
constructor({ filePath, initialProjects = new Map(), platform = process.platform, now = () => new Date().toISOString() }) {
|
|
36
|
+
this.filePath = filePath;
|
|
37
|
+
this.initialProjects = initialProjects;
|
|
38
|
+
this.platform = platform;
|
|
39
|
+
this.now = now;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async init() {
|
|
43
|
+
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
44
|
+
await this.#load();
|
|
45
|
+
let changed = false;
|
|
46
|
+
for (const [alias, requestedPath] of this.initialProjects) {
|
|
47
|
+
const result = await this.#registerRecord({ requestedPath, alias, source: "config" });
|
|
48
|
+
changed ||= result.changed;
|
|
49
|
+
}
|
|
50
|
+
if (changed) await this.#persist();
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get revision() {
|
|
55
|
+
return this.#revision;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
list() {
|
|
59
|
+
return [...this.#byId.values()]
|
|
60
|
+
.map(copy)
|
|
61
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
listPublic() {
|
|
65
|
+
return this.list()
|
|
66
|
+
.filter((record) => record.enabled)
|
|
67
|
+
.map(({ id, alias, name, enabled }) => ({ id, alias, name, enabled }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get(identifier) {
|
|
71
|
+
return copy(this.#byId.get(identifier) ?? this.#byAlias.get(identifier));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async register({ requestedPath, alias = null, source = "local" }) {
|
|
75
|
+
const result = await this.#registerRecord({ requestedPath, alias, source });
|
|
76
|
+
if (result.changed) await this.#persist();
|
|
77
|
+
return copy(result.record);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async disable(identifier) {
|
|
81
|
+
const record = this.#byId.get(identifier) ?? this.#byAlias.get(identifier);
|
|
82
|
+
if (!record || !record.enabled) return false;
|
|
83
|
+
record.enabled = false;
|
|
84
|
+
record.updatedAt = this.now();
|
|
85
|
+
this.#revision += 1;
|
|
86
|
+
await this.#persist();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async #canonicalize(requestedPath) {
|
|
91
|
+
if (typeof requestedPath !== "string" || !path.isAbsolute(requestedPath)) {
|
|
92
|
+
throw Object.assign(new Error("cwd must be an absolute local path"), { statusCode: 400 });
|
|
93
|
+
}
|
|
94
|
+
let canonicalPath;
|
|
95
|
+
try {
|
|
96
|
+
canonicalPath = await realpath(requestedPath);
|
|
97
|
+
if (!(await stat(canonicalPath)).isDirectory()) throw new Error("not a directory");
|
|
98
|
+
} catch {
|
|
99
|
+
throw Object.assign(new Error("cwd must be an existing directory"), { statusCode: 400 });
|
|
100
|
+
}
|
|
101
|
+
return canonicalPath;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async #registerRecord({ requestedPath, alias, source }) {
|
|
105
|
+
const canonicalPath = await this.#canonicalize(requestedPath);
|
|
106
|
+
const existing = this.#byPath.get(pathKey(canonicalPath, this.platform));
|
|
107
|
+
if (existing) {
|
|
108
|
+
if (!existing.enabled) {
|
|
109
|
+
existing.enabled = true;
|
|
110
|
+
existing.updatedAt = this.now();
|
|
111
|
+
this.#revision += 1;
|
|
112
|
+
return { record: existing, changed: true };
|
|
113
|
+
}
|
|
114
|
+
return { record: existing, changed: false };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const selectedAlias = alias ?? generatedAlias(canonicalPath);
|
|
118
|
+
if (!validateAlias(selectedAlias)) {
|
|
119
|
+
throw Object.assign(new Error("Workspace alias is invalid"), { statusCode: 400 });
|
|
120
|
+
}
|
|
121
|
+
if (this.#byAlias.has(selectedAlias)) {
|
|
122
|
+
throw Object.assign(new Error(`Workspace alias already belongs to another path: ${selectedAlias}`), { statusCode: 409 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const timestamp = this.now();
|
|
126
|
+
const record = {
|
|
127
|
+
id: `ws_${randomUUID()}`,
|
|
128
|
+
alias: selectedAlias,
|
|
129
|
+
name: path.basename(canonicalPath) || selectedAlias,
|
|
130
|
+
canonicalPath,
|
|
131
|
+
source: source === "config" ? "config" : "local",
|
|
132
|
+
enabled: true,
|
|
133
|
+
createdAt: timestamp,
|
|
134
|
+
updatedAt: timestamp,
|
|
135
|
+
};
|
|
136
|
+
this.#index(record);
|
|
137
|
+
this.#revision += 1;
|
|
138
|
+
return { record, changed: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#index(record) {
|
|
142
|
+
this.#byId.set(record.id, record);
|
|
143
|
+
this.#byAlias.set(record.alias, record);
|
|
144
|
+
this.#byPath.set(pathKey(record.canonicalPath, this.platform), record);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async #load() {
|
|
148
|
+
let parsed;
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(await readFile(this.filePath, "utf8"));
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error.code === "ENOENT") return;
|
|
153
|
+
throw Object.assign(new Error(`Workspace catalog could not be read: ${error.message}`), { cause: error });
|
|
154
|
+
}
|
|
155
|
+
if (parsed?.schemaVersion !== SCHEMA_VERSION || !Array.isArray(parsed.items)) {
|
|
156
|
+
throw new Error("Workspace catalog has an unsupported schema");
|
|
157
|
+
}
|
|
158
|
+
this.#revision = Number.isSafeInteger(parsed.revision) ? parsed.revision : 0;
|
|
159
|
+
for (const record of parsed.items) {
|
|
160
|
+
if (!record || typeof record.id !== "string" || !validateAlias(record.alias) || !path.isAbsolute(record.canonicalPath)) {
|
|
161
|
+
throw new Error("Workspace catalog contains an invalid record");
|
|
162
|
+
}
|
|
163
|
+
this.#index({
|
|
164
|
+
id: record.id,
|
|
165
|
+
alias: record.alias,
|
|
166
|
+
name: String(record.name || path.basename(record.canonicalPath) || record.alias).slice(0, 100),
|
|
167
|
+
canonicalPath: record.canonicalPath,
|
|
168
|
+
source: record.source === "config" ? "config" : "local",
|
|
169
|
+
enabled: record.enabled !== false,
|
|
170
|
+
createdAt: String(record.createdAt || this.now()),
|
|
171
|
+
updatedAt: String(record.updatedAt || record.createdAt || this.now()),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#persist() {
|
|
177
|
+
const payload = JSON.stringify({
|
|
178
|
+
schemaVersion: SCHEMA_VERSION,
|
|
179
|
+
revision: this.#revision,
|
|
180
|
+
items: this.list(),
|
|
181
|
+
});
|
|
182
|
+
const operation = this.#writes.catch(() => undefined).then(async () => {
|
|
183
|
+
const temporaryPath = `${this.filePath}.tmp`;
|
|
184
|
+
const handle = await open(temporaryPath, "w");
|
|
185
|
+
try {
|
|
186
|
+
await handle.writeFile(payload, "utf8");
|
|
187
|
+
await handle.sync();
|
|
188
|
+
} finally {
|
|
189
|
+
await handle.close();
|
|
190
|
+
}
|
|
191
|
+
await rename(temporaryPath, this.filePath);
|
|
192
|
+
});
|
|
193
|
+
this.#writes = operation;
|
|
194
|
+
return operation;
|
|
195
|
+
}
|
|
196
|
+
}
|