@dench.com/cli 0.2.0 → 0.3.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/README.md +2 -2
- package/dench.ts +1 -1
- package/fs-daemon.ts +74 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,13 +7,13 @@ Agent-facing CLI for the Dench agent workspace.
|
|
|
7
7
|
Run without installing:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx -y dench
|
|
10
|
+
npx -y @dench.com/cli login --name "AI Agent - Billing Repo"
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Or install globally:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install -g dench
|
|
16
|
+
npm install -g @dench.com/cli
|
|
17
17
|
dench login --name "AI Agent - Billing Repo"
|
|
18
18
|
```
|
|
19
19
|
|
package/dench.ts
CHANGED
package/fs-daemon.ts
CHANGED
|
@@ -27,19 +27,21 @@
|
|
|
27
27
|
*
|
|
28
28
|
* Usage (daemon mode):
|
|
29
29
|
* dench-fs-daemon \
|
|
30
|
-
* --org-id <orgId> \
|
|
31
30
|
* --workspace /workspace \
|
|
32
31
|
* --hot-cache /tmp/dench-cache
|
|
32
|
+
*
|
|
33
|
+
* `--org-id` is optional: when omitted, the daemon resolves the organization
|
|
34
|
+
* from DENCH_API_KEY via Convex before it starts watching files.
|
|
33
35
|
*/
|
|
34
36
|
import { createHash } from "node:crypto";
|
|
35
37
|
import { mkdirSync } from "node:fs";
|
|
36
|
-
import { mkdir, readFile, stat,
|
|
38
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
37
39
|
import { dirname, relative } from "node:path";
|
|
38
40
|
import { ConvexClient, ConvexHttpClient } from "convex/browser";
|
|
39
41
|
import { makeFunctionReference } from "convex/server";
|
|
40
42
|
|
|
41
43
|
type Args = {
|
|
42
|
-
orgId
|
|
44
|
+
orgId?: string;
|
|
43
45
|
workspace: string;
|
|
44
46
|
hotCache: string;
|
|
45
47
|
};
|
|
@@ -67,11 +69,10 @@ function parseArgs(argv: string[]): Args {
|
|
|
67
69
|
break;
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
);
|
|
74
|
-
}
|
|
72
|
+
out.orgId ??= process.env.DENCH_ORG_ID?.trim() || undefined;
|
|
73
|
+
out.workspace ??= process.env.DENCH_VOLUME_PATH?.trim() || "/workspace";
|
|
74
|
+
out.hotCache ??=
|
|
75
|
+
process.env.DENCH_HOT_CACHE_PATH?.trim() || "/tmp/dench-cache";
|
|
75
76
|
return out as Args;
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -144,7 +145,9 @@ class HashTracker {
|
|
|
144
145
|
setTimeout(() => {
|
|
145
146
|
this.paused.delete(absPath);
|
|
146
147
|
this.recentSyncs.delete(absPath);
|
|
147
|
-
console.warn(
|
|
148
|
+
console.warn(
|
|
149
|
+
`[dench-fs-daemon] circuit breaker released for ${absPath}`,
|
|
150
|
+
);
|
|
148
151
|
}, 60_000);
|
|
149
152
|
return true;
|
|
150
153
|
}
|
|
@@ -177,10 +180,11 @@ const api = {
|
|
|
177
180
|
commitFileUpload: makeFunctionReference<"mutation">(
|
|
178
181
|
"functions/files:commitFileUpload",
|
|
179
182
|
),
|
|
180
|
-
deleteFile: makeFunctionReference<"mutation">(
|
|
181
|
-
"functions/files:deleteFile",
|
|
182
|
-
),
|
|
183
|
+
deleteFile: makeFunctionReference<"mutation">("functions/files:deleteFile"),
|
|
183
184
|
listTree: makeFunctionReference<"query">("functions/files:listTree"),
|
|
185
|
+
resolveApiKeyOrganization: makeFunctionReference<"query">(
|
|
186
|
+
"functions/files:resolveApiKeyOrganization",
|
|
187
|
+
),
|
|
184
188
|
appendLiveBuffer: makeFunctionReference<"mutation">(
|
|
185
189
|
"functions/files:appendLiveBuffer",
|
|
186
190
|
),
|
|
@@ -197,17 +201,24 @@ class DenchFileClient {
|
|
|
197
201
|
private http: ConvexHttpClient;
|
|
198
202
|
private realtime: ConvexClient | null = null;
|
|
199
203
|
|
|
200
|
-
constructor(
|
|
204
|
+
constructor(
|
|
205
|
+
convexUrl: string,
|
|
206
|
+
private readonly apiKey: string,
|
|
207
|
+
) {
|
|
201
208
|
this.http = new ConvexHttpClient(convexUrl);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.http.
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async resolveOrganizationId(): Promise<string> {
|
|
212
|
+
const result = (await this.http.query(api.files.resolveApiKeyOrganization, {
|
|
213
|
+
apiKey: this.apiKey,
|
|
214
|
+
})) as { organizationId: string };
|
|
215
|
+
return result.organizationId;
|
|
206
216
|
}
|
|
207
217
|
|
|
208
218
|
async generateUploadUrl(path: string): Promise<string> {
|
|
209
219
|
return (await this.http.mutation(api.files.generateUploadUrl, {
|
|
210
220
|
path,
|
|
221
|
+
apiKey: this.apiKey,
|
|
211
222
|
})) as string;
|
|
212
223
|
}
|
|
213
224
|
|
|
@@ -238,7 +249,10 @@ class DenchFileClient {
|
|
|
238
249
|
lastModifiedBy: string;
|
|
239
250
|
}): Promise<{ versionConflict: boolean; currentHash?: string }> {
|
|
240
251
|
try {
|
|
241
|
-
await this.http.mutation(api.files.commitFileUpload,
|
|
252
|
+
await this.http.mutation(api.files.commitFileUpload, {
|
|
253
|
+
...args,
|
|
254
|
+
apiKey: this.apiKey,
|
|
255
|
+
});
|
|
242
256
|
return { versionConflict: false };
|
|
243
257
|
} catch (error) {
|
|
244
258
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -248,7 +262,7 @@ class DenchFileClient {
|
|
|
248
262
|
) {
|
|
249
263
|
// Convex serializes ConvexError data into the message; pull
|
|
250
264
|
// currentHash if present.
|
|
251
|
-
const m = /currentHash[
|
|
265
|
+
const m = /currentHash["']?:["']?([a-f0-9]{64})/.exec(message);
|
|
252
266
|
return { versionConflict: true, currentHash: m?.[1] };
|
|
253
267
|
}
|
|
254
268
|
throw error;
|
|
@@ -256,14 +270,17 @@ class DenchFileClient {
|
|
|
256
270
|
}
|
|
257
271
|
|
|
258
272
|
async deleteFile(path: string): Promise<void> {
|
|
259
|
-
await this.http.mutation(api.files.deleteFile, {
|
|
273
|
+
await this.http.mutation(api.files.deleteFile, {
|
|
274
|
+
path,
|
|
275
|
+
apiKey: this.apiKey,
|
|
276
|
+
});
|
|
260
277
|
}
|
|
261
278
|
|
|
262
279
|
async getSignedDownloadUrl(path: string): Promise<string | null> {
|
|
263
|
-
const url = (await this.http.action(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
)) as string | null;
|
|
280
|
+
const url = (await this.http.action(api.files.getFileSignedDownloadUrl, {
|
|
281
|
+
path,
|
|
282
|
+
apiKey: this.apiKey,
|
|
283
|
+
})) as string | null;
|
|
267
284
|
return url ?? null;
|
|
268
285
|
}
|
|
269
286
|
|
|
@@ -272,11 +289,17 @@ class DenchFileClient {
|
|
|
272
289
|
runId: string;
|
|
273
290
|
chunk: string;
|
|
274
291
|
}): Promise<void> {
|
|
275
|
-
await this.http.mutation(api.files.appendLiveBuffer,
|
|
292
|
+
await this.http.mutation(api.files.appendLiveBuffer, {
|
|
293
|
+
...args,
|
|
294
|
+
apiKey: this.apiKey,
|
|
295
|
+
} as never);
|
|
276
296
|
}
|
|
277
297
|
|
|
278
298
|
async finalizeLiveBuffer(path: string): Promise<void> {
|
|
279
|
-
await this.http.mutation(api.files.finalizeLiveBuffer, {
|
|
299
|
+
await this.http.mutation(api.files.finalizeLiveBuffer, {
|
|
300
|
+
path,
|
|
301
|
+
apiKey: this.apiKey,
|
|
302
|
+
});
|
|
280
303
|
}
|
|
281
304
|
|
|
282
305
|
/**
|
|
@@ -295,7 +318,7 @@ class DenchFileClient {
|
|
|
295
318
|
}
|
|
296
319
|
const unsubscribe = this.realtime.onUpdate(
|
|
297
320
|
api.files.listTree,
|
|
298
|
-
{ prefix: "/" },
|
|
321
|
+
{ prefix: "/", apiKey: this.apiKey },
|
|
299
322
|
(rows: unknown) => {
|
|
300
323
|
const list = Array.isArray(rows)
|
|
301
324
|
? (rows as Array<{
|
|
@@ -347,14 +370,16 @@ async function runDaemon(daemonArgs: Args): Promise<void> {
|
|
|
347
370
|
return;
|
|
348
371
|
}
|
|
349
372
|
|
|
373
|
+
const client = new DenchFileClient(convexUrl, apiKey);
|
|
374
|
+
const resolvedOrgId =
|
|
375
|
+
daemonArgs.orgId ?? (await client.resolveOrganizationId());
|
|
350
376
|
console.log(
|
|
351
|
-
`[dench-fs-daemon] starting org=${
|
|
377
|
+
`[dench-fs-daemon] starting org=${resolvedOrgId} workspace=${daemonArgs.workspace}`,
|
|
352
378
|
);
|
|
353
379
|
mkdirSync(daemonArgs.workspace, { recursive: true });
|
|
354
380
|
mkdirSync(daemonArgs.hotCache, { recursive: true });
|
|
355
381
|
|
|
356
382
|
const tracker = new HashTracker();
|
|
357
|
-
const client = new DenchFileClient(convexUrl, apiKey);
|
|
358
383
|
|
|
359
384
|
let chokidar: typeof import("chokidar");
|
|
360
385
|
try {
|
|
@@ -518,8 +543,7 @@ async function runStreamOpen(args: string[]): Promise<void> {
|
|
|
518
543
|
if (!path) {
|
|
519
544
|
throw new Error("dench-fs-daemon stream-open requires <path>");
|
|
520
545
|
}
|
|
521
|
-
const runId =
|
|
522
|
-
process.env.DENCH_RUN_ID?.trim() ?? `manual-${Date.now()}`;
|
|
546
|
+
const runId = process.env.DENCH_RUN_ID?.trim() ?? `manual-${Date.now()}`;
|
|
523
547
|
const token = newToken();
|
|
524
548
|
const store = await readTokenStore();
|
|
525
549
|
store[token] = { path, runId };
|
|
@@ -531,9 +555,7 @@ async function runStreamAppend(args: string[]): Promise<void> {
|
|
|
531
555
|
const token = args[0];
|
|
532
556
|
const chunk = args.slice(1).join(" ");
|
|
533
557
|
if (!token || chunk === undefined) {
|
|
534
|
-
throw new Error(
|
|
535
|
-
"dench-fs-daemon stream-append requires <token> <chunk>",
|
|
536
|
-
);
|
|
558
|
+
throw new Error("dench-fs-daemon stream-append requires <token> <chunk>");
|
|
537
559
|
}
|
|
538
560
|
const store = await readTokenStore();
|
|
539
561
|
const entry = store[token];
|
|
@@ -606,21 +628,31 @@ async function main(): Promise<void> {
|
|
|
606
628
|
*/
|
|
607
629
|
async function runInitialSync(argv: string[]): Promise<void> {
|
|
608
630
|
const initial = argv.includes("--initial");
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
|
|
631
|
+
const orgIdIndex = argv.indexOf("--org-id");
|
|
632
|
+
const workspaceIndex = argv.indexOf("--workspace");
|
|
633
|
+
const orgId =
|
|
634
|
+
orgIdIndex === -1
|
|
635
|
+
? (process.env.DENCH_ORG_ID?.trim() ?? undefined)
|
|
636
|
+
: argv[orgIdIndex + 1];
|
|
637
|
+
const workspace =
|
|
638
|
+
workspaceIndex === -1
|
|
639
|
+
? "/workspace"
|
|
640
|
+
: (argv[workspaceIndex + 1] ?? "/workspace");
|
|
641
|
+
if (!initial) {
|
|
612
642
|
throw new Error(
|
|
613
|
-
"dench fs sync --initial --org-id <orgId> [--workspace /workspace]",
|
|
643
|
+
"dench fs sync --initial [--org-id <orgId>] [--workspace /workspace]",
|
|
614
644
|
);
|
|
615
645
|
}
|
|
616
646
|
const convexUrl = process.env.CONVEX_URL?.trim();
|
|
617
647
|
const apiKey = process.env.DENCH_API_KEY?.trim();
|
|
618
648
|
if (!convexUrl || !apiKey) {
|
|
619
|
-
throw new Error(
|
|
620
|
-
"dench fs sync requires CONVEX_URL + DENCH_API_KEY in env",
|
|
621
|
-
);
|
|
649
|
+
throw new Error("dench fs sync requires CONVEX_URL + DENCH_API_KEY in env");
|
|
622
650
|
}
|
|
623
651
|
const client = new DenchFileClient(convexUrl, apiKey);
|
|
652
|
+
const resolvedOrgId = orgId ?? (await client.resolveOrganizationId());
|
|
653
|
+
process.stdout.write(
|
|
654
|
+
`[dench-fs-daemon] initial sync org=${resolvedOrgId} workspace=${workspace}\n`,
|
|
655
|
+
);
|
|
624
656
|
const fs = await import("node:fs/promises");
|
|
625
657
|
const path = await import("node:path");
|
|
626
658
|
|
|
@@ -670,7 +702,7 @@ async function runInitialSync(argv: string[]): Promise<void> {
|
|
|
670
702
|
main().catch((error) => {
|
|
671
703
|
console.error(
|
|
672
704
|
`[dench-fs-daemon] fatal: ${
|
|
673
|
-
error instanceof Error ? error.stack ?? error.message : String(error)
|
|
705
|
+
error instanceof Error ? (error.stack ?? error.message) : String(error)
|
|
674
706
|
}`,
|
|
675
707
|
);
|
|
676
708
|
// Long-running daemon: don't exit on transient errors. One-shot
|