writbase 0.1.0 → 0.1.2
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/writbase.js +307 -146
- package/package.json +3 -2
- package/skills/manager/SKILL.md +138 -0
- package/skills/manager/references/agent-provisioning.md +251 -0
- package/skills/manager/references/permission-model.md +156 -0
- package/skills/worker/SKILL.md +130 -0
- package/skills/worker/references/error-handling.md +201 -0
package/dist/writbase.js
CHANGED
|
@@ -4,17 +4,18 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
|
-
import { webcrypto } from "crypto";
|
|
7
|
+
import { webcrypto as webcrypto2 } from "crypto";
|
|
8
8
|
import { execSync } from "child_process";
|
|
9
|
-
import { existsSync } from "fs";
|
|
10
|
-
import { join as
|
|
9
|
+
import { existsSync as existsSync2 } from "fs";
|
|
10
|
+
import { join as join3 } from "path";
|
|
11
11
|
import { confirm, input, select } from "@inquirer/prompts";
|
|
12
12
|
import { createSpinner } from "nanospinner";
|
|
13
13
|
|
|
14
14
|
// src/lib/config.ts
|
|
15
15
|
import dotenv from "dotenv";
|
|
16
|
-
import { writeFileSync, renameSync } from "fs";
|
|
16
|
+
import { mkdirSync, writeFileSync, renameSync } from "fs";
|
|
17
17
|
import { join } from "path";
|
|
18
|
+
import { homedir } from "os";
|
|
18
19
|
|
|
19
20
|
// src/lib/output.ts
|
|
20
21
|
import pc from "picocolors";
|
|
@@ -44,8 +45,9 @@ function table(headers, rows) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
// src/lib/config.ts
|
|
48
|
+
var WRITBASE_HOME = join(homedir(), ".writbase");
|
|
47
49
|
function loadConfigPartial() {
|
|
48
|
-
dotenv.config();
|
|
50
|
+
dotenv.config({ path: join(WRITBASE_HOME, ".env") });
|
|
49
51
|
return {
|
|
50
52
|
supabaseUrl: process.env.SUPABASE_URL,
|
|
51
53
|
supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
@@ -68,8 +70,9 @@ function loadConfig() {
|
|
|
68
70
|
return partial;
|
|
69
71
|
}
|
|
70
72
|
function writeEnv(vars) {
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
+
mkdirSync(WRITBASE_HOME, { recursive: true });
|
|
74
|
+
const envPath = join(WRITBASE_HOME, ".env");
|
|
75
|
+
const tmpPath = join(WRITBASE_HOME, ".env.tmp");
|
|
73
76
|
const content = Object.entries(vars).map(([key2, value]) => `${key2}=${value}`).join("\n") + "\n";
|
|
74
77
|
writeFileSync(tmpPath, content, { mode: 384 });
|
|
75
78
|
renameSync(tmpPath, envPath);
|
|
@@ -86,15 +89,221 @@ function createAdminClient(url, key2) {
|
|
|
86
89
|
});
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
// src/lib/agent-keys.ts
|
|
93
|
+
import { webcrypto } from "crypto";
|
|
94
|
+
|
|
95
|
+
// src/lib/event-log.ts
|
|
96
|
+
async function logEvent(supabase, params) {
|
|
97
|
+
const { error: error2 } = await supabase.from("event_log").insert({
|
|
98
|
+
event_category: params.eventCategory,
|
|
99
|
+
target_type: params.targetType,
|
|
100
|
+
target_id: params.targetId,
|
|
101
|
+
event_type: params.eventType,
|
|
102
|
+
field_name: params.fieldName ?? null,
|
|
103
|
+
old_value: params.oldValue ?? null,
|
|
104
|
+
new_value: params.newValue ?? null,
|
|
105
|
+
actor_type: params.actorType,
|
|
106
|
+
actor_id: params.actorId,
|
|
107
|
+
actor_label: params.actorLabel,
|
|
108
|
+
source: params.source,
|
|
109
|
+
workspace_id: params.workspaceId
|
|
110
|
+
});
|
|
111
|
+
if (error2) {
|
|
112
|
+
console.error(
|
|
113
|
+
"Failed to log event:",
|
|
114
|
+
JSON.stringify({
|
|
115
|
+
event_type: params.eventType,
|
|
116
|
+
target_id: params.targetId,
|
|
117
|
+
error: error2.message
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/lib/agent-keys.ts
|
|
124
|
+
async function hashSecret(secret) {
|
|
125
|
+
const data = new TextEncoder().encode(secret);
|
|
126
|
+
const hashBuffer = await webcrypto.subtle.digest("SHA-256", data);
|
|
127
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
128
|
+
}
|
|
129
|
+
async function generateKey(keyId) {
|
|
130
|
+
const randomBytes = new Uint8Array(32);
|
|
131
|
+
webcrypto.getRandomValues(randomBytes);
|
|
132
|
+
const secret = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
133
|
+
const hash = await hashSecret(secret);
|
|
134
|
+
const prefix = secret.slice(0, 8);
|
|
135
|
+
const fullKey = `wb_${keyId}_${secret}`;
|
|
136
|
+
return { fullKey, prefix, hash };
|
|
137
|
+
}
|
|
138
|
+
var KEY_COLUMNS = "id, name, role, key_prefix, is_active, special_prompt, created_at, last_used_at, created_by, project_id, department_id";
|
|
139
|
+
async function listAgentKeys(supabase, workspaceId) {
|
|
140
|
+
const { data, error: error2 } = await supabase.from("agent_keys").select(KEY_COLUMNS).eq("workspace_id", workspaceId).order("created_at", { ascending: false });
|
|
141
|
+
if (error2) throw error2;
|
|
142
|
+
return data;
|
|
143
|
+
}
|
|
144
|
+
async function createAgentKey(supabase, params) {
|
|
145
|
+
const keyId = webcrypto.randomUUID();
|
|
146
|
+
const { fullKey, prefix, hash } = await generateKey(keyId);
|
|
147
|
+
const { data, error: error2 } = await supabase.from("agent_keys").insert({
|
|
148
|
+
id: keyId,
|
|
149
|
+
name: params.name,
|
|
150
|
+
role: params.role ?? "worker",
|
|
151
|
+
key_hash: hash,
|
|
152
|
+
key_prefix: prefix,
|
|
153
|
+
special_prompt: null,
|
|
154
|
+
created_by: "writbase-cli",
|
|
155
|
+
workspace_id: params.workspaceId,
|
|
156
|
+
project_id: params.projectId ?? null,
|
|
157
|
+
department_id: params.departmentId ?? null
|
|
158
|
+
}).select(KEY_COLUMNS).single();
|
|
159
|
+
if (error2) throw error2;
|
|
160
|
+
await logEvent(supabase, {
|
|
161
|
+
eventCategory: "admin",
|
|
162
|
+
targetType: "agent_key",
|
|
163
|
+
targetId: data.id,
|
|
164
|
+
eventType: "agent_key.created",
|
|
165
|
+
actorType: "system",
|
|
166
|
+
actorId: "writbase-cli",
|
|
167
|
+
actorLabel: "writbase-cli",
|
|
168
|
+
source: "system",
|
|
169
|
+
workspaceId: params.workspaceId
|
|
170
|
+
});
|
|
171
|
+
return { key: data, fullKey };
|
|
172
|
+
}
|
|
173
|
+
async function rotateAgentKey(supabase, params) {
|
|
174
|
+
const { fullKey, prefix, hash } = await generateKey(params.id);
|
|
175
|
+
const { data, error: error2 } = await supabase.from("agent_keys").update({ key_hash: hash, key_prefix: prefix }).eq("id", params.id).eq("workspace_id", params.workspaceId).select(KEY_COLUMNS).single();
|
|
176
|
+
if (error2) throw error2;
|
|
177
|
+
await logEvent(supabase, {
|
|
178
|
+
eventCategory: "admin",
|
|
179
|
+
targetType: "agent_key",
|
|
180
|
+
targetId: params.id,
|
|
181
|
+
eventType: "agent_key.rotated",
|
|
182
|
+
actorType: "system",
|
|
183
|
+
actorId: "writbase-cli",
|
|
184
|
+
actorLabel: "writbase-cli",
|
|
185
|
+
source: "system",
|
|
186
|
+
workspaceId: params.workspaceId
|
|
187
|
+
});
|
|
188
|
+
return { key: data, fullKey };
|
|
189
|
+
}
|
|
190
|
+
async function deactivateAgentKey(supabase, params) {
|
|
191
|
+
const { data, error: error2 } = await supabase.from("agent_keys").update({ is_active: false }).eq("id", params.id).eq("workspace_id", params.workspaceId).select(KEY_COLUMNS).single();
|
|
192
|
+
if (error2) throw error2;
|
|
193
|
+
await logEvent(supabase, {
|
|
194
|
+
eventCategory: "admin",
|
|
195
|
+
targetType: "agent_key",
|
|
196
|
+
targetId: params.id,
|
|
197
|
+
eventType: "agent_key.deactivated",
|
|
198
|
+
actorType: "system",
|
|
199
|
+
actorId: "writbase-cli",
|
|
200
|
+
actorLabel: "writbase-cli",
|
|
201
|
+
source: "system",
|
|
202
|
+
workspaceId: params.workspaceId
|
|
203
|
+
});
|
|
204
|
+
return data;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/lib/claude-code.ts
|
|
208
|
+
import { homedir as homedir2 } from "os";
|
|
209
|
+
import { join as join2 } from "path";
|
|
210
|
+
import { fileURLToPath } from "url";
|
|
211
|
+
import { existsSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync, renameSync as renameSync2, cpSync } from "fs";
|
|
212
|
+
function generatePluginJson() {
|
|
213
|
+
return JSON.stringify(
|
|
214
|
+
{
|
|
215
|
+
name: "writbase",
|
|
216
|
+
description: "WritBase \u2014 agent-first task management. Skills for MCP tool usage, permissions, and error handling.",
|
|
217
|
+
version: "0.1.2"
|
|
218
|
+
},
|
|
219
|
+
null,
|
|
220
|
+
2
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
function generateMcpJson(mcpUrl, agentKey) {
|
|
224
|
+
return JSON.stringify(
|
|
225
|
+
{
|
|
226
|
+
writbase: {
|
|
227
|
+
type: "http",
|
|
228
|
+
url: mcpUrl,
|
|
229
|
+
headers: {
|
|
230
|
+
Authorization: `Bearer ${agentKey}`
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
null,
|
|
235
|
+
2
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
function atomicWriteJson(filePath, content) {
|
|
239
|
+
const tmpPath = filePath + ".tmp";
|
|
240
|
+
writeFileSync2(tmpPath, content);
|
|
241
|
+
renameSync2(tmpPath, filePath);
|
|
242
|
+
}
|
|
243
|
+
function installPlugin(config) {
|
|
244
|
+
const claudeDir = join2(homedir2(), ".claude");
|
|
245
|
+
mkdirSync2(join2(WRITBASE_HOME, ".claude-plugin"), { recursive: true });
|
|
246
|
+
mkdirSync2(join2(WRITBASE_HOME, "skills"), { recursive: true });
|
|
247
|
+
const packageRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
248
|
+
const skillsSource = join2(packageRoot, "skills");
|
|
249
|
+
cpSync(skillsSource, join2(WRITBASE_HOME, "skills"), { recursive: true });
|
|
250
|
+
writeFileSync2(join2(WRITBASE_HOME, ".claude-plugin", "plugin.json"), generatePluginJson());
|
|
251
|
+
const mcpContent = generateMcpJson(config.mcpUrl, config.agentKey);
|
|
252
|
+
const mcpPath = join2(WRITBASE_HOME, ".mcp.json");
|
|
253
|
+
const mcpTmpPath = join2(WRITBASE_HOME, ".mcp.json.tmp");
|
|
254
|
+
writeFileSync2(mcpTmpPath, mcpContent, { mode: 384 });
|
|
255
|
+
renameSync2(mcpTmpPath, mcpPath);
|
|
256
|
+
const pluginsDir = join2(claudeDir, "plugins");
|
|
257
|
+
mkdirSync2(pluginsDir, { recursive: true });
|
|
258
|
+
const marketplacesPath = join2(pluginsDir, "known_marketplaces.json");
|
|
259
|
+
const marketplaces = existsSync(marketplacesPath) ? JSON.parse(readFileSync(marketplacesPath, "utf-8")) : {};
|
|
260
|
+
marketplaces["writbase"] = {
|
|
261
|
+
source: { source: "directory", path: WRITBASE_HOME },
|
|
262
|
+
autoUpdate: false
|
|
263
|
+
};
|
|
264
|
+
atomicWriteJson(marketplacesPath, JSON.stringify(marketplaces, null, 2));
|
|
265
|
+
const settingsPath = join2(claudeDir, "settings.json");
|
|
266
|
+
const settings = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, "utf-8")) : {};
|
|
267
|
+
if (!settings.enabledPlugins) {
|
|
268
|
+
settings.enabledPlugins = {};
|
|
269
|
+
}
|
|
270
|
+
settings.enabledPlugins["writbase@writbase"] = true;
|
|
271
|
+
atomicWriteJson(settingsPath, JSON.stringify(settings, null, 2));
|
|
272
|
+
}
|
|
273
|
+
async function grantBasicPermissions(supabase, params) {
|
|
274
|
+
const perms = {
|
|
275
|
+
agent_key_id: params.keyId,
|
|
276
|
+
project_id: params.projectId,
|
|
277
|
+
workspace_id: params.workspaceId,
|
|
278
|
+
can_read: true,
|
|
279
|
+
can_create: true,
|
|
280
|
+
can_update: true,
|
|
281
|
+
can_assign: params.role === "manager"
|
|
282
|
+
};
|
|
283
|
+
const { error: error2 } = await supabase.from("agent_permissions").insert(perms);
|
|
284
|
+
if (error2) throw error2;
|
|
285
|
+
await logEvent(supabase, {
|
|
286
|
+
eventCategory: "admin",
|
|
287
|
+
targetType: "agent_key",
|
|
288
|
+
targetId: params.keyId,
|
|
289
|
+
eventType: "agent_permission.granted",
|
|
290
|
+
actorType: "system",
|
|
291
|
+
actorId: "writbase-cli",
|
|
292
|
+
actorLabel: "writbase-cli",
|
|
293
|
+
source: "system",
|
|
294
|
+
workspaceId: params.workspaceId
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
89
298
|
// src/commands/init.ts
|
|
90
299
|
async function initCommand() {
|
|
91
300
|
console.log();
|
|
92
301
|
info("WritBase \u2014 Interactive Setup");
|
|
93
302
|
console.log();
|
|
94
|
-
const envPath =
|
|
95
|
-
if (
|
|
303
|
+
const envPath = join3(WRITBASE_HOME, ".env");
|
|
304
|
+
if (existsSync2(envPath)) {
|
|
96
305
|
const overwrite = await confirm({
|
|
97
|
-
message:
|
|
306
|
+
message: `${WRITBASE_HOME} already configured. Reconfigure?`,
|
|
98
307
|
default: false
|
|
99
308
|
});
|
|
100
309
|
if (!overwrite) return;
|
|
@@ -207,7 +416,7 @@ async function initCommand() {
|
|
|
207
416
|
SUPABASE_SERVICE_ROLE_KEY: serviceRoleKey,
|
|
208
417
|
DATABASE_URL: databaseUrl
|
|
209
418
|
});
|
|
210
|
-
success(
|
|
419
|
+
success(`Config written to ${WRITBASE_HOME} (partial \u2014 no workspace yet)`);
|
|
211
420
|
console.log();
|
|
212
421
|
info("Next steps:");
|
|
213
422
|
console.log(" 1. writbase migrate");
|
|
@@ -236,7 +445,7 @@ async function initCommand() {
|
|
|
236
445
|
try {
|
|
237
446
|
const { data: user, error: userError } = await supabase.auth.admin.createUser({
|
|
238
447
|
email: "system@writbase.local",
|
|
239
|
-
password:
|
|
448
|
+
password: webcrypto2.randomUUID(),
|
|
240
449
|
email_confirm: true
|
|
241
450
|
});
|
|
242
451
|
let userId;
|
|
@@ -279,25 +488,94 @@ async function initCommand() {
|
|
|
279
488
|
WRITBASE_WORKSPACE_ID: workspaceId
|
|
280
489
|
});
|
|
281
490
|
console.log();
|
|
282
|
-
success(
|
|
491
|
+
success(`Config written to ${WRITBASE_HOME}`);
|
|
492
|
+
console.log();
|
|
493
|
+
const setupClaude = await confirm({
|
|
494
|
+
message: "Set up Claude Code integration? (installs skills + MCP config globally)",
|
|
495
|
+
default: true
|
|
496
|
+
});
|
|
497
|
+
if (setupClaude) {
|
|
498
|
+
const keyName = await input({
|
|
499
|
+
message: "Agent key name:",
|
|
500
|
+
default: "claude-code"
|
|
501
|
+
});
|
|
502
|
+
const keyRole = await select({
|
|
503
|
+
message: "Agent role:",
|
|
504
|
+
choices: [
|
|
505
|
+
{ name: "worker", value: "worker" },
|
|
506
|
+
{ name: "manager", value: "manager" }
|
|
507
|
+
],
|
|
508
|
+
default: "worker"
|
|
509
|
+
});
|
|
510
|
+
const { data: projects } = await supabase.from("projects").select("id, name, slug").eq("workspace_id", workspaceId).eq("is_archived", false).order("name");
|
|
511
|
+
let projectId = null;
|
|
512
|
+
if (projects && projects.length > 0) {
|
|
513
|
+
if (projects.length === 1) {
|
|
514
|
+
projectId = projects[0].id;
|
|
515
|
+
info(`Using project: ${projects[0].name} (${projects[0].slug})`);
|
|
516
|
+
} else {
|
|
517
|
+
projectId = await select({
|
|
518
|
+
message: "Grant permissions for project:",
|
|
519
|
+
choices: projects.map((p) => ({
|
|
520
|
+
name: `${p.name} (${p.slug})`,
|
|
521
|
+
value: p.id
|
|
522
|
+
}))
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const mcpUrl = await input({
|
|
527
|
+
message: "MCP endpoint URL:",
|
|
528
|
+
default: `${supabaseUrl}/functions/v1/mcp-server`
|
|
529
|
+
});
|
|
530
|
+
const keySpinner = createSpinner("Creating agent key...").start();
|
|
531
|
+
try {
|
|
532
|
+
const { key: key2, fullKey } = await createAgentKey(supabase, {
|
|
533
|
+
name: keyName.trim() || "claude-code",
|
|
534
|
+
role: keyRole,
|
|
535
|
+
workspaceId,
|
|
536
|
+
projectId
|
|
537
|
+
});
|
|
538
|
+
keySpinner.success({ text: `Agent key created: ${key2.name} (${key2.role})` });
|
|
539
|
+
if (projectId) {
|
|
540
|
+
await grantBasicPermissions(supabase, {
|
|
541
|
+
keyId: key2.id,
|
|
542
|
+
projectId,
|
|
543
|
+
workspaceId,
|
|
544
|
+
role: keyRole
|
|
545
|
+
});
|
|
546
|
+
success("Permissions granted");
|
|
547
|
+
}
|
|
548
|
+
const pluginSpinner = createSpinner("Installing Claude Code plugin...").start();
|
|
549
|
+
installPlugin({ mcpUrl, agentKey: fullKey });
|
|
550
|
+
pluginSpinner.success({ text: "Claude Code plugin installed" });
|
|
551
|
+
console.log();
|
|
552
|
+
warn("Save this agent key \u2014 it cannot be retrieved later:");
|
|
553
|
+
console.log();
|
|
554
|
+
console.log(` ${fullKey}`);
|
|
555
|
+
console.log();
|
|
556
|
+
success("Claude Code integration complete. Restart Claude Code to activate.");
|
|
557
|
+
} catch (err) {
|
|
558
|
+
keySpinner.error({ text: "Claude Code setup failed" });
|
|
559
|
+
error(err instanceof Error ? err.message : String(err));
|
|
560
|
+
console.log();
|
|
561
|
+
info("You can set up Claude Code later with `writbase key create`");
|
|
562
|
+
}
|
|
563
|
+
}
|
|
283
564
|
console.log();
|
|
284
565
|
info("Next steps:");
|
|
285
|
-
if (!
|
|
286
|
-
console.log(" 1. writbase migrate");
|
|
287
|
-
console.log(" 2. writbase key create");
|
|
288
|
-
} else {
|
|
566
|
+
if (!setupClaude) {
|
|
289
567
|
console.log(" 1. writbase key create");
|
|
290
568
|
}
|
|
291
|
-
console.log("
|
|
569
|
+
console.log(" writbase status (verify connection)");
|
|
292
570
|
console.log();
|
|
293
571
|
}
|
|
294
572
|
|
|
295
573
|
// src/commands/migrate.ts
|
|
296
574
|
import { execSync as execSync2 } from "child_process";
|
|
297
|
-
import { mkdtempSync, mkdirSync, cpSync, writeFileSync as
|
|
298
|
-
import { join as
|
|
575
|
+
import { mkdtempSync, mkdirSync as mkdirSync3, cpSync as cpSync2, writeFileSync as writeFileSync3, rmSync } from "fs";
|
|
576
|
+
import { join as join4 } from "path";
|
|
299
577
|
import { tmpdir } from "os";
|
|
300
|
-
import { fileURLToPath } from "url";
|
|
578
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
301
579
|
import { createSpinner as createSpinner2 } from "nanospinner";
|
|
302
580
|
var CONFIG_TOML = `project_id = "writbase"
|
|
303
581
|
|
|
@@ -319,15 +597,15 @@ async function migrateCommand(opts) {
|
|
|
319
597
|
error("Supabase CLI not found. Install it: https://supabase.com/docs/guides/cli");
|
|
320
598
|
process.exit(1);
|
|
321
599
|
}
|
|
322
|
-
const packageRoot =
|
|
323
|
-
const migrationsSource =
|
|
600
|
+
const packageRoot = fileURLToPath2(new URL("..", import.meta.url));
|
|
601
|
+
const migrationsSource = join4(packageRoot, "migrations");
|
|
324
602
|
const spinner = createSpinner2("Running migrations...").start();
|
|
325
|
-
const tmpDir = mkdtempSync(
|
|
603
|
+
const tmpDir = mkdtempSync(join4(tmpdir(), "writbase-migrate-"));
|
|
326
604
|
try {
|
|
327
|
-
const supabaseDir =
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
605
|
+
const supabaseDir = join4(tmpDir, "supabase");
|
|
606
|
+
mkdirSync3(supabaseDir);
|
|
607
|
+
writeFileSync3(join4(supabaseDir, "config.toml"), CONFIG_TOML);
|
|
608
|
+
cpSync2(migrationsSource, join4(supabaseDir, "migrations"), { recursive: true });
|
|
331
609
|
const args = [`--db-url "${config.databaseUrl}"`, `--workdir ${tmpDir}`];
|
|
332
610
|
if (opts.dryRun) args.push("--dry-run");
|
|
333
611
|
const cmd = `supabase migration up ${args.join(" ")}`;
|
|
@@ -360,123 +638,6 @@ async function migrateCommand(opts) {
|
|
|
360
638
|
// src/commands/key.ts
|
|
361
639
|
import { confirm as confirm2, input as input2, select as select2 } from "@inquirer/prompts";
|
|
362
640
|
import { createSpinner as createSpinner3 } from "nanospinner";
|
|
363
|
-
|
|
364
|
-
// src/lib/agent-keys.ts
|
|
365
|
-
import { webcrypto as webcrypto2 } from "crypto";
|
|
366
|
-
|
|
367
|
-
// src/lib/event-log.ts
|
|
368
|
-
async function logEvent(supabase, params) {
|
|
369
|
-
const { error: error2 } = await supabase.from("event_log").insert({
|
|
370
|
-
event_category: params.eventCategory,
|
|
371
|
-
target_type: params.targetType,
|
|
372
|
-
target_id: params.targetId,
|
|
373
|
-
event_type: params.eventType,
|
|
374
|
-
field_name: params.fieldName ?? null,
|
|
375
|
-
old_value: params.oldValue ?? null,
|
|
376
|
-
new_value: params.newValue ?? null,
|
|
377
|
-
actor_type: params.actorType,
|
|
378
|
-
actor_id: params.actorId,
|
|
379
|
-
actor_label: params.actorLabel,
|
|
380
|
-
source: params.source,
|
|
381
|
-
workspace_id: params.workspaceId
|
|
382
|
-
});
|
|
383
|
-
if (error2) {
|
|
384
|
-
console.error(
|
|
385
|
-
"Failed to log event:",
|
|
386
|
-
JSON.stringify({
|
|
387
|
-
event_type: params.eventType,
|
|
388
|
-
target_id: params.targetId,
|
|
389
|
-
error: error2.message
|
|
390
|
-
})
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// src/lib/agent-keys.ts
|
|
396
|
-
async function hashSecret(secret) {
|
|
397
|
-
const data = new TextEncoder().encode(secret);
|
|
398
|
-
const hashBuffer = await webcrypto2.subtle.digest("SHA-256", data);
|
|
399
|
-
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
400
|
-
}
|
|
401
|
-
async function generateKey(keyId) {
|
|
402
|
-
const randomBytes = new Uint8Array(32);
|
|
403
|
-
webcrypto2.getRandomValues(randomBytes);
|
|
404
|
-
const secret = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
405
|
-
const hash = await hashSecret(secret);
|
|
406
|
-
const prefix = secret.slice(0, 8);
|
|
407
|
-
const fullKey = `wb_${keyId}_${secret}`;
|
|
408
|
-
return { fullKey, prefix, hash };
|
|
409
|
-
}
|
|
410
|
-
var KEY_COLUMNS = "id, name, role, key_prefix, is_active, special_prompt, created_at, last_used_at, created_by, project_id, department_id";
|
|
411
|
-
async function listAgentKeys(supabase, workspaceId) {
|
|
412
|
-
const { data, error: error2 } = await supabase.from("agent_keys").select(KEY_COLUMNS).eq("workspace_id", workspaceId).order("created_at", { ascending: false });
|
|
413
|
-
if (error2) throw error2;
|
|
414
|
-
return data;
|
|
415
|
-
}
|
|
416
|
-
async function createAgentKey(supabase, params) {
|
|
417
|
-
const keyId = webcrypto2.randomUUID();
|
|
418
|
-
const { fullKey, prefix, hash } = await generateKey(keyId);
|
|
419
|
-
const { data, error: error2 } = await supabase.from("agent_keys").insert({
|
|
420
|
-
id: keyId,
|
|
421
|
-
name: params.name,
|
|
422
|
-
role: params.role ?? "worker",
|
|
423
|
-
key_hash: hash,
|
|
424
|
-
key_prefix: prefix,
|
|
425
|
-
special_prompt: null,
|
|
426
|
-
created_by: "writbase-cli",
|
|
427
|
-
workspace_id: params.workspaceId,
|
|
428
|
-
project_id: params.projectId ?? null,
|
|
429
|
-
department_id: params.departmentId ?? null
|
|
430
|
-
}).select(KEY_COLUMNS).single();
|
|
431
|
-
if (error2) throw error2;
|
|
432
|
-
await logEvent(supabase, {
|
|
433
|
-
eventCategory: "admin",
|
|
434
|
-
targetType: "agent_key",
|
|
435
|
-
targetId: data.id,
|
|
436
|
-
eventType: "agent_key.created",
|
|
437
|
-
actorType: "system",
|
|
438
|
-
actorId: "writbase-cli",
|
|
439
|
-
actorLabel: "writbase-cli",
|
|
440
|
-
source: "system",
|
|
441
|
-
workspaceId: params.workspaceId
|
|
442
|
-
});
|
|
443
|
-
return { key: data, fullKey };
|
|
444
|
-
}
|
|
445
|
-
async function rotateAgentKey(supabase, params) {
|
|
446
|
-
const { fullKey, prefix, hash } = await generateKey(params.id);
|
|
447
|
-
const { data, error: error2 } = await supabase.from("agent_keys").update({ key_hash: hash, key_prefix: prefix }).eq("id", params.id).eq("workspace_id", params.workspaceId).select(KEY_COLUMNS).single();
|
|
448
|
-
if (error2) throw error2;
|
|
449
|
-
await logEvent(supabase, {
|
|
450
|
-
eventCategory: "admin",
|
|
451
|
-
targetType: "agent_key",
|
|
452
|
-
targetId: params.id,
|
|
453
|
-
eventType: "agent_key.rotated",
|
|
454
|
-
actorType: "system",
|
|
455
|
-
actorId: "writbase-cli",
|
|
456
|
-
actorLabel: "writbase-cli",
|
|
457
|
-
source: "system",
|
|
458
|
-
workspaceId: params.workspaceId
|
|
459
|
-
});
|
|
460
|
-
return { key: data, fullKey };
|
|
461
|
-
}
|
|
462
|
-
async function deactivateAgentKey(supabase, params) {
|
|
463
|
-
const { data, error: error2 } = await supabase.from("agent_keys").update({ is_active: false }).eq("id", params.id).eq("workspace_id", params.workspaceId).select(KEY_COLUMNS).single();
|
|
464
|
-
if (error2) throw error2;
|
|
465
|
-
await logEvent(supabase, {
|
|
466
|
-
eventCategory: "admin",
|
|
467
|
-
targetType: "agent_key",
|
|
468
|
-
targetId: params.id,
|
|
469
|
-
eventType: "agent_key.deactivated",
|
|
470
|
-
actorType: "system",
|
|
471
|
-
actorId: "writbase-cli",
|
|
472
|
-
actorLabel: "writbase-cli",
|
|
473
|
-
source: "system",
|
|
474
|
-
workspaceId: params.workspaceId
|
|
475
|
-
});
|
|
476
|
-
return data;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// src/commands/key.ts
|
|
480
641
|
async function resolveKeyByNameOrId(supabase, workspaceId, nameOrId) {
|
|
481
642
|
const keys = await listAgentKeys(supabase, workspaceId);
|
|
482
643
|
const byName = keys.find(
|
|
@@ -695,7 +856,7 @@ async function statusCommand() {
|
|
|
695
856
|
|
|
696
857
|
// src/bin/writbase.ts
|
|
697
858
|
var program = new Command();
|
|
698
|
-
program.name("writbase").description("WritBase CLI \u2014 agent-first task management").version("0.1.
|
|
859
|
+
program.name("writbase").description("WritBase CLI \u2014 agent-first task management").version("0.1.2");
|
|
699
860
|
program.command("init").description("Interactive setup \u2014 configure credentials and workspace").action(initCommand);
|
|
700
861
|
program.command("migrate").description("Apply database migrations via supabase migration up").option("--dry-run", "Show what would be applied without making changes").action(migrateCommand);
|
|
701
862
|
var key = program.command("key").description("Manage agent keys");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "writbase",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WritBase CLI — self-hosted operator toolkit for agent-first task management",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/",
|
|
11
|
-
"migrations/"
|
|
11
|
+
"migrations/",
|
|
12
|
+
"skills/"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
14
15
|
"build": "tsup src/bin/writbase.ts --format esm --target node18",
|