@data-club/ai-hub-server 0.0.8 → 0.0.10
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/auth/encryption.d.ts +5 -5
- package/dist/auth/encryption.js +5 -5
- package/dist/auth/userView.d.ts +8 -15
- package/dist/auth/userView.d.ts.map +1 -1
- package/dist/auth/userView.js +10 -23
- package/dist/auth/userView.js.map +1 -1
- package/dist/bootstrap.d.ts +2 -8
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +2 -35
- package/dist/bootstrap.js.map +1 -1
- package/dist/db/migrate.js +0 -17
- package/dist/db/migrate.js.map +1 -1
- package/dist/db/schema.d.ts +0 -263
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +0 -57
- package/dist/db/schema.js.map +1 -1
- package/dist/db/seed.d.ts +4 -19
- package/dist/db/seed.d.ts.map +1 -1
- package/dist/db/seed.js +6 -33
- package/dist/db/seed.js.map +1 -1
- package/dist/handlers/fetch.d.ts +6 -7
- package/dist/handlers/fetch.d.ts.map +1 -1
- package/dist/handlers/fetch.js +0 -2
- package/dist/handlers/fetch.js.map +1 -1
- package/dist/handlers/types.d.ts +5 -6
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/letta/client.d.ts +86 -263
- package/dist/letta/client.d.ts.map +1 -1
- package/dist/letta/client.js +99 -404
- package/dist/letta/client.js.map +1 -1
- package/dist/letta/index.d.ts +6 -8
- package/dist/letta/index.d.ts.map +1 -1
- package/dist/letta/index.js +5 -7
- package/dist/letta/index.js.map +1 -1
- package/dist/letta/sandbox.d.ts +45 -9
- package/dist/letta/sandbox.d.ts.map +1 -1
- package/dist/letta/sandbox.js +123 -11
- package/dist/letta/sandbox.js.map +1 -1
- package/dist/letta/types.d.ts +6 -49
- package/dist/letta/types.d.ts.map +1 -1
- package/dist/letta/types.js +5 -10
- package/dist/letta/types.js.map +1 -1
- package/dist/observability/logger.d.ts.map +1 -1
- package/dist/observability/logger.js +0 -2
- package/dist/observability/logger.js.map +1 -1
- package/dist/routes/admin/agents.d.ts.map +1 -1
- package/dist/routes/admin/agents.js +0 -40
- package/dist/routes/admin/agents.js.map +1 -1
- package/dist/routes/admin/settings.d.ts +6 -15
- package/dist/routes/admin/settings.d.ts.map +1 -1
- package/dist/routes/admin/settings.js +9 -107
- package/dist/routes/admin/settings.js.map +1 -1
- package/dist/routes/auth.d.ts +0 -4
- package/dist/routes/auth.d.ts.map +1 -1
- package/dist/routes/auth.js +1 -5
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +9 -30
- package/dist/routes/sessions.js.map +1 -1
- package/dist/state/turnRunner.d.ts.map +1 -1
- package/dist/state/turnRunner.js +1 -75
- package/dist/state/turnRunner.js.map +1 -1
- package/package.json +4 -4
- package/src/auth/encryption.ts +5 -5
- package/src/auth/userView.ts +9 -22
- package/src/bootstrap.ts +4 -43
- package/src/db/migrate.ts +0 -17
- package/src/db/schema.ts +0 -68
- package/src/db/seed.ts +6 -47
- package/src/handlers/fetch.ts +6 -9
- package/src/handlers/types.ts +5 -6
- package/src/letta/client.ts +170 -634
- package/src/letta/index.ts +8 -21
- package/src/letta/sandbox.ts +151 -11
- package/src/letta/types.ts +9 -60
- package/src/observability/logger.ts +0 -2
- package/src/routes/admin/agents.ts +0 -49
- package/src/routes/admin/settings.ts +9 -113
- package/src/routes/auth.ts +1 -5
- package/src/routes/sessions.ts +11 -32
- package/src/state/turnRunner.ts +1 -77
- package/src/letta/lettaCliShim.mjs +0 -40
- package/src/letta/settingsFileSeed.ts +0 -254
package/src/routes/sessions.ts
CHANGED
|
@@ -35,7 +35,6 @@ import { protectedRoute } from "../auth/middleware.js";
|
|
|
35
35
|
import {
|
|
36
36
|
agent,
|
|
37
37
|
messageOverride,
|
|
38
|
-
messageUsage,
|
|
39
38
|
rotatingMessage,
|
|
40
39
|
session,
|
|
41
40
|
turn,
|
|
@@ -893,45 +892,21 @@ function projectAndRespond(
|
|
|
893
892
|
}
|
|
894
893
|
}
|
|
895
894
|
|
|
896
|
-
// Per-assistant-message usage (cache hits/writes + token counts).
|
|
897
|
-
// Stamped by the runner on the last assistant message of each
|
|
898
|
-
// turn (see turnRunner.ts step 5.5). Renders as the cache badge
|
|
899
|
-
// on the chat UI's assistant bubbles.
|
|
900
|
-
const usageRows = ctx.db
|
|
901
|
-
.select()
|
|
902
|
-
.from(messageUsage)
|
|
903
|
-
.where(eq(messageUsage.sessionId, row.id))
|
|
904
|
-
.all();
|
|
905
|
-
const usageByMessageId = new Map<string, AIHubMessage["usage"]>();
|
|
906
|
-
for (const u of usageRows) {
|
|
907
|
-
usageByMessageId.set(u.lettaMessageId, {
|
|
908
|
-
input: u.promptTokens,
|
|
909
|
-
output: u.completionTokens,
|
|
910
|
-
cache_read: u.cachedInputTokens,
|
|
911
|
-
cache_write: u.cacheWriteTokens,
|
|
912
|
-
reasoning: u.reasoningTokens,
|
|
913
|
-
context: u.contextTokens,
|
|
914
|
-
total: u.totalTokens,
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
|
|
918
895
|
const projected = sorted.map((msg): AIHubMessage => {
|
|
919
896
|
const pos = positionByLettaId.get(msg.id);
|
|
920
|
-
const usage = usageByMessageId.get(msg.id);
|
|
921
|
-
const withUsage = usage !== undefined ? { ...msg, usage } : msg;
|
|
922
897
|
if (pos !== undefined) {
|
|
923
898
|
const explicit = overrideByPosition.get(pos);
|
|
924
899
|
if (explicit !== undefined) {
|
|
925
|
-
return { ...
|
|
900
|
+
return { ...msg, content: explicit };
|
|
926
901
|
}
|
|
927
902
|
}
|
|
928
903
|
if (msg.role === "system") {
|
|
929
904
|
const stripped = stripMemoryMetadata(msg.content);
|
|
930
905
|
if (stripped !== msg.content) {
|
|
931
|
-
return { ...
|
|
906
|
+
return { ...msg, content: stripped };
|
|
932
907
|
}
|
|
933
908
|
}
|
|
934
|
-
return
|
|
909
|
+
return msg;
|
|
935
910
|
});
|
|
936
911
|
|
|
937
912
|
const finalMessages = applyRotationDisplayFilter
|
|
@@ -1882,21 +1857,21 @@ function toWireMessage(msg: LettaMessage): AIHubMessage | null {
|
|
|
1882
1857
|
id: msg.id,
|
|
1883
1858
|
role: "user",
|
|
1884
1859
|
content: extractTextContent(msg.content),
|
|
1885
|
-
created_at: msg.date,
|
|
1860
|
+
created_at: msg.date ?? "",
|
|
1886
1861
|
};
|
|
1887
1862
|
case "assistant_message":
|
|
1888
1863
|
return {
|
|
1889
1864
|
id: msg.id,
|
|
1890
1865
|
role: "assistant",
|
|
1891
1866
|
content: extractTextContent(msg.content),
|
|
1892
|
-
created_at: msg.date,
|
|
1867
|
+
created_at: msg.date ?? "",
|
|
1893
1868
|
};
|
|
1894
1869
|
case "system_message":
|
|
1895
1870
|
return {
|
|
1896
1871
|
id: msg.id,
|
|
1897
1872
|
role: "system",
|
|
1898
1873
|
content: extractTextContent(msg.content),
|
|
1899
|
-
created_at: msg.date,
|
|
1874
|
+
created_at: msg.date ?? "",
|
|
1900
1875
|
};
|
|
1901
1876
|
default:
|
|
1902
1877
|
// reasoning_message / hidden_reasoning_message / tool_call_message /
|
|
@@ -1921,8 +1896,12 @@ function toWireMessage(msg: LettaMessage): AIHubMessage | null {
|
|
|
1921
1896
|
* consistent regardless of where reminders end up.
|
|
1922
1897
|
*/
|
|
1923
1898
|
function extractTextContent(
|
|
1924
|
-
content:
|
|
1899
|
+
content:
|
|
1900
|
+
| string
|
|
1901
|
+
| ReadonlyArray<{ type?: string; text?: string }>
|
|
1902
|
+
| undefined,
|
|
1925
1903
|
): string {
|
|
1904
|
+
if (content === undefined) return "";
|
|
1926
1905
|
if (typeof content === "string") return stripSystemReminders(content);
|
|
1927
1906
|
if (!Array.isArray(content)) return "";
|
|
1928
1907
|
let out = "";
|
package/src/state/turnRunner.ts
CHANGED
|
@@ -45,7 +45,7 @@ import type {
|
|
|
45
45
|
TurnStatus,
|
|
46
46
|
} from "@data-club/ai-hub";
|
|
47
47
|
|
|
48
|
-
import {
|
|
48
|
+
import { turn, turnEvent } from "../db/schema.js";
|
|
49
49
|
import type { DrizzleDB } from "../db/connection.js";
|
|
50
50
|
import { ServerOverloadedError, type ChatTurnHandle, type LettaClient } from "../letta/client.js";
|
|
51
51
|
import {
|
|
@@ -269,20 +269,8 @@ export async function runTurn(opts: RunTurnOpts): Promise<RunTurnResult> {
|
|
|
269
269
|
cancelController.signal.addEventListener("abort", onCancel, { once: true });
|
|
270
270
|
|
|
271
271
|
// Step 5 — drain the SDK message stream.
|
|
272
|
-
// Track the last Letta run id we see — used to fetch usage stats
|
|
273
|
-
// after the terminal `result`. The SDK strips Letta message ids
|
|
274
|
-
// from its translated payloads, so we re-fetch via REST
|
|
275
|
-
// (`getRunUsageStats(runId)`) to get both the usage_statistics
|
|
276
|
-
// and the last assistant message id we'll stamp the badge on.
|
|
277
|
-
let lastRunId: string | null = null;
|
|
278
272
|
try {
|
|
279
273
|
for await (const sdkMsg of handle.events) {
|
|
280
|
-
// SDKAssistantMessage / SDKReasoningMessage / SDKToolCallMessage
|
|
281
|
-
// each carry `runId`. Take the most recent — a multi-step turn
|
|
282
|
-
// can have several runs, and we want the latest.
|
|
283
|
-
if ((sdkMsg as { runId?: string }).runId) {
|
|
284
|
-
lastRunId = (sdkMsg as { runId: string }).runId;
|
|
285
|
-
}
|
|
286
274
|
if (sdkMsg.type === "result") {
|
|
287
275
|
// Terminal — finalizeTerminal emits the right closing events
|
|
288
276
|
// based on success / interrupted / error.
|
|
@@ -315,70 +303,6 @@ export async function runTurn(opts: RunTurnOpts): Promise<RunTurnResult> {
|
|
|
315
303
|
metrics.lettaRequestDurationMs.set(Date.now() - lettaStartedMs);
|
|
316
304
|
}
|
|
317
305
|
|
|
318
|
-
// Step 5.5 — usage emission + persistence. Only fire on successful
|
|
319
|
-
// termination (cancelled / failed turns may or may not have a
|
|
320
|
-
// usable runId, and the badge isn't load-bearing). Best-effort:
|
|
321
|
-
// any failure here logs and continues — we already emitted
|
|
322
|
-
// stream_end above, so the turn is "done" from the client's POV.
|
|
323
|
-
if (
|
|
324
|
-
lastRunId !== null &&
|
|
325
|
-
translatorState.status === "done"
|
|
326
|
-
) {
|
|
327
|
-
try {
|
|
328
|
-
const usage = await opts.lettaClient.getRunUsageStats(lastRunId);
|
|
329
|
-
if (usage && usage.lastAssistantMessageId) {
|
|
330
|
-
// Persist for /sessions/{id}/messages history rendering.
|
|
331
|
-
opts.db
|
|
332
|
-
.insert(messageUsage)
|
|
333
|
-
.values({
|
|
334
|
-
lettaMessageId: usage.lastAssistantMessageId,
|
|
335
|
-
sessionId: opts.sessionId,
|
|
336
|
-
promptTokens: usage.promptTokens,
|
|
337
|
-
completionTokens: usage.completionTokens,
|
|
338
|
-
totalTokens: usage.totalTokens,
|
|
339
|
-
cachedInputTokens: usage.cachedInputTokens,
|
|
340
|
-
cacheWriteTokens: usage.cacheWriteTokens,
|
|
341
|
-
reasoningTokens: usage.reasoningTokens,
|
|
342
|
-
contextTokens: usage.contextTokens,
|
|
343
|
-
runId: lastRunId,
|
|
344
|
-
})
|
|
345
|
-
.onConflictDoUpdate({
|
|
346
|
-
target: messageUsage.lettaMessageId,
|
|
347
|
-
set: {
|
|
348
|
-
promptTokens: usage.promptTokens,
|
|
349
|
-
completionTokens: usage.completionTokens,
|
|
350
|
-
totalTokens: usage.totalTokens,
|
|
351
|
-
cachedInputTokens: usage.cachedInputTokens,
|
|
352
|
-
cacheWriteTokens: usage.cacheWriteTokens,
|
|
353
|
-
reasoningTokens: usage.reasoningTokens,
|
|
354
|
-
contextTokens: usage.contextTokens,
|
|
355
|
-
runId: lastRunId,
|
|
356
|
-
},
|
|
357
|
-
})
|
|
358
|
-
.run();
|
|
359
|
-
// Live wire event — UI can attach the badge immediately
|
|
360
|
-
// without waiting for a /messages refetch.
|
|
361
|
-
opts.onEvent({
|
|
362
|
-
seq: 0, // emit() bypassed here; the stream's already drained.
|
|
363
|
-
event: "usage",
|
|
364
|
-
data: {
|
|
365
|
-
letta_message_id: usage.lastAssistantMessageId,
|
|
366
|
-
...(usage.promptTokens !== null ? { input: usage.promptTokens } : {}),
|
|
367
|
-
...(usage.completionTokens !== null ? { output: usage.completionTokens } : {}),
|
|
368
|
-
...(usage.cachedInputTokens !== null ? { cache_read: usage.cachedInputTokens } : {}),
|
|
369
|
-
...(usage.cacheWriteTokens !== null ? { cache_write: usage.cacheWriteTokens } : {}),
|
|
370
|
-
...(usage.reasoningTokens !== null ? { reasoning: usage.reasoningTokens } : {}),
|
|
371
|
-
...(usage.contextTokens !== null ? { context: usage.contextTokens } : {}),
|
|
372
|
-
...(usage.totalTokens !== null ? { total: usage.totalTokens } : {}),
|
|
373
|
-
},
|
|
374
|
-
state: { status: "done", phase: null } satisfies AIHubEventState,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
} catch (err) {
|
|
378
|
-
opts.logger.warn({ err, runId: lastRunId }, "usage-stats fetch/persist failed");
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
306
|
publisher.close();
|
|
383
307
|
opts.onEvent(null);
|
|
384
308
|
metrics.chatDurationMs.set(Date.now() - startedMs);
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Letta Code CLI shim — runs in front of the spawned CLI subprocess
|
|
4
|
-
* (via `LETTA_CLI_PATH`) and injects `--tools ""` into argv before
|
|
5
|
-
* loading the CLI bundle.
|
|
6
|
-
*
|
|
7
|
-
* **Why `--tools ""`.** The SDK doesn't expose the flag that actually
|
|
8
|
-
* empties the CLI's tool registry. It only exposes `--allowedTools` /
|
|
9
|
-
* `--disallowedTools`, which are runtime permission gates, not
|
|
10
|
-
* registry filters. The `--tools` flag is the only mechanism that
|
|
11
|
-
* makes the LLM see ONLY the agent's ADE-attached tools (equivalent
|
|
12
|
-
* to the TUI `/toolset none` action).
|
|
13
|
-
*
|
|
14
|
-
* **CLI resolution** — `require.resolve("@data-club/letta-code")` from
|
|
15
|
-
* node_modules. The DataClub fork is published to npmjs.org under the
|
|
16
|
-
* `@data-club` scope (forked from `@letta-ai/letta-code`); locally it's
|
|
17
|
-
* `link:`-overridden to `../letta-code-fork` via `pnpm.overrides`.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { createRequire } from "node:module";
|
|
21
|
-
|
|
22
|
-
const require = createRequire(import.meta.url);
|
|
23
|
-
const lettaCliPath = require.resolve("@data-club/letta-code");
|
|
24
|
-
|
|
25
|
-
// Rewrite argv to point at the real CLI + inject `--tools ""`. argv[0]
|
|
26
|
-
// (the node binary path) is preserved; argv[1] becomes the real CLI
|
|
27
|
-
// path so any path-relative resolution inside the CLI (e.g. finding
|
|
28
|
-
// the bundled `skills/` dir) works correctly.
|
|
29
|
-
process.argv = [
|
|
30
|
-
process.argv[0],
|
|
31
|
-
lettaCliPath,
|
|
32
|
-
"--tools",
|
|
33
|
-
"",
|
|
34
|
-
...process.argv.slice(2),
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
// Load the real CLI. The bundle's top-level code parses argv, sees
|
|
38
|
-
// our injected `--tools ""`, calls `toolFilter.setEnabledTools("")`,
|
|
39
|
-
// and starts up with an empty tool-registration allowlist.
|
|
40
|
-
await import(lettaCliPath);
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Seeds Letta Code's global agent-settings file (`~/.letta/settings.json`)
|
|
3
|
-
* with `{toolset: "none"}` for each ai-hub-registered Letta agent.
|
|
4
|
-
*
|
|
5
|
-
* **Why this matters.** Letta Code's bundled CLI auto-attaches a "toolset"
|
|
6
|
-
* (a set of file-edit / shell / search tools — `Read`, `Write`, `Edit`,
|
|
7
|
-
* `Bash`, etc.) to every agent at session start, based on the model handle.
|
|
8
|
-
* ai-hub doesn't want those tools — we want only the tools the ADE admin
|
|
9
|
-
* explicitly attached to the agent. The CLI honors a per-agent
|
|
10
|
-
* `toolset: "none"` preference stored in its global settings file, which
|
|
11
|
-
* suppresses the entire auto-toolset.
|
|
12
|
-
*
|
|
13
|
-
* **Why global, not project-local.** Letta Code's `SettingsManager` reads
|
|
14
|
-
* the toolset preference from `~/.letta/settings.json` (the global file),
|
|
15
|
-
* not `<cwd>/.letta/settings.local.json` (the project file). The global
|
|
16
|
-
* settings carry agent records keyed by `(agentId, normalizedBaseUrl)`
|
|
17
|
-
* where `normalizedBaseUrl` is the value of `LETTA_BASE_URL` with the
|
|
18
|
-
* scheme and trailing slash stripped, or `undefined` when the agent lives
|
|
19
|
-
* on Letta Cloud. Self-hosted Letta deployments always have a non-default
|
|
20
|
-
* `LETTA_BASE_URL` so the agent record always has the `baseUrl` field set.
|
|
21
|
-
*
|
|
22
|
-
* The `LETTA_CLI_PATH` shim (which injects `--tools ""`) is the primary
|
|
23
|
-
* mechanism that forces an empty tool registry; this settings-file seed
|
|
24
|
-
* is the defense-in-depth fallback the TUI also honors.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
existsSync,
|
|
29
|
-
mkdirSync,
|
|
30
|
-
readFileSync,
|
|
31
|
-
renameSync,
|
|
32
|
-
writeFileSync,
|
|
33
|
-
} from "node:fs";
|
|
34
|
-
import { homedir } from "node:os";
|
|
35
|
-
import { dirname, join } from "node:path";
|
|
36
|
-
|
|
37
|
-
import type { Logger } from "../observability/logger.js";
|
|
38
|
-
|
|
39
|
-
/* ────────────────────────────────────────────────────────────────────────── */
|
|
40
|
-
/* Settings file shape */
|
|
41
|
-
/* ────────────────────────────────────────────────────────────────────────── */
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Toolset preference values accepted by Letta Code. `"none"` is the
|
|
45
|
-
* documented-but-not-help-listed value that suppresses every auto-attached
|
|
46
|
-
* tool; the named toolsets (`codex`, `gemini`, `default`) attach specific
|
|
47
|
-
* sets of file/shell tools that ai-hub doesn't want.
|
|
48
|
-
*
|
|
49
|
-
* The string is treated as an opaque scalar by Letta Code's persistence
|
|
50
|
-
* layer; we still narrow the TypeScript type here so a typo in this file
|
|
51
|
-
* fails at compile time.
|
|
52
|
-
*/
|
|
53
|
-
export type ToolsetPreference = "none" | "auto" | "codex" | "codex_snake" | "gemini" | "gemini_snake" | "default";
|
|
54
|
-
|
|
55
|
-
/** One agent entry inside the global settings file. Mirrors Letta Code's
|
|
56
|
-
* internal shape so our writes round-trip with its own writes. Optional
|
|
57
|
-
* fields are intentionally absent when unset, matching Letta Code's
|
|
58
|
-
* delete-when-falsy convention. */
|
|
59
|
-
export interface AgentSettingsEntry {
|
|
60
|
-
agentId: string;
|
|
61
|
-
/** Normalized base URL ("localhost:8283/v1" style — no scheme, no
|
|
62
|
-
* trailing slash). Omitted for Letta Cloud agents. */
|
|
63
|
-
baseUrl?: string;
|
|
64
|
-
pinned?: boolean;
|
|
65
|
-
memfs?: boolean;
|
|
66
|
-
toolset?: ToolsetPreference;
|
|
67
|
-
systemPromptPreset?: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Shape of `~/.letta/settings.json` that we touch. We pass through every
|
|
71
|
-
* unknown top-level field unchanged so concurrent Letta Code edits don't
|
|
72
|
-
* lose data. */
|
|
73
|
-
export interface LettaCodeSettingsFile {
|
|
74
|
-
agents?: AgentSettingsEntry[];
|
|
75
|
-
/** Any other fields Letta Code writes (oauth state, env vars, …). We
|
|
76
|
-
* treat them as opaque on read + write. */
|
|
77
|
-
[other: string]: unknown;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/* ────────────────────────────────────────────────────────────────────────── */
|
|
81
|
-
/* Path resolution */
|
|
82
|
-
/* ────────────────────────────────────────────────────────────────────────── */
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Returns the absolute path to Letta Code's global settings file. Tests
|
|
86
|
-
* override `HOME` to point at a tmp dir.
|
|
87
|
-
*
|
|
88
|
-
* @returns the path. (Doesn't check existence — caller does.)
|
|
89
|
-
*/
|
|
90
|
-
export function resolveGlobalSettingsPath(): string {
|
|
91
|
-
return join(homedir(), ".letta", "settings.json");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Normalizes `LETTA_BASE_URL` to match the key Letta Code's CLI uses to
|
|
96
|
-
* look up agent settings AFTER ai-hub-server's `bootstrap.ts` runs its
|
|
97
|
-
* own `/v1` stripping. The CLI itself only strips scheme + trailing
|
|
98
|
-
* slash (so `http://host:p/v1` → `host:p/v1`), but bootstrap.ts mutates
|
|
99
|
-
* `process.env.LETTA_BASE_URL` at boot to drop the `/v1` (because the
|
|
100
|
-
* Letta Client SDK that the CLI uses internally appends `/v1` itself —
|
|
101
|
-
* leaving it in the env produces double-`/v1` requests that 404).
|
|
102
|
-
*
|
|
103
|
-
* So by the time the spawned CLI looks up the agent settings, its
|
|
104
|
-
* `getCurrentServerKey` reads `http://host:p` from env and produces
|
|
105
|
-
* `host:p`. The settings file entry must use the same key — hence the
|
|
106
|
-
* extra `/v1` strip here. If `bootstrap.ts` changes its policy, this
|
|
107
|
-
* helper has to change in lockstep.
|
|
108
|
-
*
|
|
109
|
-
* Returns `undefined` for the Letta Cloud default URL (so the agent
|
|
110
|
-
* record omits the `baseUrl` field, matching Letta Code's behavior on
|
|
111
|
-
* Cloud).
|
|
112
|
-
*
|
|
113
|
-
* @param baseUrl - raw `LETTA_BASE_URL` value.
|
|
114
|
-
* @returns the normalized form, or `undefined` for Letta Cloud.
|
|
115
|
-
*/
|
|
116
|
-
export function normalizeLettaBaseUrl(baseUrl: string | undefined): string | undefined {
|
|
117
|
-
if (!baseUrl) return undefined;
|
|
118
|
-
const stripped = baseUrl
|
|
119
|
-
.replace(/^https?:\/\//, "")
|
|
120
|
-
.replace(/\/v1\/?$/i, "")
|
|
121
|
-
.replace(/\/$/, "");
|
|
122
|
-
if (stripped === "api.letta.com") return undefined;
|
|
123
|
-
return stripped;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/* ────────────────────────────────────────────────────────────────────────── */
|
|
127
|
-
/* Read + write */
|
|
128
|
-
/* ────────────────────────────────────────────────────────────────────────── */
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Reads the settings file from disk. Returns an empty object if the file
|
|
132
|
-
* doesn't exist or is unparseable — both cases are recoverable by an
|
|
133
|
-
* idempotent upsert that recreates the file.
|
|
134
|
-
*
|
|
135
|
-
* @param path - the resolved settings path.
|
|
136
|
-
* @returns the parsed settings (or `{}`).
|
|
137
|
-
*/
|
|
138
|
-
export function readSettingsFile(path: string): LettaCodeSettingsFile {
|
|
139
|
-
if (!existsSync(path)) return {};
|
|
140
|
-
try {
|
|
141
|
-
const raw = readFileSync(path, "utf8");
|
|
142
|
-
if (!raw.trim()) return {};
|
|
143
|
-
const parsed = JSON.parse(raw);
|
|
144
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
145
|
-
return parsed as LettaCodeSettingsFile;
|
|
146
|
-
}
|
|
147
|
-
return {};
|
|
148
|
-
} catch {
|
|
149
|
-
// Malformed file (concurrent write, disk corruption) — treat as empty
|
|
150
|
-
// so the seed proceeds. Worst case the operator's other settings (if
|
|
151
|
-
// any) are clobbered, which is acceptable when the file was already
|
|
152
|
-
// broken.
|
|
153
|
-
return {};
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Writes the settings file atomically: write to `<path>.tmp` then rename.
|
|
159
|
-
* The rename is atomic on every filesystem the ai-hub-server supports
|
|
160
|
-
* (ext4, xfs, btrfs, apfs). Creates `~/.letta/` if missing.
|
|
161
|
-
*
|
|
162
|
-
* @param path - the resolved settings path.
|
|
163
|
-
* @param settings - the new contents.
|
|
164
|
-
*/
|
|
165
|
-
export function writeSettingsFile(path: string, settings: LettaCodeSettingsFile): void {
|
|
166
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
167
|
-
const tmp = `${path}.tmp`;
|
|
168
|
-
writeFileSync(tmp, JSON.stringify(settings, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
169
|
-
renameSync(tmp, path);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/* ────────────────────────────────────────────────────────────────────────── */
|
|
173
|
-
/* Upsert */
|
|
174
|
-
/* ────────────────────────────────────────────────────────────────────────── */
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Outcome of a single seed call. Useful for tests and observability.
|
|
178
|
-
*/
|
|
179
|
-
export type SeedOutcome =
|
|
180
|
-
| { kind: "created"; previousToolset: undefined }
|
|
181
|
-
| { kind: "updated"; previousToolset: ToolsetPreference | undefined }
|
|
182
|
-
| { kind: "noop"; previousToolset: ToolsetPreference };
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Idempotent upsert of `{toolset: "none"}` for a single agent in the
|
|
186
|
-
* global settings file.
|
|
187
|
-
*
|
|
188
|
-
* Matching is by `(agentId, baseUrl)` — same composite key Letta Code
|
|
189
|
-
* uses. If the agent record exists with a different toolset (`"auto"`,
|
|
190
|
-
* `"codex"`, etc.), it's replaced. If it exists already with `"none"`,
|
|
191
|
-
* we no-op (don't churn the file's mtime).
|
|
192
|
-
*
|
|
193
|
-
* @param opts.agentId - the Letta agent id (e.g. `agent-xxx`).
|
|
194
|
-
* @param opts.lettaBaseUrl - the raw `LETTA_BASE_URL` value (so the
|
|
195
|
-
* helper can normalize it the same way Letta Code does). Pass
|
|
196
|
-
* `process.env.LETTA_BASE_URL` from the caller.
|
|
197
|
-
* @param opts.settingsPath - override the settings-file path. Defaults to
|
|
198
|
-
* `~/.letta/settings.json`. Tests inject a tmp path.
|
|
199
|
-
* @param opts.logger - optional pino logger for observability lines.
|
|
200
|
-
* @returns the outcome (created / updated / noop) so the caller can log.
|
|
201
|
-
*/
|
|
202
|
-
export function seedAgentToolsetNone(opts: {
|
|
203
|
-
agentId: string;
|
|
204
|
-
lettaBaseUrl: string | undefined;
|
|
205
|
-
settingsPath?: string;
|
|
206
|
-
logger?: Logger;
|
|
207
|
-
}): SeedOutcome {
|
|
208
|
-
const path = opts.settingsPath ?? resolveGlobalSettingsPath();
|
|
209
|
-
const normalizedBaseUrl = normalizeLettaBaseUrl(opts.lettaBaseUrl);
|
|
210
|
-
const settings = readSettingsFile(path);
|
|
211
|
-
const agents: AgentSettingsEntry[] = Array.isArray(settings.agents) ? [...settings.agents] : [];
|
|
212
|
-
|
|
213
|
-
const matchIdx = agents.findIndex(
|
|
214
|
-
(a) => a.agentId === opts.agentId && (a.baseUrl ?? undefined) === normalizedBaseUrl,
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
let outcome: SeedOutcome;
|
|
218
|
-
|
|
219
|
-
if (matchIdx >= 0) {
|
|
220
|
-
const existing = agents[matchIdx]!;
|
|
221
|
-
const prev = existing.toolset;
|
|
222
|
-
if (prev === "none") {
|
|
223
|
-
outcome = { kind: "noop", previousToolset: "none" };
|
|
224
|
-
opts.logger?.debug({ agentId: opts.agentId, baseUrl: normalizedBaseUrl }, "letta_code_settings_seed_noop");
|
|
225
|
-
return outcome;
|
|
226
|
-
}
|
|
227
|
-
agents[matchIdx] = { ...existing, toolset: "none" };
|
|
228
|
-
outcome = { kind: "updated", previousToolset: prev };
|
|
229
|
-
} else {
|
|
230
|
-
const entry: AgentSettingsEntry = {
|
|
231
|
-
agentId: opts.agentId,
|
|
232
|
-
toolset: "none",
|
|
233
|
-
};
|
|
234
|
-
if (normalizedBaseUrl !== undefined) entry.baseUrl = normalizedBaseUrl;
|
|
235
|
-
agents.push(entry);
|
|
236
|
-
outcome = { kind: "created", previousToolset: undefined };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const next: LettaCodeSettingsFile = { ...settings, agents };
|
|
240
|
-
writeSettingsFile(path, next);
|
|
241
|
-
|
|
242
|
-
opts.logger?.info(
|
|
243
|
-
{
|
|
244
|
-
agentId: opts.agentId,
|
|
245
|
-
baseUrl: normalizedBaseUrl,
|
|
246
|
-
outcome: outcome.kind,
|
|
247
|
-
previousToolset: outcome.previousToolset,
|
|
248
|
-
settingsPath: path,
|
|
249
|
-
},
|
|
250
|
-
"letta_code_settings_seed",
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
return outcome;
|
|
254
|
-
}
|