@workbench-ai/workbench 0.0.73 → 0.0.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev-open/client.css +0 -6
- package/dist/dev-open/client.js +113 -113
- package/dist/fanout.d.ts +13 -0
- package/dist/fanout.d.ts.map +1 -0
- package/dist/fanout.js +223 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +306 -229
- package/dist/install-targets.d.ts +54 -21
- package/dist/install-targets.d.ts.map +1 -1
- package/dist/install-targets.js +333 -118
- package/package.json +7 -6
|
@@ -1,35 +1,68 @@
|
|
|
1
1
|
import { type Json, type SurfaceSnapshotFile } from "@workbench-ai/workbench-core";
|
|
2
|
-
|
|
3
|
-
export type WorkbenchInstallTargetName = WorkbenchInstallAgentTarget | "local";
|
|
2
|
+
type StoreKind = "machine" | "local";
|
|
4
3
|
export interface WorkbenchInstallSnapshot {
|
|
5
4
|
name: string;
|
|
6
5
|
files: SurfaceSnapshotFile[];
|
|
7
6
|
}
|
|
8
|
-
export interface
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
export interface WorkbenchInstallProvenanceInput {
|
|
8
|
+
handle: string;
|
|
9
|
+
versionId: string;
|
|
10
|
+
baseUrl: string;
|
|
12
11
|
}
|
|
13
|
-
export interface
|
|
14
|
-
|
|
12
|
+
export interface WorkbenchInstallProvenanceRecord extends WorkbenchInstallProvenanceInput {
|
|
13
|
+
installedAt: string;
|
|
14
|
+
contentHash: string;
|
|
15
15
|
}
|
|
16
|
-
export interface
|
|
17
|
-
result: "installed" | "unchanged";
|
|
18
|
-
|
|
16
|
+
export interface WorkbenchInstallStoreResult {
|
|
17
|
+
result: "installed" | "planned" | "unchanged";
|
|
18
|
+
store: StoreKind;
|
|
19
|
+
directoryName: string;
|
|
20
|
+
destination: string;
|
|
21
|
+
previous: "none" | "overwritten" | "unchanged";
|
|
19
22
|
filesCopied: number;
|
|
23
|
+
contentHash: string;
|
|
24
|
+
provenancePath: string;
|
|
25
|
+
}
|
|
26
|
+
export type WorkbenchInstalledSkillStatus = "current" | "update" | "modified" | "unmanaged";
|
|
27
|
+
export interface WorkbenchInstalledSkill {
|
|
28
|
+
store: StoreKind;
|
|
29
|
+
storeRoot: string;
|
|
30
|
+
name: string;
|
|
31
|
+
directoryName: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
path: string;
|
|
34
|
+
versionId?: string;
|
|
35
|
+
handle?: string;
|
|
36
|
+
baseUrl?: string;
|
|
37
|
+
installedAt?: string;
|
|
38
|
+
contentHash: string;
|
|
39
|
+
status: WorkbenchInstalledSkillStatus;
|
|
40
|
+
latestVersionId?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface WorkbenchInstalledInventory {
|
|
43
|
+
stores: Array<{
|
|
44
|
+
kind: StoreKind;
|
|
45
|
+
path: string;
|
|
46
|
+
}>;
|
|
47
|
+
skills: WorkbenchInstalledSkill[];
|
|
48
|
+
next: string | null;
|
|
20
49
|
}
|
|
21
|
-
export declare function
|
|
22
|
-
export declare function
|
|
23
|
-
|
|
24
|
-
local: boolean;
|
|
25
|
-
skillName: string;
|
|
26
|
-
}): WorkbenchInstallTarget[];
|
|
27
|
-
export declare function installSnapshotToTargets(options: {
|
|
50
|
+
export declare function canonicalSkillsStore(): string;
|
|
51
|
+
export declare function provenancePathForStore(storeRoot?: string): string;
|
|
52
|
+
export declare function installSnapshotToStore(options: {
|
|
28
53
|
snapshot: WorkbenchInstallSnapshot;
|
|
29
|
-
targets: readonly WorkbenchInstallTarget[];
|
|
30
54
|
overwrite: boolean;
|
|
31
55
|
dryRun: boolean;
|
|
32
|
-
|
|
56
|
+
provenance: WorkbenchInstallProvenanceInput;
|
|
57
|
+
installedAt?: string;
|
|
58
|
+
}): Promise<WorkbenchInstallStoreResult>;
|
|
59
|
+
export declare function canonicalSkillDirectoryName(snapshot: WorkbenchInstallSnapshot): string;
|
|
60
|
+
export declare function readInstalledSkillsInventory(options?: {
|
|
61
|
+
cwd?: string;
|
|
62
|
+
includeUpdates?: boolean;
|
|
63
|
+
lookupLatestVersion?: (record: WorkbenchInstallProvenanceRecord) => Promise<string | undefined>;
|
|
64
|
+
}): Promise<WorkbenchInstalledInventory>;
|
|
65
|
+
export declare function installedInventoryToJson(inventory: WorkbenchInstalledInventory): Record<string, Json>;
|
|
33
66
|
export declare function normalizeInstallSnapshotPath(filePath: string): string;
|
|
34
|
-
export
|
|
67
|
+
export {};
|
|
35
68
|
//# sourceMappingURL=install-targets.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install-targets.d.ts","sourceRoot":"","sources":["../src/install-targets.ts"],"names":[],"mappings":"AAIA,OAAO,
|
|
1
|
+
{"version":3,"file":"install-targets.d.ts","sourceRoot":"","sources":["../src/install-targets.ts"],"names":[],"mappings":"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"}
|
package/dist/install-targets.js
CHANGED
|
@@ -1,115 +1,126 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { WorkbenchCodedError } from "@workbench-ai/workbench-core";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
];
|
|
4
|
+
import { hashJson, WorkbenchCodedError } from "@workbench-ai/workbench-core";
|
|
5
|
+
import YAML from "yaml";
|
|
6
|
+
const PROVENANCE_SCHEMA = "workbench.provenance.v1";
|
|
7
|
+
const PROVENANCE_FILE = ".workbench-provenance.json";
|
|
8
|
+
export function canonicalSkillsStore() {
|
|
9
|
+
return path.join(homeDirectory(), ".agents", "skills");
|
|
11
10
|
}
|
|
12
|
-
export function
|
|
13
|
-
|
|
14
|
-
for (const rawAgent of options.agents) {
|
|
15
|
-
const agent = rawAgent.trim().toLowerCase();
|
|
16
|
-
if (agent !== "codex" && agent !== "claude") {
|
|
17
|
-
throw new WorkbenchCodedError("usage", `Unsupported install agent: ${rawAgent}`, {
|
|
18
|
-
remediation: "Use --to codex, --to claude, or --to local.",
|
|
19
|
-
exitCode: 2,
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
targets.push({
|
|
23
|
-
agent,
|
|
24
|
-
mode: "copy",
|
|
25
|
-
destination: path.join(workbenchAgentHome(agent), "skills", options.skillName),
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
if (options.local) {
|
|
29
|
-
targets.push({
|
|
30
|
-
agent: "local",
|
|
31
|
-
mode: "copy",
|
|
32
|
-
destination: path.join(process.cwd(), ".agents", "skills", options.skillName),
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
if (targets.length === 0) {
|
|
36
|
-
throw new WorkbenchCodedError("install_target_required", "workbench install requires an explicit target.", {
|
|
37
|
-
remediation: "Run workbench install OWNER/SKILL --to codex, workbench install OWNER/SKILL --to claude, or workbench install OWNER/SKILL --to local.",
|
|
38
|
-
exitCode: 2,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
return dedupeTargets(targets);
|
|
11
|
+
export function provenancePathForStore(storeRoot = canonicalSkillsStore()) {
|
|
12
|
+
return path.join(storeRoot, PROVENANCE_FILE);
|
|
42
13
|
}
|
|
43
|
-
export async function
|
|
14
|
+
export async function installSnapshotToStore(options) {
|
|
15
|
+
const skillName = canonicalSkillDirectoryName(options.snapshot);
|
|
16
|
+
const destination = path.join(canonicalSkillsStore(), skillName);
|
|
44
17
|
const normalizedFiles = options.snapshot.files.map((file) => ({
|
|
45
18
|
...file,
|
|
46
19
|
path: normalizeInstallSnapshotPath(file.path),
|
|
47
20
|
}));
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const existing = await readExistingTree(target.destination).catch((error) => {
|
|
54
|
-
const code = error.code;
|
|
55
|
-
if (code === "ENOENT") {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
if (code === "ENOTDIR") {
|
|
59
|
-
const conflictPath = error.path ?? target.destination;
|
|
60
|
-
throw new WorkbenchCodedError("install_failed", `Install target path is blocked by an existing file: ${conflictPath}`, {
|
|
61
|
-
remediation: `Remove the conflicting file ${conflictPath} and rerun the install.`,
|
|
62
|
-
subject: { destination: target.destination, conflictPath },
|
|
63
|
-
exitCode: 1,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
throw error;
|
|
67
|
-
});
|
|
68
|
-
const identical = existing ? fileMapsEqual(existing, next) : false;
|
|
69
|
-
if (identical) {
|
|
70
|
-
plan.push({ target, previous: "unchanged" });
|
|
71
|
-
continue;
|
|
21
|
+
const contentHash = contentHashForFiles(normalizedFiles);
|
|
22
|
+
const existingHash = await readExistingTreeHash(destination).catch((error) => {
|
|
23
|
+
const code = error.code;
|
|
24
|
+
if (code === "ENOENT") {
|
|
25
|
+
return null;
|
|
72
26
|
}
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
27
|
+
if (code === "ENOTDIR") {
|
|
28
|
+
const conflictPath = error.path ?? destination;
|
|
29
|
+
throw new WorkbenchCodedError("install_failed", `Canonical skill path is blocked by an existing file: ${conflictPath}`, {
|
|
30
|
+
remediation: `Remove the conflicting file ${conflictPath} and rerun the install.`,
|
|
31
|
+
subject: { destination, conflictPath },
|
|
77
32
|
exitCode: 1,
|
|
78
33
|
});
|
|
79
34
|
}
|
|
80
|
-
|
|
35
|
+
throw error;
|
|
36
|
+
});
|
|
37
|
+
const previous = existingHash
|
|
38
|
+
? existingHash === contentHash ? "unchanged" : "overwritten"
|
|
39
|
+
: "none";
|
|
40
|
+
if (existingHash && previous !== "unchanged" && !options.overwrite) {
|
|
41
|
+
throw new WorkbenchCodedError("install_failed", `Canonical skill already exists: ${destination}`, {
|
|
42
|
+
remediation: "Pass --yes to overwrite the existing canonical store skill.",
|
|
43
|
+
subject: { destination },
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
});
|
|
81
46
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
await writeSnapshotFiles(entry.target.destination, normalizedFiles);
|
|
93
|
-
}
|
|
47
|
+
if (!options.dryRun && previous !== "unchanged") {
|
|
48
|
+
await fs.rm(destination, { recursive: true, force: true });
|
|
49
|
+
await writeSnapshotFiles(destination, normalizedFiles);
|
|
50
|
+
}
|
|
51
|
+
if (!options.dryRun) {
|
|
52
|
+
await writeProvenanceRecord(canonicalSkillsStore(), skillName, {
|
|
53
|
+
...options.provenance,
|
|
54
|
+
installedAt: options.installedAt ?? new Date().toISOString(),
|
|
55
|
+
contentHash,
|
|
56
|
+
});
|
|
94
57
|
}
|
|
95
58
|
return {
|
|
96
|
-
result:
|
|
97
|
-
|
|
98
|
-
|
|
59
|
+
result: previous === "unchanged" ? "unchanged" : options.dryRun ? "planned" : "installed",
|
|
60
|
+
store: "machine",
|
|
61
|
+
directoryName: skillName,
|
|
62
|
+
destination,
|
|
63
|
+
previous,
|
|
64
|
+
filesCopied: previous === "unchanged" ? 0 : normalizedFiles.length,
|
|
65
|
+
contentHash,
|
|
66
|
+
provenancePath: provenancePathForStore(),
|
|
99
67
|
};
|
|
100
68
|
}
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
69
|
+
export function canonicalSkillDirectoryName(snapshot) {
|
|
70
|
+
const skillFile = snapshot.files.find((file) => normalizeInstallSnapshotPath(file.path) === "SKILL.md");
|
|
71
|
+
if (skillFile && skillFile.encoding !== "base64") {
|
|
72
|
+
const metadata = parseSkillMetadata(skillFile.content);
|
|
73
|
+
if (metadata.name && isValidStoreDirectoryName(metadata.name)) {
|
|
74
|
+
return metadata.name;
|
|
75
|
+
}
|
|
104
76
|
}
|
|
105
|
-
return
|
|
77
|
+
return installStoreSkillDirectoryName(snapshot.name);
|
|
78
|
+
}
|
|
79
|
+
function isValidStoreDirectoryName(value) {
|
|
80
|
+
return value.length > 0 &&
|
|
81
|
+
!value.includes("\0") &&
|
|
82
|
+
!value.includes("/") &&
|
|
83
|
+
!value.includes("\\") &&
|
|
84
|
+
value !== "." &&
|
|
85
|
+
value !== "..";
|
|
106
86
|
}
|
|
107
|
-
function
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
87
|
+
export async function readInstalledSkillsInventory(options = {}) {
|
|
88
|
+
const stores = await installedSkillStores(options.cwd ?? process.cwd());
|
|
89
|
+
const skills = [];
|
|
90
|
+
for (const store of stores) {
|
|
91
|
+
skills.push(...await readStoreInventory(store, options));
|
|
111
92
|
}
|
|
112
|
-
|
|
93
|
+
skills.sort((left, right) => left.name.localeCompare(right.name) || left.store.localeCompare(right.store) || left.path.localeCompare(right.path));
|
|
94
|
+
const next = skills.find((skill) => skill.status === "update" && skill.handle)?.handle;
|
|
95
|
+
return {
|
|
96
|
+
stores,
|
|
97
|
+
skills,
|
|
98
|
+
next: next ? `workbench install ${next}` : skills.length === 0 ? "workbench install OWNER/SKILL" : null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function installedInventoryToJson(inventory) {
|
|
102
|
+
return {
|
|
103
|
+
stores: inventory.stores.map((store) => ({
|
|
104
|
+
kind: store.kind,
|
|
105
|
+
path: store.path,
|
|
106
|
+
})),
|
|
107
|
+
skills: inventory.skills.map((skill) => ({
|
|
108
|
+
store: skill.store,
|
|
109
|
+
storeRoot: skill.storeRoot,
|
|
110
|
+
name: skill.name,
|
|
111
|
+
directoryName: skill.directoryName,
|
|
112
|
+
...(skill.description ? { description: skill.description } : {}),
|
|
113
|
+
path: skill.path,
|
|
114
|
+
status: skill.status,
|
|
115
|
+
contentHash: skill.contentHash,
|
|
116
|
+
...(skill.versionId ? { versionId: skill.versionId } : {}),
|
|
117
|
+
...(skill.latestVersionId ? { latestVersionId: skill.latestVersionId } : {}),
|
|
118
|
+
...(skill.handle ? { handle: skill.handle } : {}),
|
|
119
|
+
...(skill.baseUrl ? { baseUrl: skill.baseUrl } : {}),
|
|
120
|
+
...(skill.installedAt ? { installedAt: skill.installedAt } : {}),
|
|
121
|
+
})),
|
|
122
|
+
next: inventory.next,
|
|
123
|
+
};
|
|
113
124
|
}
|
|
114
125
|
export function normalizeInstallSnapshotPath(filePath) {
|
|
115
126
|
const normalized = filePath.replace(/\\/gu, "/");
|
|
@@ -128,11 +139,218 @@ export function normalizeInstallSnapshotPath(filePath) {
|
|
|
128
139
|
}
|
|
129
140
|
return normalized;
|
|
130
141
|
}
|
|
142
|
+
async function installedSkillStores(cwd) {
|
|
143
|
+
const machine = canonicalSkillsStore();
|
|
144
|
+
const local = path.join(cwd, ".agents", "skills");
|
|
145
|
+
if (await sameFilesystemPath(local, machine) || !await directoryExists(local)) {
|
|
146
|
+
return [{ kind: "machine", path: machine }];
|
|
147
|
+
}
|
|
148
|
+
return [
|
|
149
|
+
{ kind: "machine", path: machine },
|
|
150
|
+
{ kind: "local", path: local },
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
async function directoryExists(filePath) {
|
|
154
|
+
return await fs.stat(filePath).then((stat) => stat.isDirectory(), () => false);
|
|
155
|
+
}
|
|
156
|
+
async function sameFilesystemPath(left, right) {
|
|
157
|
+
const [leftResolved, rightResolved] = await Promise.all([
|
|
158
|
+
realpathOrResolved(left),
|
|
159
|
+
realpathOrResolved(right),
|
|
160
|
+
]);
|
|
161
|
+
return leftResolved === rightResolved;
|
|
162
|
+
}
|
|
163
|
+
async function realpathOrResolved(filePath) {
|
|
164
|
+
try {
|
|
165
|
+
return await fs.realpath(filePath);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
try {
|
|
169
|
+
return path.join(await fs.realpath(path.dirname(filePath)), path.basename(filePath));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return path.resolve(filePath);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function readStoreInventory(store, options) {
|
|
177
|
+
const provenance = await readProvenanceFile(store.path);
|
|
178
|
+
let entries;
|
|
179
|
+
try {
|
|
180
|
+
entries = await fs.readdir(store.path, { withFileTypes: true });
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const code = error.code;
|
|
184
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
const rows = [];
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const skillPath = path.join(store.path, entry.name);
|
|
195
|
+
const skillFile = path.join(skillPath, "SKILL.md");
|
|
196
|
+
const skillMarkdown = await fs.readFile(skillFile, "utf8").catch((error) => {
|
|
197
|
+
if (error.code === "ENOENT") {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
});
|
|
202
|
+
if (skillMarkdown === null) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const metadata = parseSkillMetadata(skillMarkdown);
|
|
206
|
+
const contentHash = await readExistingTreeHash(skillPath);
|
|
207
|
+
const record = validProvenanceRecord(provenance.skills[entry.name]);
|
|
208
|
+
const base = {
|
|
209
|
+
store: store.kind,
|
|
210
|
+
storeRoot: store.path,
|
|
211
|
+
name: metadata.name ?? entry.name,
|
|
212
|
+
directoryName: entry.name,
|
|
213
|
+
...(metadata.description ? { description: metadata.description } : {}),
|
|
214
|
+
path: skillPath,
|
|
215
|
+
contentHash,
|
|
216
|
+
status: "unmanaged",
|
|
217
|
+
};
|
|
218
|
+
if (!record) {
|
|
219
|
+
rows.push(base);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
let status = contentHash === record.contentHash ? "current" : "modified";
|
|
223
|
+
let latestVersionId;
|
|
224
|
+
if (status === "current" && options.includeUpdates === true && options.lookupLatestVersion) {
|
|
225
|
+
latestVersionId = await options.lookupLatestVersion(record).catch(() => undefined);
|
|
226
|
+
if (latestVersionId && latestVersionId !== record.versionId) {
|
|
227
|
+
status = "update";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
rows.push({
|
|
231
|
+
...base,
|
|
232
|
+
status,
|
|
233
|
+
versionId: record.versionId,
|
|
234
|
+
handle: record.handle,
|
|
235
|
+
baseUrl: record.baseUrl,
|
|
236
|
+
installedAt: record.installedAt,
|
|
237
|
+
...(latestVersionId ? { latestVersionId } : {}),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return rows;
|
|
241
|
+
}
|
|
242
|
+
function parseSkillMetadata(markdown) {
|
|
243
|
+
if (!markdown.startsWith("---\n") && !markdown.startsWith("---\r\n")) {
|
|
244
|
+
return {};
|
|
245
|
+
}
|
|
246
|
+
const newline = markdown.startsWith("---\r\n") ? "\r\n" : "\n";
|
|
247
|
+
const closing = markdown.indexOf(`${newline}---${newline}`, 3);
|
|
248
|
+
if (closing === -1) {
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
const source = markdown.slice(3 + newline.length, closing);
|
|
252
|
+
try {
|
|
253
|
+
const parsed = YAML.parse(source);
|
|
254
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
const record = parsed;
|
|
258
|
+
return {
|
|
259
|
+
...(typeof record.name === "string" && record.name.trim() ? { name: record.name.trim() } : {}),
|
|
260
|
+
...(typeof record.description === "string" && record.description.trim() ? { description: record.description.trim() } : {}),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return {};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function validProvenanceRecord(value) {
|
|
268
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const record = value;
|
|
272
|
+
const handle = typeof record.handle === "string" ? record.handle : "";
|
|
273
|
+
const versionId = typeof record.versionId === "string" ? record.versionId : "";
|
|
274
|
+
const baseUrl = typeof record.baseUrl === "string" ? record.baseUrl : "";
|
|
275
|
+
const installedAt = typeof record.installedAt === "string" ? record.installedAt : "";
|
|
276
|
+
const contentHash = typeof record.contentHash === "string" ? record.contentHash : "";
|
|
277
|
+
return handle && versionId && baseUrl && installedAt && contentHash
|
|
278
|
+
? { handle, versionId, baseUrl, installedAt, contentHash }
|
|
279
|
+
: null;
|
|
280
|
+
}
|
|
281
|
+
async function writeProvenanceRecord(storeRoot, skillName, record) {
|
|
282
|
+
const provenance = await readProvenanceFile(storeRoot);
|
|
283
|
+
for (const key of Object.keys(provenance.skills)) {
|
|
284
|
+
if (key !== skillName && !await pathExists(path.join(storeRoot, key))) {
|
|
285
|
+
delete provenance.skills[key];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
provenance.skills[skillName] = record;
|
|
289
|
+
await fs.mkdir(storeRoot, { recursive: true });
|
|
290
|
+
const filePath = provenancePathForStore(storeRoot);
|
|
291
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
292
|
+
await fs.writeFile(tempPath, `${JSON.stringify(provenance, null, 2)}\n`);
|
|
293
|
+
await fs.rename(tempPath, filePath);
|
|
294
|
+
}
|
|
295
|
+
async function readProvenanceFile(storeRoot) {
|
|
296
|
+
const filePath = provenancePathForStore(storeRoot);
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
299
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
300
|
+
return emptyProvenance();
|
|
301
|
+
}
|
|
302
|
+
const record = parsed;
|
|
303
|
+
if (record.schema !== PROVENANCE_SCHEMA || !record.skills || typeof record.skills !== "object" || Array.isArray(record.skills)) {
|
|
304
|
+
return emptyProvenance();
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
schema: PROVENANCE_SCHEMA,
|
|
308
|
+
skills: Object.fromEntries(Object.entries(record.skills).flatMap(([key, value]) => {
|
|
309
|
+
const valid = validProvenanceRecord(value);
|
|
310
|
+
return valid ? [[key, valid]] : [];
|
|
311
|
+
})),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (error.code === "ENOENT") {
|
|
316
|
+
return emptyProvenance();
|
|
317
|
+
}
|
|
318
|
+
return emptyProvenance();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function emptyProvenance() {
|
|
322
|
+
return {
|
|
323
|
+
schema: PROVENANCE_SCHEMA,
|
|
324
|
+
skills: {},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function installStoreSkillDirectoryName(skillName) {
|
|
328
|
+
const normalized = skillName.trim();
|
|
329
|
+
if (!normalized ||
|
|
330
|
+
normalized.includes("\0") ||
|
|
331
|
+
normalized.includes("/") ||
|
|
332
|
+
normalized.includes("\\") ||
|
|
333
|
+
normalized === "." ||
|
|
334
|
+
normalized === "..") {
|
|
335
|
+
throw new WorkbenchCodedError("install_failed", `Invalid skill name for install store: ${skillName}`, {
|
|
336
|
+
subject: { skillName },
|
|
337
|
+
exitCode: 1,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return normalized;
|
|
341
|
+
}
|
|
342
|
+
function contentHashForFiles(files) {
|
|
343
|
+
return hashJson(files.map((file) => ({
|
|
344
|
+
path: normalizeInstallSnapshotPath(file.path),
|
|
345
|
+
executable: file.executable === true,
|
|
346
|
+
contentBase64: installFileContent(file).toString("base64"),
|
|
347
|
+
})).sort((left, right) => left.path.localeCompare(right.path)));
|
|
348
|
+
}
|
|
131
349
|
function installFileContent(file) {
|
|
132
350
|
if (file.encoding === "base64") {
|
|
133
351
|
return Buffer.from(file.content, "base64");
|
|
134
352
|
}
|
|
135
|
-
return file.content;
|
|
353
|
+
return Buffer.from(file.content);
|
|
136
354
|
}
|
|
137
355
|
async function writeSnapshotFiles(root, files) {
|
|
138
356
|
for (const file of files) {
|
|
@@ -144,45 +362,42 @@ async function writeSnapshotFiles(root, files) {
|
|
|
144
362
|
}
|
|
145
363
|
}
|
|
146
364
|
}
|
|
147
|
-
async function
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
365
|
+
async function readExistingTreeHash(root) {
|
|
366
|
+
const files = new Map();
|
|
367
|
+
const executable = new Map();
|
|
368
|
+
await readExistingTreeInto(root, "", files, executable);
|
|
369
|
+
return hashJson([...files.entries()].map(([filePath, content]) => ({
|
|
370
|
+
path: filePath,
|
|
371
|
+
executable: executable.get(filePath) === true,
|
|
372
|
+
contentBase64: content.toString("base64"),
|
|
373
|
+
})).sort((left, right) => left.path.localeCompare(right.path)));
|
|
151
374
|
}
|
|
152
|
-
async function readExistingTreeInto(root, relativeDir, result) {
|
|
375
|
+
async function readExistingTreeInto(root, relativeDir, result, executable) {
|
|
153
376
|
const dir = path.join(root, relativeDir);
|
|
154
377
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
155
378
|
for (const entry of entries) {
|
|
156
379
|
const relativePath = relativeDir ? path.join(relativeDir, entry.name) : entry.name;
|
|
157
380
|
const fullPath = path.join(root, relativePath);
|
|
158
381
|
if (entry.isDirectory()) {
|
|
159
|
-
await readExistingTreeInto(root, relativePath, result);
|
|
382
|
+
await readExistingTreeInto(root, relativePath, result, executable);
|
|
160
383
|
}
|
|
161
384
|
else if (entry.isFile()) {
|
|
162
|
-
|
|
385
|
+
const stat = await fs.stat(fullPath);
|
|
386
|
+
const normalized = relativePath.replace(/\\/gu, "/");
|
|
387
|
+
result.set(normalized, await fs.readFile(fullPath));
|
|
388
|
+
executable.set(normalized, (stat.mode & 0o111) !== 0);
|
|
163
389
|
}
|
|
164
390
|
}
|
|
165
391
|
}
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
392
|
+
async function pathExists(filePath) {
|
|
393
|
+
try {
|
|
394
|
+
await fs.access(filePath);
|
|
395
|
+
return true;
|
|
169
396
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (!leftValue) {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
const rightBuffer = Buffer.isBuffer(rightValue) ? rightValue : Buffer.from(rightValue);
|
|
176
|
-
if (!leftValue.equals(rightBuffer)) {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
397
|
+
catch {
|
|
398
|
+
return false;
|
|
179
399
|
}
|
|
180
|
-
return true;
|
|
181
400
|
}
|
|
182
|
-
|
|
183
|
-
return
|
|
184
|
-
agent: target.agent,
|
|
185
|
-
mode: target.mode,
|
|
186
|
-
destination: target.destination,
|
|
187
|
-
}));
|
|
401
|
+
function homeDirectory() {
|
|
402
|
+
return process.env.HOME?.trim() || os.homedir();
|
|
188
403
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workbench-ai/workbench",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.75",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/workbench-ai/workbench.git",
|
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"skills": "1.5.11",
|
|
23
24
|
"yaml": "^2.8.2",
|
|
24
|
-
"@workbench-ai/workbench-built-in-adapters": "0.0.
|
|
25
|
-
"@workbench-ai/workbench-
|
|
26
|
-
"@workbench-ai/workbench-
|
|
27
|
-
"@workbench-ai/workbench-
|
|
25
|
+
"@workbench-ai/workbench-built-in-adapters": "0.0.75",
|
|
26
|
+
"@workbench-ai/workbench-contract": "0.0.75",
|
|
27
|
+
"@workbench-ai/workbench-core": "0.0.75",
|
|
28
|
+
"@workbench-ai/workbench-protocol": "0.0.75"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@tailwindcss/postcss": "^4.2.2",
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"react-dom": "^19.2.0",
|
|
36
37
|
"typescript": "^5.9.2",
|
|
37
38
|
"vitest": "^3.2.4",
|
|
38
|
-
"@workbench-ai/workbench-ui": "0.0.
|
|
39
|
+
"@workbench-ai/workbench-ui": "0.0.75"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"build": "rm -rf dist && tsc -p tsconfig.json && chmod 755 dist/workbench.js && node ./scripts/build-dev-open-assets.mjs",
|