wowbagger 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/LICENSE +201 -0
- package/README.md +464 -0
- package/adapters/claude-code/entrypoint.js +19 -0
- package/adapters/claude-code/wowbagger-adapter.json +25 -0
- package/adapters/codex/entrypoint.js +11 -0
- package/adapters/codex/wowbagger-adapter.json +25 -0
- package/adapters/opencode/entrypoint.js +11 -0
- package/adapters/opencode/wowbagger-adapter.json +25 -0
- package/bin/wowbagger.js +7 -0
- package/package.json +51 -0
- package/skills/wowbagger/SKILL.md +136 -0
- package/src/adapter/approval.js +135 -0
- package/src/adapter/bootstrap.js +43 -0
- package/src/adapter/context.js +34 -0
- package/src/adapter/core-probe.js +231 -0
- package/src/adapter/describe.js +383 -0
- package/src/adapter/entrypoint-main.js +335 -0
- package/src/adapter/entrypoint-path.js +103 -0
- package/src/adapter/handoff.js +124 -0
- package/src/adapter/instructions.js +106 -0
- package/src/adapter/invoke.js +294 -0
- package/src/adapter/limits.js +26 -0
- package/src/adapter/manifest.js +93 -0
- package/src/adapter/messages.js +15 -0
- package/src/adapter/paths.js +88 -0
- package/src/adapter/process-outcome.js +1116 -0
- package/src/adapter/schema-helpers.js +60 -0
- package/src/claim-capabilities.js +54 -0
- package/src/claim-coordinator.js +85 -0
- package/src/claim-journal.js +236 -0
- package/src/claim-operations.js +138 -0
- package/src/claim-publication.js +739 -0
- package/src/claim-request.js +140 -0
- package/src/claim-store.js +198 -0
- package/src/cli.js +1130 -0
- package/src/dependencies.js +3 -0
- package/src/git-reconciliation.js +62 -0
- package/src/ledger.js +296 -0
- package/src/mint.js +32 -0
- package/src/mutation.js +1979 -0
- package/src/namespace.js +35 -0
- package/src/ready.js +85 -0
- package/src/request.js +246 -0
- package/src/schema-migration.js +300 -0
- package/src/validate.js +1208 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const MAX_GIT_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
8
|
+
const GIT_ENVIRONMENT = Object.fromEntries(
|
|
9
|
+
Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_')),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export async function readGitHeadLedger(ledgerDirectory) {
|
|
13
|
+
const root = (await gitText(ledgerDirectory, ['rev-parse', '--show-toplevel'])).trim();
|
|
14
|
+
const relativeLedger = path.relative(root, await realpath(ledgerDirectory));
|
|
15
|
+
if (relativeLedger === '' || relativeLedger.startsWith(`..${path.sep}`) || path.isAbsolute(relativeLedger)) {
|
|
16
|
+
throw new Error(`ledger is outside the git worktree: ${relativeLedger}`);
|
|
17
|
+
}
|
|
18
|
+
let commit;
|
|
19
|
+
try {
|
|
20
|
+
commit = (await gitText(root, ['rev-parse', '--verify', 'HEAD'])).trim();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error?.code === 128) return { commit: null, items: new Map(), root };
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
const gitLedger = toGitPath(relativeLedger);
|
|
26
|
+
const listing = await gitBuffer(root, [
|
|
27
|
+
'ls-tree', '-r', '-z', '--name-only', 'HEAD', '--', gitLedger,
|
|
28
|
+
]);
|
|
29
|
+
const prefix = `${gitLedger}/`;
|
|
30
|
+
const files = listing.toString('utf8').split('\0')
|
|
31
|
+
.filter((name) => name.startsWith(prefix) && name.endsWith('.md'));
|
|
32
|
+
const items = new Map();
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const bytes = await gitBuffer(root, ['show', `HEAD:${file}`]);
|
|
35
|
+
items.set(file.slice(prefix.length), bytes);
|
|
36
|
+
}
|
|
37
|
+
return { commit, items, root };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function gitText(cwd, argumentsList) {
|
|
41
|
+
const { stdout } = await execFileAsync('git', argumentsList, {
|
|
42
|
+
cwd,
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
45
|
+
env: GIT_ENVIRONMENT,
|
|
46
|
+
});
|
|
47
|
+
return stdout;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function gitBuffer(cwd, argumentsList) {
|
|
51
|
+
const { stdout } = await execFileAsync('git', argumentsList, {
|
|
52
|
+
cwd,
|
|
53
|
+
encoding: 'buffer',
|
|
54
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
55
|
+
env: GIT_ENVIRONMENT,
|
|
56
|
+
});
|
|
57
|
+
return stdout;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toGitPath(value) {
|
|
61
|
+
return value.split(path.sep).join('/');
|
|
62
|
+
}
|
package/src/ledger.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open, readdir } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { TextDecoder } from 'node:util';
|
|
5
|
+
import { parseDocument } from 'yaml';
|
|
6
|
+
|
|
7
|
+
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
|
|
8
|
+
const DEFAULT_FILE_SYSTEM = { lstat, open, readdir };
|
|
9
|
+
|
|
10
|
+
export async function loadLedger(ledgerDirectory, fileSystem = DEFAULT_FILE_SYSTEM) {
|
|
11
|
+
const root = path.resolve(ledgerDirectory);
|
|
12
|
+
let rootStat;
|
|
13
|
+
try {
|
|
14
|
+
rootStat = await fileSystem.lstat(root);
|
|
15
|
+
} catch {
|
|
16
|
+
return {
|
|
17
|
+
items: [],
|
|
18
|
+
errors: [ledgerReadError(ledgerPath(root, root))],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (rootStat.isSymbolicLink()) {
|
|
23
|
+
return {
|
|
24
|
+
items: [],
|
|
25
|
+
errors: [symlinkError(ledgerPath(root, root))],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!rootStat.isDirectory()) {
|
|
30
|
+
return {
|
|
31
|
+
items: [],
|
|
32
|
+
errors: [rootNotDirectoryError(ledgerPath(root, root))],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const collected = await collectMarkdownFiles(root, root, fileSystem);
|
|
37
|
+
const items = [];
|
|
38
|
+
const errors = [...collected.errors];
|
|
39
|
+
|
|
40
|
+
for (const file of collected.files) {
|
|
41
|
+
const displayPath = ledgerPath(root, file);
|
|
42
|
+
let source;
|
|
43
|
+
let bytes;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const handle = await fileSystem.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
47
|
+
try {
|
|
48
|
+
const fileStat = await handle.stat();
|
|
49
|
+
if (!fileStat.isFile()) {
|
|
50
|
+
errors.push(ledgerReadError(displayPath));
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
bytes = await handle.readFile();
|
|
54
|
+
try {
|
|
55
|
+
source = UTF8_DECODER.decode(bytes);
|
|
56
|
+
} catch {
|
|
57
|
+
errors.push(invalidUtf8Error(displayPath));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
await handle.close();
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
errors.push(error?.code === 'ELOOP' ? symlinkError(displayPath) : ledgerReadError(displayPath));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const parsed = parseLedgerItemSource(source);
|
|
69
|
+
|
|
70
|
+
if (parsed.error) {
|
|
71
|
+
errors.push({ path: displayPath, ...parsed.error });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
items.push({
|
|
76
|
+
path: displayPath,
|
|
77
|
+
file,
|
|
78
|
+
bytes,
|
|
79
|
+
source,
|
|
80
|
+
body: parsed.body,
|
|
81
|
+
data: parsed.data,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { items, errors };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function bodyFromSource(source) {
|
|
89
|
+
let index = 0;
|
|
90
|
+
let lineNumber = 0;
|
|
91
|
+
|
|
92
|
+
while (index <= source.length) {
|
|
93
|
+
const lineEnd = source.indexOf('\n', index);
|
|
94
|
+
const nextIndex = lineEnd === -1 ? source.length : lineEnd + 1;
|
|
95
|
+
const rawLine = source.slice(index, lineEnd === -1 ? source.length : lineEnd);
|
|
96
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
97
|
+
|
|
98
|
+
if (lineNumber > 0 && line === '---') {
|
|
99
|
+
return source.slice(nextIndex);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (nextIndex === source.length) {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
index = nextIndex;
|
|
106
|
+
lineNumber += 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function collectMarkdownFiles(root, directory, fileSystem) {
|
|
113
|
+
let directoryStat;
|
|
114
|
+
try {
|
|
115
|
+
directoryStat = await fileSystem.lstat(directory);
|
|
116
|
+
} catch {
|
|
117
|
+
return {
|
|
118
|
+
files: [],
|
|
119
|
+
errors: [ledgerReadError(ledgerPath(root, directory))],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) {
|
|
124
|
+
return {
|
|
125
|
+
files: [],
|
|
126
|
+
errors: [symlinkError(ledgerPath(root, directory))],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let entries;
|
|
131
|
+
try {
|
|
132
|
+
entries = await fileSystem.readdir(directory, { withFileTypes: true });
|
|
133
|
+
} catch {
|
|
134
|
+
return {
|
|
135
|
+
files: [],
|
|
136
|
+
errors: [ledgerReadError(ledgerPath(root, directory))],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const files = [];
|
|
140
|
+
const errors = [];
|
|
141
|
+
|
|
142
|
+
for (const entry of entries.sort((left, right) => compareText(left.name, right.name))) {
|
|
143
|
+
const entryPath = path.join(directory, entry.name);
|
|
144
|
+
let entryType = entry;
|
|
145
|
+
|
|
146
|
+
if (!entry.isSymbolicLink() && !entry.isDirectory() && !entry.isFile()) {
|
|
147
|
+
try {
|
|
148
|
+
entryType = await fileSystem.lstat(entryPath);
|
|
149
|
+
} catch {
|
|
150
|
+
errors.push(ledgerReadError(ledgerPath(root, entryPath)));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (entryType.isSymbolicLink()) {
|
|
156
|
+
errors.push(symlinkError(ledgerPath(root, entryPath)));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (entryType.isDirectory()) {
|
|
161
|
+
const nested = await collectMarkdownFiles(root, entryPath, fileSystem);
|
|
162
|
+
files.push(...nested.files);
|
|
163
|
+
errors.push(...nested.errors);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (entryType.isFile() && entry.name.endsWith('.md')) {
|
|
168
|
+
files.push(entryPath);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (entry.name.endsWith('.md')) {
|
|
173
|
+
errors.push(ledgerReadError(ledgerPath(root, entryPath)));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { files, errors };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function compareText(left, right) {
|
|
181
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function symlinkError(displayPath) {
|
|
185
|
+
return {
|
|
186
|
+
path: displayPath,
|
|
187
|
+
field: 'path',
|
|
188
|
+
code: 'symlink-not-allowed',
|
|
189
|
+
message: 'Ledger entries must not be symbolic links.',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ledgerReadError(displayPath) {
|
|
194
|
+
return {
|
|
195
|
+
path: displayPath,
|
|
196
|
+
field: 'path',
|
|
197
|
+
code: 'ledger-read-error',
|
|
198
|
+
message: 'Ledger path could not be read.',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function rootNotDirectoryError(displayPath) {
|
|
203
|
+
return {
|
|
204
|
+
path: displayPath,
|
|
205
|
+
field: 'path',
|
|
206
|
+
code: 'ledger-root-not-directory',
|
|
207
|
+
message: 'Ledger root must be a real directory.',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function invalidUtf8Error(displayPath) {
|
|
212
|
+
return {
|
|
213
|
+
path: displayPath,
|
|
214
|
+
field: 'encoding',
|
|
215
|
+
code: 'invalid-utf8',
|
|
216
|
+
message: 'Ledger items must be valid UTF-8.',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function ledgerPath(root, file) {
|
|
221
|
+
const relative = path.relative(root, file).split(path.sep).join('/');
|
|
222
|
+
return relative ? `${path.basename(root)}/${relative}` : path.basename(root);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function parseLedgerItemSource(source) {
|
|
226
|
+
const frontmatter = extractFrontmatter(source);
|
|
227
|
+
|
|
228
|
+
if (frontmatter === null) {
|
|
229
|
+
return {
|
|
230
|
+
error: {
|
|
231
|
+
field: 'frontmatter',
|
|
232
|
+
code: 'malformed-frontmatter',
|
|
233
|
+
message: 'Item must begin with one YAML frontmatter document delimited by --- lines.',
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const document = parseDocument(frontmatter, {
|
|
239
|
+
prettyErrors: false,
|
|
240
|
+
schema: 'core',
|
|
241
|
+
uniqueKeys: true,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (document.errors.length > 0) {
|
|
245
|
+
const error = document.errors[0];
|
|
246
|
+
return {
|
|
247
|
+
error: {
|
|
248
|
+
field: 'frontmatter',
|
|
249
|
+
code: error.code === 'DUPLICATE_KEY' ? 'duplicate-yaml-key' : 'invalid-yaml',
|
|
250
|
+
message: error.code === 'DUPLICATE_KEY'
|
|
251
|
+
? 'YAML mapping keys must be unique.'
|
|
252
|
+
: 'Frontmatter contains invalid YAML.',
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let data;
|
|
258
|
+
try {
|
|
259
|
+
data = document.toJS();
|
|
260
|
+
} catch {
|
|
261
|
+
return {
|
|
262
|
+
error: {
|
|
263
|
+
field: 'frontmatter',
|
|
264
|
+
code: 'invalid-yaml',
|
|
265
|
+
message: 'Frontmatter contains invalid YAML.',
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (data === null || Array.isArray(data) || typeof data !== 'object') {
|
|
271
|
+
return {
|
|
272
|
+
error: {
|
|
273
|
+
field: 'frontmatter',
|
|
274
|
+
code: 'invalid-frontmatter-type',
|
|
275
|
+
message: 'Frontmatter must be a YAML mapping.',
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { data, body: bodyFromSource(source) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function extractFrontmatter(source) {
|
|
284
|
+
const lines = source.split(/\r?\n/);
|
|
285
|
+
|
|
286
|
+
if (lines[0] !== '---') {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const closeIndex = lines.indexOf('---', 1);
|
|
291
|
+
if (closeIndex === -1) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return lines.slice(1, closeIndex).join('\n');
|
|
296
|
+
}
|
package/src/mint.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const ULID_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
4
|
+
|
|
5
|
+
// Mints a canonical Wowbagger item ID: a 48-bit millisecond timestamp and 80
|
|
6
|
+
// bits of collision-resistant entropy, Crockford base32, `wb_` prefix. The
|
|
7
|
+
// encoded instant becomes the item's created date, so a caller supplying a
|
|
8
|
+
// calendar date gets that day's first UTC instant.
|
|
9
|
+
export function mintId(date = null) {
|
|
10
|
+
const instant = date === null ? Date.now() : Date.parse(`${date}T00:00:00.000Z`);
|
|
11
|
+
return `wb_${encodeTimestamp(instant)}${encodeEntropy()}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function encodeTimestamp(milliseconds) {
|
|
15
|
+
let remaining = milliseconds;
|
|
16
|
+
let encoded = '';
|
|
17
|
+
for (let index = 0; index < 10; index += 1) {
|
|
18
|
+
encoded = ULID_ALPHABET[remaining % 32] + encoded;
|
|
19
|
+
remaining = Math.floor(remaining / 32);
|
|
20
|
+
}
|
|
21
|
+
return encoded;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function encodeEntropy() {
|
|
25
|
+
let value = BigInt(`0x${randomBytes(10).toString('hex')}`);
|
|
26
|
+
let encoded = '';
|
|
27
|
+
for (let index = 0; index < 16; index += 1) {
|
|
28
|
+
encoded = ULID_ALPHABET[Number(value & 31n)] + encoded;
|
|
29
|
+
value >>= 5n;
|
|
30
|
+
}
|
|
31
|
+
return encoded;
|
|
32
|
+
}
|