@dench.com/cli 0.3.0 → 0.3.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/README.md +33 -0
- package/crm.ts +16 -5
- package/dench.ts +172 -27
- package/fs-daemon.ts +76 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,3 +149,36 @@ For staging:
|
|
|
149
149
|
dench login --staging
|
|
150
150
|
dench login --host https://workspace-staging.dench.com
|
|
151
151
|
```
|
|
152
|
+
|
|
153
|
+
## Sandbox / API key auth (no `dench login` required)
|
|
154
|
+
|
|
155
|
+
Inside a Daytona sandbox the unified Dench API key is baked into envVars
|
|
156
|
+
at create time (see `src/lib/secrets/sandbox-tokens.ts`). When
|
|
157
|
+
`DENCH_API_KEY` and `CONVEX_URL` are both present, the CLI skips the
|
|
158
|
+
`dench login` flow entirely and uses the API key as a bearer for every
|
|
159
|
+
org-scoped command — `dench crm`, `dench files`, `dench fs sync`, etc.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
export DENCH_API_KEY=dench_pk_live_…
|
|
163
|
+
export CONVEX_URL=https://your-convex-deployment.convex.cloud
|
|
164
|
+
# Optional but enables nicer URLs in CLI output:
|
|
165
|
+
export DENCH_API_URL=https://app.dench.com
|
|
166
|
+
export DENCH_ORG_ID=…
|
|
167
|
+
export DENCH_ORG_SLUG=…
|
|
168
|
+
|
|
169
|
+
dench crm entries list people
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Auth precedence:
|
|
173
|
+
|
|
174
|
+
1. `--dev` flag → use the dev workspace env (`NEXT_PUBLIC_CONVEX_URL`,
|
|
175
|
+
`DENCH_WORKSPACE`, `DENCH_DEV_AGENT_KEY`).
|
|
176
|
+
2. A locally-stored `dench login` session for the current host.
|
|
177
|
+
3. `DENCH_API_KEY` + `CONVEX_URL` (sandbox / automation path).
|
|
178
|
+
4. `--dev`-style env vars (without the flag).
|
|
179
|
+
|
|
180
|
+
Commands that need to impersonate a specific agent (`dench memory`,
|
|
181
|
+
`dench task`, `dench claim`, `dench log`, `dench approval request`,
|
|
182
|
+
etc.) still require an agent session (`dench login`) — the unified API
|
|
183
|
+
key is org-scoped and cannot speak as a single agent. Those commands
|
|
184
|
+
return a clear "agent session required" error in API-key mode.
|
package/crm.ts
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
* `dench crm <subcommand>` — CLI surface for the Convex CRM.
|
|
3
3
|
*
|
|
4
4
|
* All operations resolve via the unified Dench API key baked into the
|
|
5
|
-
* sandbox (DENCH_API_KEY) or
|
|
6
|
-
*
|
|
5
|
+
* sandbox (DENCH_API_KEY) or a locally-stored agent session token from
|
|
6
|
+
* `dench login`. The dispatcher does not care which one — the parent
|
|
7
|
+
* (`cli/dench.ts → getRuntime`) picks whichever auth is available and
|
|
8
|
+
* forwards a single bearer through `sessionToken`. The server-side
|
|
9
|
+
* `requireCrmAccess` (convex/lib/crm/access.ts) accepts either format.
|
|
10
|
+
*
|
|
11
|
+
* Talks to the Convex deployment URL provided by the active runtime
|
|
12
|
+
* (the session's `convexUrl` or the sandbox's `CONVEX_URL` envvar).
|
|
7
13
|
*
|
|
8
14
|
* This is a thin command dispatcher; the heavy lifting lives in
|
|
9
15
|
* convex/functions/crm/* and convex/lib/crm/*.
|
|
@@ -37,9 +43,14 @@ type CrmCliContext = {
|
|
|
37
43
|
args: string[];
|
|
38
44
|
jsonOutput: boolean;
|
|
39
45
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
46
|
+
* Bearer token sent with every Convex call so the server-side
|
|
47
|
+
* `requireCrmAccess` helper can resolve the caller's organization
|
|
48
|
+
* without a Convex Auth cookie. Two formats are accepted:
|
|
49
|
+
* • `dch_agent_*` agent session minted by `dench login`.
|
|
50
|
+
* • Unified Dench API key (`dench_pk_*`, `dench_live_*`,
|
|
51
|
+
* `gw_sk_*`, etc.) baked into sandbox envVars as `DENCH_API_KEY`.
|
|
52
|
+
* The server dispatches on prefix; both resolve to the same
|
|
53
|
+
* organization context.
|
|
43
54
|
*/
|
|
44
55
|
sessionToken?: string;
|
|
45
56
|
};
|
package/dench.ts
CHANGED
|
@@ -1620,11 +1620,27 @@ function hasDevEnv() {
|
|
|
1620
1620
|
);
|
|
1621
1621
|
}
|
|
1622
1622
|
|
|
1623
|
+
/**
|
|
1624
|
+
* Resolve the Convex URL the API-key runtime should talk to. Sandboxes
|
|
1625
|
+
* bake `CONVEX_URL` (see `src/lib/secrets/sandbox-tokens.ts`); local
|
|
1626
|
+
* dev typically only has `NEXT_PUBLIC_CONVEX_URL` exported. We accept
|
|
1627
|
+
* either so the same code path works in both shells without callers
|
|
1628
|
+
* needing to know which env var is set.
|
|
1629
|
+
*/
|
|
1630
|
+
function resolveApiKeyConvexUrl(): string | undefined {
|
|
1631
|
+
return (
|
|
1632
|
+
process.env.CONVEX_URL?.trim() ||
|
|
1633
|
+
process.env.NEXT_PUBLIC_CONVEX_URL?.trim() ||
|
|
1634
|
+
undefined
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1623
1638
|
function loginRequiredError(command = "this command") {
|
|
1624
1639
|
return new CliError(`${command} requires dench login`, {
|
|
1625
1640
|
code: "login_required",
|
|
1626
1641
|
nextActions: [
|
|
1627
|
-
|
|
1642
|
+
"Set DENCH_API_KEY (and CONVEX_URL) in your env if running inside a sandbox or CI.",
|
|
1643
|
+
'Or run dench onboard --kind <kind> --name "AI Agent - Project" to mint an agent session.',
|
|
1628
1644
|
"If already approved, run dench sessions --json, then dench use <session-key-or-workspace-slug>.",
|
|
1629
1645
|
],
|
|
1630
1646
|
});
|
|
@@ -1670,6 +1686,33 @@ async function getRuntime() {
|
|
|
1670
1686
|
],
|
|
1671
1687
|
});
|
|
1672
1688
|
}
|
|
1689
|
+
|
|
1690
|
+
// Sandbox / automation path: the unified Dench API key + Convex URL
|
|
1691
|
+
// baked into sandbox envVars (see
|
|
1692
|
+
// src/lib/secrets/sandbox-tokens.ts) is enough to drive every
|
|
1693
|
+
// command that goes through `requireCrmAccess` server-side. We only
|
|
1694
|
+
// fall into this branch when no stored session is present, so
|
|
1695
|
+
// explicit `dench login` setups keep their existing behavior.
|
|
1696
|
+
const apiKey = process.env.DENCH_API_KEY?.trim();
|
|
1697
|
+
const convexUrl = resolveApiKeyConvexUrl();
|
|
1698
|
+
if (apiKey && convexUrl) {
|
|
1699
|
+
const orgId = process.env.DENCH_ORG_ID?.trim();
|
|
1700
|
+
const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
|
|
1701
|
+
const apiHost =
|
|
1702
|
+
process.env.DENCH_API_URL?.trim() ||
|
|
1703
|
+
process.env.DENCH_INTERNAL_BASE?.trim() ||
|
|
1704
|
+
host;
|
|
1705
|
+
return {
|
|
1706
|
+
mode: "apiKey" as const,
|
|
1707
|
+
host: apiHost,
|
|
1708
|
+
client: new ConvexHttpClient(convexUrl),
|
|
1709
|
+
sessionToken: apiKey,
|
|
1710
|
+
organization:
|
|
1711
|
+
orgId && orgSlug
|
|
1712
|
+
? { id: orgId, name: orgSlug, slug: orgSlug }
|
|
1713
|
+
: undefined,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1673
1716
|
}
|
|
1674
1717
|
|
|
1675
1718
|
if (!hasDevEnv()) {
|
|
@@ -1688,14 +1731,73 @@ async function getRuntime() {
|
|
|
1688
1731
|
|
|
1689
1732
|
type Runtime = Awaited<ReturnType<typeof getRuntime>>;
|
|
1690
1733
|
|
|
1734
|
+
/**
|
|
1735
|
+
* `session` mode is required for `agentWorkspace.*` calls because they
|
|
1736
|
+
* gate on `validateAgentSession` server-side, which only accepts the
|
|
1737
|
+
* `dch_agent_*` token format. Sandboxes that only have a unified
|
|
1738
|
+
* `DENCH_API_KEY` (apiKey mode) cannot impersonate a specific agent
|
|
1739
|
+
* and will fail at the server with `Invalid agent session` — surface
|
|
1740
|
+
* that here as a clear error instead.
|
|
1741
|
+
*/
|
|
1691
1742
|
function requireSessionRuntime(
|
|
1692
1743
|
runtime: Runtime,
|
|
1693
1744
|
command: string,
|
|
1694
1745
|
): Extract<Runtime, { mode: "session" }> {
|
|
1695
|
-
if (runtime.mode
|
|
1696
|
-
|
|
1746
|
+
if (runtime.mode === "session") return runtime;
|
|
1747
|
+
if (runtime.mode === "apiKey") {
|
|
1748
|
+
throw agentSessionRequiredError(command);
|
|
1749
|
+
}
|
|
1750
|
+
throw loginRequiredError(command);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* Org-scoped commands (`dench crm`, `dench files`, etc.) accept either
|
|
1755
|
+
* a `dch_agent_*` agent session token OR a unified Dench API key
|
|
1756
|
+
* (`dench_*` / `gw_sk_*` / etc.) baked into sandbox envVars. The
|
|
1757
|
+
* server-side `requireCrmAccess` helper dispatches on the prefix.
|
|
1758
|
+
*/
|
|
1759
|
+
type AuthenticatedRuntime = Extract<
|
|
1760
|
+
Runtime,
|
|
1761
|
+
{ mode: "session" | "apiKey" }
|
|
1762
|
+
>;
|
|
1763
|
+
|
|
1764
|
+
function requireAuthenticatedRuntime(
|
|
1765
|
+
runtime: Runtime,
|
|
1766
|
+
command: string,
|
|
1767
|
+
): AuthenticatedRuntime {
|
|
1768
|
+
if (runtime.mode === "session" || runtime.mode === "apiKey") return runtime;
|
|
1769
|
+
throw loginRequiredError(command);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function agentSessionRequiredError(command: string) {
|
|
1773
|
+
return new CliError(
|
|
1774
|
+
`${command} requires an agent session — DENCH_API_KEY is org-scoped and cannot impersonate a specific agent`,
|
|
1775
|
+
{
|
|
1776
|
+
code: "agent_session_required",
|
|
1777
|
+
nextActions: [
|
|
1778
|
+
'Run dench onboard --kind <kind> --name "AI Agent - Project" to mint an agent session for this command.',
|
|
1779
|
+
"Or run the equivalent action via the workspace UI / dench crm if it is org-level (e.g. CRM data, files).",
|
|
1780
|
+
],
|
|
1781
|
+
},
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
/**
|
|
1786
|
+
* Some legacy `agentWorkspace.*` commands (task create / claim / log /
|
|
1787
|
+
* approval request) have separate session vs dev branches. With apiKey
|
|
1788
|
+
* mode added, the dev branch would silently receive partial args. Use
|
|
1789
|
+
* this to short-circuit those branches with a clear error.
|
|
1790
|
+
*
|
|
1791
|
+
* Asserts away the `apiKey` mode so the caller's downstream type
|
|
1792
|
+
* narrowing (e.g. `runtime.workspaceArgs`) keeps working.
|
|
1793
|
+
*/
|
|
1794
|
+
function rejectApiKeyForAgentCommand(
|
|
1795
|
+
runtime: Runtime,
|
|
1796
|
+
command: string,
|
|
1797
|
+
): asserts runtime is Exclude<Runtime, { mode: "apiKey" }> {
|
|
1798
|
+
if (runtime.mode === "apiKey") {
|
|
1799
|
+
throw agentSessionRequiredError(command);
|
|
1697
1800
|
}
|
|
1698
|
-
return runtime;
|
|
1699
1801
|
}
|
|
1700
1802
|
|
|
1701
1803
|
async function printToolStatus(runtime: Runtime, toolkit?: string) {
|
|
@@ -1927,23 +2029,36 @@ async function connectedAppsContext(runtime: Runtime) {
|
|
|
1927
2029
|
}
|
|
1928
2030
|
|
|
1929
2031
|
async function buildContext(runtime: Runtime) {
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2032
|
+
let mine: JsonRecord;
|
|
2033
|
+
let urls: JsonRecord;
|
|
2034
|
+
if (runtime.mode === "session") {
|
|
2035
|
+
mine = (await runtime.client.query(
|
|
2036
|
+
api.functions.agentWorkspace.whatCanIDoHere,
|
|
2037
|
+
{ sessionToken: runtime.sessionToken },
|
|
2038
|
+
)) as JsonRecord;
|
|
2039
|
+
urls = workspaceUrls(runtime.host, workspaceSlugFromContext(mine));
|
|
2040
|
+
} else if (runtime.mode === "dev") {
|
|
2041
|
+
mine = summarizeDevMine(
|
|
2042
|
+
runtime.workspaceArgs.workspaceSlug,
|
|
2043
|
+
await devOverview(runtime),
|
|
2044
|
+
);
|
|
2045
|
+
urls = {};
|
|
2046
|
+
} else {
|
|
2047
|
+
// apiKey mode: no agent session is available, so we can't call
|
|
2048
|
+
// `whatCanIDoHere` (it gates on `validateAgentSession`). Return a
|
|
2049
|
+
// minimal context derived from sandbox envVars so dench-cli help
|
|
2050
|
+
// surfaces still work without a full login.
|
|
2051
|
+
mine = {
|
|
2052
|
+
authMode: "apiKey",
|
|
2053
|
+
organization: runtime.organization ?? null,
|
|
2054
|
+
runId: process.env.DENCH_RUN_ID?.trim() ?? null,
|
|
2055
|
+
parentRunId: process.env.DENCH_PARENT_RUN_ID?.trim() ?? null,
|
|
2056
|
+
rootRunId: process.env.DENCH_ROOT_RUN_ID?.trim() ?? null,
|
|
2057
|
+
};
|
|
2058
|
+
urls = runtime.host
|
|
2059
|
+
? workspaceUrls(runtime.host, runtime.organization?.slug)
|
|
1946
2060
|
: {};
|
|
2061
|
+
}
|
|
1947
2062
|
|
|
1948
2063
|
return {
|
|
1949
2064
|
...mine,
|
|
@@ -2341,7 +2456,7 @@ function withToolRunGuidance(runtime: Runtime, result: unknown) {
|
|
|
2341
2456
|
if (!record || record.requiresApproval !== true) return result;
|
|
2342
2457
|
const approvalId = stringField(record, "approvalId");
|
|
2343
2458
|
const statusContext =
|
|
2344
|
-
runtime.mode === "session"
|
|
2459
|
+
runtime.mode === "session" || runtime.mode === "apiKey"
|
|
2345
2460
|
? workspaceUrls(runtime.host, runtime.organization?.slug)
|
|
2346
2461
|
: {};
|
|
2347
2462
|
const approvalCommand = approvalId
|
|
@@ -2501,6 +2616,12 @@ async function listArtifacts(runtime: Runtime, limit?: number) {
|
|
|
2501
2616
|
);
|
|
2502
2617
|
}
|
|
2503
2618
|
}
|
|
2619
|
+
if (runtime.mode === "apiKey") {
|
|
2620
|
+
// No agent identity → no artifact list. Return empty so callers
|
|
2621
|
+
// (e.g. `dench artifacts`, `dench suggested-work`) degrade gracefully
|
|
2622
|
+
// instead of throwing for sandbox callers.
|
|
2623
|
+
return [] as JsonRecord[];
|
|
2624
|
+
}
|
|
2504
2625
|
const data = await devOverview(runtime);
|
|
2505
2626
|
return asRecordArray(data.recentArtifacts);
|
|
2506
2627
|
}
|
|
@@ -2675,14 +2796,16 @@ async function main() {
|
|
|
2675
2796
|
}
|
|
2676
2797
|
const { runCrmCommand } = await import("./crm");
|
|
2677
2798
|
const runtime = await getRuntime();
|
|
2678
|
-
|
|
2799
|
+
// CRM is org-scoped — accept either an agent session token
|
|
2800
|
+
// (`dch_agent_*` from `dench login`) or a unified Dench API key
|
|
2801
|
+
// (`DENCH_API_KEY` baked into sandbox envVars). Server-side
|
|
2802
|
+
// `requireCrmAccess` (convex/lib/crm/access.ts) dispatches on the
|
|
2803
|
+
// bearer prefix and resolves the org for both.
|
|
2804
|
+
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench crm");
|
|
2679
2805
|
await runCrmCommand({
|
|
2680
|
-
convex:
|
|
2806
|
+
convex: authedRuntime.client,
|
|
2681
2807
|
args: subArgs,
|
|
2682
|
-
|
|
2683
|
-
// this token and resolves the agent's organization without a Convex
|
|
2684
|
-
// Auth cookie. Mirrors the pattern used by every agentWorkspace fn.
|
|
2685
|
-
sessionToken: sessionRuntime.sessionToken,
|
|
2808
|
+
sessionToken: authedRuntime.sessionToken,
|
|
2686
2809
|
});
|
|
2687
2810
|
return;
|
|
2688
2811
|
}
|
|
@@ -2881,6 +3004,13 @@ async function main() {
|
|
|
2881
3004
|
print(status);
|
|
2882
3005
|
return;
|
|
2883
3006
|
}
|
|
3007
|
+
if (runtime.mode === "apiKey") {
|
|
3008
|
+
// No agent session → no agent-status query. Fall back to a thin
|
|
3009
|
+
// env-derived snapshot so sandbox callers still get something
|
|
3010
|
+
// useful from `dench status`.
|
|
3011
|
+
print(await buildContext(runtime));
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
2884
3014
|
const data = await devOverview(runtime);
|
|
2885
3015
|
const output = scopedStatus
|
|
2886
3016
|
? summarizeDevMine(runtime.workspaceArgs.workspaceSlug, data)
|
|
@@ -2903,6 +3033,11 @@ async function main() {
|
|
|
2903
3033
|
);
|
|
2904
3034
|
return;
|
|
2905
3035
|
}
|
|
3036
|
+
if (runtime.mode === "apiKey") {
|
|
3037
|
+
throw loginRequiredError(
|
|
3038
|
+
"dench tasks (agent-scoped task list)",
|
|
3039
|
+
);
|
|
3040
|
+
}
|
|
2906
3041
|
const data = await devOverview(runtime);
|
|
2907
3042
|
print(data.tasks);
|
|
2908
3043
|
return;
|
|
@@ -2919,6 +3054,9 @@ async function main() {
|
|
|
2919
3054
|
print(status.agents);
|
|
2920
3055
|
return;
|
|
2921
3056
|
}
|
|
3057
|
+
if (runtime.mode === "apiKey") {
|
|
3058
|
+
throw loginRequiredError("dench agents");
|
|
3059
|
+
}
|
|
2922
3060
|
const data = await devOverview(runtime);
|
|
2923
3061
|
print(data.agents);
|
|
2924
3062
|
return;
|
|
@@ -2935,6 +3073,9 @@ async function main() {
|
|
|
2935
3073
|
print(status.approvals);
|
|
2936
3074
|
return;
|
|
2937
3075
|
}
|
|
3076
|
+
if (runtime.mode === "apiKey") {
|
|
3077
|
+
throw loginRequiredError("dench approvals");
|
|
3078
|
+
}
|
|
2938
3079
|
const data = await devOverview(runtime);
|
|
2939
3080
|
print(data.approvals);
|
|
2940
3081
|
return;
|
|
@@ -3000,6 +3141,7 @@ async function main() {
|
|
|
3000
3141
|
);
|
|
3001
3142
|
return;
|
|
3002
3143
|
}
|
|
3144
|
+
rejectApiKeyForAgentCommand(runtime, "dench task create");
|
|
3003
3145
|
const taskId = await runtime.client.mutation(
|
|
3004
3146
|
api.functions.agentWorkspace.devCreateTask,
|
|
3005
3147
|
{
|
|
@@ -3108,6 +3250,7 @@ async function main() {
|
|
|
3108
3250
|
);
|
|
3109
3251
|
return;
|
|
3110
3252
|
}
|
|
3253
|
+
rejectApiKeyForAgentCommand(runtime, "dench claim");
|
|
3111
3254
|
const agentId = option("--agent") ?? (await defaultDevAgentId(runtime));
|
|
3112
3255
|
print(
|
|
3113
3256
|
await runtime.client.mutation(api.functions.agentWorkspace.devClaimTask, {
|
|
@@ -3135,6 +3278,7 @@ async function main() {
|
|
|
3135
3278
|
);
|
|
3136
3279
|
return;
|
|
3137
3280
|
}
|
|
3281
|
+
rejectApiKeyForAgentCommand(runtime, "dench log");
|
|
3138
3282
|
print(
|
|
3139
3283
|
await runtime.client.mutation(api.functions.agentWorkspace.devAppendLog, {
|
|
3140
3284
|
...runtime.workspaceArgs,
|
|
@@ -3164,6 +3308,7 @@ async function main() {
|
|
|
3164
3308
|
);
|
|
3165
3309
|
return;
|
|
3166
3310
|
}
|
|
3311
|
+
rejectApiKeyForAgentCommand(runtime, "dench approval request");
|
|
3167
3312
|
const approvalId = await runtime.client.mutation(
|
|
3168
3313
|
api.functions.agentWorkspace.devRequestApproval,
|
|
3169
3314
|
{
|
package/fs-daemon.ts
CHANGED
|
@@ -27,9 +27,11 @@
|
|
|
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";
|
|
@@ -39,7 +41,7 @@ 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
|
};
|
|
@@ -71,11 +73,6 @@ function parseArgs(argv: string[]): Args {
|
|
|
71
73
|
out.workspace ??= process.env.DENCH_VOLUME_PATH?.trim() || "/workspace";
|
|
72
74
|
out.hotCache ??=
|
|
73
75
|
process.env.DENCH_HOT_CACHE_PATH?.trim() || "/tmp/dench-cache";
|
|
74
|
-
if (!out.orgId) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
"dench-fs-daemon: org id is required. Set DENCH_ORG_ID env var or pass --org-id.",
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
76
|
return out as Args;
|
|
80
77
|
}
|
|
81
78
|
|
|
@@ -185,6 +182,9 @@ const api = {
|
|
|
185
182
|
),
|
|
186
183
|
deleteFile: makeFunctionReference<"mutation">("functions/files:deleteFile"),
|
|
187
184
|
listTree: makeFunctionReference<"query">("functions/files:listTree"),
|
|
185
|
+
resolveApiKeyOrganization: makeFunctionReference<"query">(
|
|
186
|
+
"functions/files:resolveApiKeyOrganization",
|
|
187
|
+
),
|
|
188
188
|
appendLiveBuffer: makeFunctionReference<"mutation">(
|
|
189
189
|
"functions/files:appendLiveBuffer",
|
|
190
190
|
),
|
|
@@ -201,17 +201,24 @@ class DenchFileClient {
|
|
|
201
201
|
private http: ConvexHttpClient;
|
|
202
202
|
private realtime: ConvexClient | null = null;
|
|
203
203
|
|
|
204
|
-
constructor(
|
|
204
|
+
constructor(
|
|
205
|
+
convexUrl: string,
|
|
206
|
+
private readonly apiKey: string,
|
|
207
|
+
) {
|
|
205
208
|
this.http = new ConvexHttpClient(convexUrl);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
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;
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
async generateUploadUrl(path: string): Promise<string> {
|
|
213
219
|
return (await this.http.mutation(api.files.generateUploadUrl, {
|
|
214
220
|
path,
|
|
221
|
+
apiKey: this.apiKey,
|
|
215
222
|
})) as string;
|
|
216
223
|
}
|
|
217
224
|
|
|
@@ -242,7 +249,10 @@ class DenchFileClient {
|
|
|
242
249
|
lastModifiedBy: string;
|
|
243
250
|
}): Promise<{ versionConflict: boolean; currentHash?: string }> {
|
|
244
251
|
try {
|
|
245
|
-
await this.http.mutation(api.files.commitFileUpload,
|
|
252
|
+
await this.http.mutation(api.files.commitFileUpload, {
|
|
253
|
+
...args,
|
|
254
|
+
apiKey: this.apiKey,
|
|
255
|
+
});
|
|
246
256
|
return { versionConflict: false };
|
|
247
257
|
} catch (error) {
|
|
248
258
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -260,12 +270,16 @@ class DenchFileClient {
|
|
|
260
270
|
}
|
|
261
271
|
|
|
262
272
|
async deleteFile(path: string): Promise<void> {
|
|
263
|
-
await this.http.mutation(api.files.deleteFile, {
|
|
273
|
+
await this.http.mutation(api.files.deleteFile, {
|
|
274
|
+
path,
|
|
275
|
+
apiKey: this.apiKey,
|
|
276
|
+
});
|
|
264
277
|
}
|
|
265
278
|
|
|
266
279
|
async getSignedDownloadUrl(path: string): Promise<string | null> {
|
|
267
280
|
const url = (await this.http.action(api.files.getFileSignedDownloadUrl, {
|
|
268
281
|
path,
|
|
282
|
+
apiKey: this.apiKey,
|
|
269
283
|
})) as string | null;
|
|
270
284
|
return url ?? null;
|
|
271
285
|
}
|
|
@@ -275,11 +289,17 @@ class DenchFileClient {
|
|
|
275
289
|
runId: string;
|
|
276
290
|
chunk: string;
|
|
277
291
|
}): Promise<void> {
|
|
278
|
-
await this.http.mutation(api.files.appendLiveBuffer,
|
|
292
|
+
await this.http.mutation(api.files.appendLiveBuffer, {
|
|
293
|
+
...args,
|
|
294
|
+
apiKey: this.apiKey,
|
|
295
|
+
} as never);
|
|
279
296
|
}
|
|
280
297
|
|
|
281
298
|
async finalizeLiveBuffer(path: string): Promise<void> {
|
|
282
|
-
await this.http.mutation(api.files.finalizeLiveBuffer, {
|
|
299
|
+
await this.http.mutation(api.files.finalizeLiveBuffer, {
|
|
300
|
+
path,
|
|
301
|
+
apiKey: this.apiKey,
|
|
302
|
+
});
|
|
283
303
|
}
|
|
284
304
|
|
|
285
305
|
/**
|
|
@@ -298,7 +318,7 @@ class DenchFileClient {
|
|
|
298
318
|
}
|
|
299
319
|
const unsubscribe = this.realtime.onUpdate(
|
|
300
320
|
api.files.listTree,
|
|
301
|
-
{ prefix: "/" },
|
|
321
|
+
{ prefix: "/", apiKey: this.apiKey },
|
|
302
322
|
(rows: unknown) => {
|
|
303
323
|
const list = Array.isArray(rows)
|
|
304
324
|
? (rows as Array<{
|
|
@@ -339,37 +359,46 @@ async function pullDownRemoteFile(
|
|
|
339
359
|
);
|
|
340
360
|
}
|
|
341
361
|
|
|
362
|
+
/**
|
|
363
|
+
* sysexits.h-style exit codes the supervisor (in
|
|
364
|
+
* src/lib/daytona/sandbox-bootstrap.ts) recognizes as **permanent** —
|
|
365
|
+
* keep restarting won't fix a missing env var or a missing dependency,
|
|
366
|
+
* so the supervisor gives up instead of crash-looping. Anything else
|
|
367
|
+
* (network blip, transient Convex error) exits 1 and gets retried.
|
|
368
|
+
*/
|
|
369
|
+
const EXIT_CONFIG_ERROR = 78;
|
|
370
|
+
|
|
342
371
|
async function runDaemon(daemonArgs: Args): Promise<void> {
|
|
343
372
|
const convexUrl = process.env.CONVEX_URL?.trim();
|
|
344
373
|
const apiKey = process.env.DENCH_API_KEY?.trim();
|
|
345
374
|
if (!convexUrl || !apiKey) {
|
|
346
375
|
console.error(
|
|
347
|
-
"[dench-fs-daemon] CONVEX_URL or DENCH_API_KEY missing in env;
|
|
376
|
+
"[dench-fs-daemon] CONVEX_URL or DENCH_API_KEY missing in env; refusing to start",
|
|
348
377
|
);
|
|
349
|
-
|
|
350
|
-
return;
|
|
378
|
+
process.exit(EXIT_CONFIG_ERROR);
|
|
351
379
|
}
|
|
352
380
|
|
|
381
|
+
const client = new DenchFileClient(convexUrl, apiKey);
|
|
382
|
+
const resolvedOrgId =
|
|
383
|
+
daemonArgs.orgId ?? (await client.resolveOrganizationId());
|
|
353
384
|
console.log(
|
|
354
|
-
`[dench-fs-daemon] starting org=${
|
|
385
|
+
`[dench-fs-daemon] starting org=${resolvedOrgId} workspace=${daemonArgs.workspace}`,
|
|
355
386
|
);
|
|
356
387
|
mkdirSync(daemonArgs.workspace, { recursive: true });
|
|
357
388
|
mkdirSync(daemonArgs.hotCache, { recursive: true });
|
|
358
389
|
|
|
359
390
|
const tracker = new HashTracker();
|
|
360
|
-
const client = new DenchFileClient(convexUrl, apiKey);
|
|
361
391
|
|
|
362
392
|
let chokidar: typeof import("chokidar");
|
|
363
393
|
try {
|
|
364
394
|
chokidar = (await import("chokidar")) as never;
|
|
365
395
|
} catch (error) {
|
|
366
396
|
console.error(
|
|
367
|
-
`[dench-fs-daemon] chokidar not available;
|
|
397
|
+
`[dench-fs-daemon] chokidar not available; refusing to start (${
|
|
368
398
|
error instanceof Error ? error.message : String(error)
|
|
369
399
|
})`,
|
|
370
400
|
);
|
|
371
|
-
|
|
372
|
-
return;
|
|
401
|
+
process.exit(EXIT_CONFIG_ERROR);
|
|
373
402
|
}
|
|
374
403
|
|
|
375
404
|
const watcher = chokidar.watch(daemonArgs.workspace, {
|
|
@@ -606,11 +635,19 @@ async function main(): Promise<void> {
|
|
|
606
635
|
*/
|
|
607
636
|
async function runInitialSync(argv: string[]): Promise<void> {
|
|
608
637
|
const initial = argv.includes("--initial");
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
|
|
638
|
+
const orgIdIndex = argv.indexOf("--org-id");
|
|
639
|
+
const workspaceIndex = argv.indexOf("--workspace");
|
|
640
|
+
const orgId =
|
|
641
|
+
orgIdIndex === -1
|
|
642
|
+
? (process.env.DENCH_ORG_ID?.trim() ?? undefined)
|
|
643
|
+
: argv[orgIdIndex + 1];
|
|
644
|
+
const workspace =
|
|
645
|
+
workspaceIndex === -1
|
|
646
|
+
? "/workspace"
|
|
647
|
+
: (argv[workspaceIndex + 1] ?? "/workspace");
|
|
648
|
+
if (!initial) {
|
|
612
649
|
throw new Error(
|
|
613
|
-
"dench fs sync --initial --org-id <orgId> [--workspace /workspace]",
|
|
650
|
+
"dench fs sync --initial [--org-id <orgId>] [--workspace /workspace]",
|
|
614
651
|
);
|
|
615
652
|
}
|
|
616
653
|
const convexUrl = process.env.CONVEX_URL?.trim();
|
|
@@ -619,6 +656,10 @@ async function runInitialSync(argv: string[]): Promise<void> {
|
|
|
619
656
|
throw new Error("dench fs sync requires CONVEX_URL + DENCH_API_KEY in env");
|
|
620
657
|
}
|
|
621
658
|
const client = new DenchFileClient(convexUrl, apiKey);
|
|
659
|
+
const resolvedOrgId = orgId ?? (await client.resolveOrganizationId());
|
|
660
|
+
process.stdout.write(
|
|
661
|
+
`[dench-fs-daemon] initial sync org=${resolvedOrgId} workspace=${workspace}\n`,
|
|
662
|
+
);
|
|
622
663
|
const fs = await import("node:fs/promises");
|
|
623
664
|
const path = await import("node:path");
|
|
624
665
|
|
|
@@ -671,10 +712,10 @@ main().catch((error) => {
|
|
|
671
712
|
error instanceof Error ? (error.stack ?? error.message) : String(error)
|
|
672
713
|
}`,
|
|
673
714
|
);
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
715
|
+
// Always exit with a non-zero code so the supervisor (in
|
|
716
|
+
// src/lib/daytona/sandbox-bootstrap.ts) can decide whether to
|
|
717
|
+
// restart. Previously we parked the process on a noop interval,
|
|
718
|
+
// which made `pgrep dench-fs-daemon` report "alive" for a daemon
|
|
719
|
+
// that had silently given up syncing.
|
|
720
|
+
process.exit(1);
|
|
680
721
|
});
|