@saadnvd1/agent-os 0.3.556 → 0.3.558

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.
Files changed (48) hide show
  1. package/README.md +1 -0
  2. package/app/api/experiments/[id]/route.ts +39 -0
  3. package/app/api/experiments/route.test.ts +51 -0
  4. package/app/api/experiments/route.ts +7 -0
  5. package/app/api/learning-cards/[id]/route.test.ts +62 -0
  6. package/app/api/learning-cards/[id]/route.ts +36 -0
  7. package/app/api/recipes/draft/route.ts +6 -1
  8. package/app/api/recipes/route.ts +3 -0
  9. package/app/api/sessions/[id]/learning-cards/route.test.ts +35 -0
  10. package/app/api/sessions/[id]/learning-cards/route.ts +10 -0
  11. package/components/Chat/ChatPanel.tsx +2 -0
  12. package/components/LearningCards/LearningCard.test.ts +156 -0
  13. package/components/LearningCards/LearningCard.tsx +210 -0
  14. package/components/Settings/ExperimentsSection.test.ts +89 -0
  15. package/components/Settings/ExperimentsSection.tsx +225 -0
  16. package/components/Settings/SettingsDialog.test.ts +3 -0
  17. package/components/Settings/SettingsDialog.tsx +9 -0
  18. package/components/Settings/commands.ts +9 -0
  19. package/components/Settings/entry-points.test.ts +1 -0
  20. package/data/experiments/index.ts +67 -0
  21. package/data/learning-cards/index.ts +79 -0
  22. package/lib/chat/store.ts +16 -0
  23. package/lib/db/migrations.ts +59 -0
  24. package/lib/done/index.ts +2 -0
  25. package/lib/experiments/hooks.ts +39 -0
  26. package/lib/experiments/index.ts +19 -0
  27. package/lib/experiments/meter.ts +327 -0
  28. package/lib/experiments/registry.ts +37 -0
  29. package/lib/experiments/view.ts +19 -0
  30. package/lib/learning-cards/__fixtures__/db.ts +52 -0
  31. package/lib/learning-cards/__fixtures__/sessions.ts +63 -0
  32. package/lib/learning-cards/analyze.ts +228 -0
  33. package/lib/learning-cards/apply.ts +196 -0
  34. package/lib/learning-cards/index.ts +29 -0
  35. package/lib/learning-cards/input.ts +116 -0
  36. package/lib/learning-cards/pairs.ts +111 -0
  37. package/lib/learning-cards/signals.ts +98 -0
  38. package/lib/learning-cards/store.ts +103 -0
  39. package/lib/learning-cards/types.ts +79 -0
  40. package/lib/orchestrator/quick.ts +14 -1
  41. package/lib/recipes/draft.ts +62 -33
  42. package/lib/recipes/settle.ts +4 -1
  43. package/lib/settings-store.ts +22 -0
  44. package/lib/settings-url.ts +1 -0
  45. package/lib/tasks/finish.ts +2 -0
  46. package/lib/tasks/session.ts +2 -0
  47. package/lib/updates/settings.ts +2 -20
  48. package/package.json +1 -1
@@ -0,0 +1,98 @@
1
+ // Zero-token check for "this session is probably a recipe".
2
+
3
+ import { parseResult } from "../recipes/result";
4
+ import type { SessionInput } from "./types";
5
+
6
+ // First words of a command that reach outside the repo.
7
+ export const EXTERNAL_CLIS = [
8
+ "asc",
9
+ "eas",
10
+ "tv",
11
+ "gh api",
12
+ "gh release",
13
+ "xcrun",
14
+ "fastlane",
15
+ "curl",
16
+ "aws",
17
+ "gcloud",
18
+ "kubectl",
19
+ ];
20
+
21
+ export interface RecipeSignals {
22
+ hit: boolean;
23
+ signals: string[];
24
+ }
25
+
26
+ // "cd x && FOO=1 npx eas submit" -> "eas submit".
27
+ export function commandStart(command: string): string {
28
+ const parts = command.split(/\s*(?:&&|;)\s*/).map((p) => p.trim());
29
+ const real = parts.find((p) => p && !/^cd\s/.test(p)) ?? "";
30
+ return real
31
+ .replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)+/, "")
32
+ .replace(/^(?:npx|bunx|sudo|time)\s+/, "");
33
+ }
34
+
35
+ export const isExternal = (command: string): boolean => {
36
+ const start = commandStart(command);
37
+ return EXTERNAL_CLIS.some((c) => start === c || start.startsWith(`${c} `));
38
+ };
39
+
40
+ const FACT_PATTERNS = [
41
+ /\b(?:build|run|release)\s*#?\d+/i,
42
+ /#\d{2,}/,
43
+ /\b\d+\.\d+\.\d+\b/,
44
+ /\b[0-9a-f]{7,}\b/i,
45
+ /^\s*[-*]?\s*[A-Za-z][\w ]{1,24}:\s+\S+/m,
46
+ ];
47
+
48
+ export function hasFacts(final: string): boolean {
49
+ if (parseResult(final)) return true;
50
+ return FACT_PATTERNS.filter((p) => p.test(final)).length >= 2;
51
+ }
52
+
53
+ const STOP = new Set(
54
+ "a an and are as at be but by do for from has have in into is it its of on or that the this to was we with you your please then than so if not can will just all any also".split(
55
+ " "
56
+ )
57
+ );
58
+
59
+ export function words(text: string): Set<string> {
60
+ return new Set(
61
+ text
62
+ .toLowerCase()
63
+ .split(/[^a-z0-9]+/)
64
+ .filter((w) => w.length > 1 && !STOP.has(w) && !/^\d+$/.test(w))
65
+ );
66
+ }
67
+
68
+ export function jaccard(a: Set<string>, b: Set<string>): number {
69
+ if (!a.size || !b.size) return 0;
70
+ let both = 0;
71
+ for (const w of a) if (b.has(w)) both++;
72
+ return both / (a.size + b.size - both);
73
+ }
74
+
75
+ export const REPEAT_AT = 0.6;
76
+ const MIN_WORDS = 4;
77
+
78
+ export function repeats(brief: string, earlier: string[]): boolean {
79
+ const mine = words(brief);
80
+ if (mine.size < MIN_WORDS) return false;
81
+ return earlier.some((b) => jaccard(mine, words(b)) >= REPEAT_AT);
82
+ }
83
+
84
+ export function recipeSignals(input: SessionInput): RecipeSignals {
85
+ if (input.skip || input.run) return { hit: false, signals: [] };
86
+ const signals: string[] = [];
87
+ const external = input.steps.some(
88
+ (s) => s.kind === "tool" && s.name === "Bash" && isExternal(s.command)
89
+ );
90
+ if (external) signals.push("external");
91
+ if (!input.inRepo) signals.push("outside-repo");
92
+ if (hasFacts(input.final)) signals.push("facts");
93
+ if (repeats(input.brief, input.recentBriefs)) signals.push("repeat");
94
+ const hit =
95
+ signals.includes("repeat") ||
96
+ ((external || !input.inRepo) && signals.includes("facts"));
97
+ return { hit, signals };
98
+ }
@@ -0,0 +1,103 @@
1
+ import { db } from "../db";
2
+ import type {
3
+ Card,
4
+ CardKind,
5
+ CardStatus,
6
+ Destination,
7
+ LessonPayload,
8
+ RecipePayload,
9
+ } from "./types";
10
+
11
+ interface Row extends Omit<Card, "payload"> {
12
+ payload: string;
13
+ }
14
+
15
+ const toCard = (r: Row): Card => ({ ...r, payload: JSON.parse(r.payload) });
16
+
17
+ export function getCard(id: number): Card | null {
18
+ const row = db
19
+ .prepare(`SELECT * FROM learning_cards WHERE id = ?`)
20
+ .get(id) as Row | undefined;
21
+ return row ? toCard(row) : null;
22
+ }
23
+
24
+ export function listCards(sessionId: string, status?: CardStatus): Card[] {
25
+ const rows = db
26
+ .prepare(
27
+ `SELECT * FROM learning_cards WHERE session_id = ?
28
+ AND (? IS NULL OR status = ?) ORDER BY id`
29
+ )
30
+ .all(sessionId, status ?? null, status ?? null) as Row[];
31
+ return rows.map(toCard);
32
+ }
33
+
34
+ // One card per kind per session; a second insert is ignored.
35
+ export function insertCard(c: {
36
+ sessionId: string;
37
+ workspaceId: string | null;
38
+ kind: CardKind;
39
+ payload: RecipePayload | LessonPayload;
40
+ destination?: Destination;
41
+ }): Card | null {
42
+ const { lastInsertRowid, changes } = db
43
+ .prepare(
44
+ `INSERT OR IGNORE INTO learning_cards
45
+ (session_id, workspace_id, kind, payload, destination, created_at)
46
+ VALUES (?, ?, ?, ?, ?, ?)`
47
+ )
48
+ .run(
49
+ c.sessionId,
50
+ c.workspaceId,
51
+ c.kind,
52
+ JSON.stringify(c.payload),
53
+ c.destination ?? null,
54
+ Date.now()
55
+ );
56
+ return changes ? getCard(Number(lastInsertRowid)) : null;
57
+ }
58
+
59
+ // Takes the card for deciding; false when someone else already did.
60
+ export function claimCard(id: number): boolean {
61
+ return (
62
+ db
63
+ .prepare(
64
+ `UPDATE learning_cards SET status = 'accepted' WHERE id = ? AND status = 'pending'`
65
+ )
66
+ .run(id).changes === 1
67
+ );
68
+ }
69
+
70
+ export function releaseCard(id: number): void {
71
+ db.prepare(
72
+ `UPDATE learning_cards SET status = 'pending' WHERE id = ? AND status = 'accepted' AND decided_at IS NULL`
73
+ ).run(id);
74
+ }
75
+
76
+ export function settleCard(
77
+ id: number,
78
+ status: "accepted" | "dismissed",
79
+ appliedRef: string | null,
80
+ payload?: RecipePayload | LessonPayload
81
+ ): void {
82
+ db.prepare(
83
+ `UPDATE learning_cards SET status = ?, applied_ref = ?, decided_at = ?,
84
+ payload = COALESCE(?, payload) WHERE id = ?`
85
+ ).run(
86
+ status,
87
+ appliedRef,
88
+ Date.now(),
89
+ payload ? JSON.stringify(payload) : null,
90
+ id
91
+ );
92
+ }
93
+
94
+ // First time a card is read: stamp it, and say whether this call did.
95
+ export function markShown(id: number): boolean {
96
+ return (
97
+ db
98
+ .prepare(
99
+ `UPDATE learning_cards SET shown_at = ? WHERE id = ? AND shown_at IS NULL`
100
+ )
101
+ .run(Date.now(), id).changes === 1
102
+ );
103
+ }
@@ -0,0 +1,79 @@
1
+ export type CardKind = "recipe" | "lesson";
2
+ export type CardStatus = "pending" | "accepted" | "dismissed";
3
+ export type Destination = "recipe" | "claude_md" | "check" | "rule";
4
+
5
+ export interface RecipePayload {
6
+ signals: string[];
7
+ brief: string;
8
+ summary: string;
9
+ }
10
+
11
+ export interface LessonPayload {
12
+ lesson: string;
13
+ destination: Destination;
14
+ recipeId?: string;
15
+ recipeName?: string;
16
+ projectId?: string;
17
+ projectName?: string;
18
+ taskPrompt?: string;
19
+ pairs: number;
20
+ }
21
+
22
+ export interface Card {
23
+ id: number;
24
+ session_id: string;
25
+ workspace_id: string | null;
26
+ kind: CardKind;
27
+ status: CardStatus;
28
+ payload: RecipePayload | LessonPayload;
29
+ destination: Destination | null;
30
+ applied_ref: string | null;
31
+ shown_at: number | null;
32
+ decided_at: number | null;
33
+ created_at: number;
34
+ }
35
+
36
+ // One step of a session, cut down to what the scripts read.
37
+ export type Step =
38
+ | { kind: "user"; text: string; byPerson: boolean }
39
+ | {
40
+ kind: "tool";
41
+ name: string;
42
+ ok: boolean;
43
+ failed: boolean;
44
+ command: string;
45
+ path: string;
46
+ input: string;
47
+ output: string;
48
+ };
49
+
50
+ export interface RunInfo {
51
+ status: string;
52
+ summary: string;
53
+ error: string;
54
+ // Settled: not queued, starting or running.
55
+ settled: boolean;
56
+ }
57
+
58
+ // What the scripts know about one session. Never the transcript.
59
+ export interface SessionInput {
60
+ id: string;
61
+ name: string;
62
+ workspaceId: string | null;
63
+ projectId: string | null;
64
+ skip: string | null;
65
+ brief: string;
66
+ final: string;
67
+ steps: Step[];
68
+ reviews: { status: string; detail: string }[];
69
+ inRepo: boolean;
70
+ run: (RunInfo & { recipeId: string }) | null;
71
+ previousRun: RunInfo | null;
72
+ recentBriefs: string[];
73
+ }
74
+
75
+ export interface Pair {
76
+ kind: "tool-retry" | "review" | "user-correction" | "recipe-retry";
77
+ before: string;
78
+ after: string;
79
+ }
@@ -53,6 +53,15 @@ export async function quickChange(
53
53
  projectRef: string,
54
54
  name: string
55
55
  ): Promise<string> {
56
+ return (await startQuickChange(workspaceId, projectRef, name)).text;
57
+ }
58
+
59
+ // The same, for a caller that needs the new session's id and worktree.
60
+ export async function startQuickChange(
61
+ workspaceId: string,
62
+ projectRef: string,
63
+ name: string
64
+ ): Promise<{ id: string; worktreePath: string; text: string }> {
56
65
  const owner = ownerOf(workspaceId);
57
66
  const project = workspaceProject(workspaceId, projectRef);
58
67
  if (isRemoteHost(project.host_id))
@@ -107,7 +116,11 @@ export async function quickChange(
107
116
  workspaceId,
108
117
  `Quick change "${label}" in ${project.name} on ${wt.branchName}.`
109
118
  );
110
- return `Quick change "${label}" (id ${id.slice(0, 8)}) in ${project.name}: worktree ${wt.worktreePath} on ${wt.branchName}, cut from ${baseBranch}. Edit files there with Write and Edit; git there only as \`git -C ${wt.worktreePath} <status|diff|add|...>\`. Then quick_pr commits, pushes and opens its PR.`;
119
+ return {
120
+ id,
121
+ worktreePath: wt.worktreePath,
122
+ text: `Quick change "${label}" (id ${id.slice(0, 8)}) in ${project.name}: worktree ${wt.worktreePath} on ${wt.branchName}, cut from ${baseBranch}. Edit files there with Write and Edit; git there only as \`git -C ${wt.worktreePath} <status|diff|add|...>\`. Then quick_pr commits, pushes and opens its PR.`,
123
+ };
111
124
  }
112
125
 
113
126
  // A quick change of this workspace's orchestrator, by name, id or PR.
@@ -40,10 +40,10 @@ const SCHEMA = {
40
40
  required: ["brief", "summary"],
41
41
  };
42
42
 
43
- export async function recipeDraft(
44
- sessionId: string,
45
- run: ClaudeRunner = runClaude
46
- ): Promise<RecipeDraft> {
43
+ export type RecipePrefill = Omit<RecipeDraft, "draft">;
44
+
45
+ // The form's deterministic part; refuses a session that can't become a recipe.
46
+ export function recipePrefill(sessionId: string): RecipePrefill {
47
47
  const s = queries.getSession(db).get(sessionId) as Session | undefined;
48
48
  if (!s) throw new Error("Session not found");
49
49
  const project = s.project_id ? getProject(s.project_id) : null;
@@ -52,34 +52,6 @@ export async function recipeDraft(
52
52
  const first = itemsOfKind(s.id, "user")[0]?.text ?? "";
53
53
  const prompt = (s.task_prompt || first).trim().slice(0, MAX_BRIEF);
54
54
  if (!prompt) throw new Error("That session has no prompt to save");
55
- const final = finalMessage(s.id);
56
- let draft: RecipeDraft["draft"] = null;
57
- try {
58
- const out = (await run({
59
- cwd: os.tmpdir(),
60
- system: SYSTEM,
61
- prompt: [
62
- `What it was asked:\n${prompt.slice(0, 1500)}`,
63
- final ? `What it ended with:\n${final.slice(-2000)}` : "",
64
- ]
65
- .filter(Boolean)
66
- .join("\n\n"),
67
- schema: SCHEMA,
68
- tools: [],
69
- role: "helper",
70
- timeoutMs: 20_000,
71
- })) as { brief?: unknown; summary?: unknown };
72
- if (typeof out?.brief === "string" && out.brief.trim())
73
- draft = {
74
- brief: out.brief.trim().slice(0, MAX_BRIEF),
75
- summary: typeof out.summary === "string" ? out.summary.trim() : "",
76
- };
77
- } catch (error) {
78
- console.warn(
79
- "[recipes] no drafted brief:",
80
- error instanceof Error ? error.message : error
81
- );
82
- }
83
55
  return {
84
56
  name: recipeNameFrom(s.name),
85
57
  workspaceId: project.workspace_id,
@@ -88,6 +60,63 @@ export async function recipeDraft(
88
60
  model: s.model_overrides ?? null,
89
61
  view: s.view,
90
62
  prompt,
91
- draft,
92
63
  };
93
64
  }
65
+
66
+ export const REPEATABLE_SYSTEM = `${SYSTEM} Also say whether this is a job someone would plausibly run again (repeatable); a one-off fix or a feature built once is not.`;
67
+ const REPEATABLE_SCHEMA = {
68
+ ...SCHEMA,
69
+ properties: { ...SCHEMA.properties, repeatable: { type: "boolean" } },
70
+ required: ["brief", "summary", "repeatable"],
71
+ };
72
+
73
+ // The model's rewrite of a brief and final report (never the transcript).
74
+ export const draftRequest = (
75
+ prompt: string,
76
+ final: string | null,
77
+ withRepeatable = false
78
+ ) => ({
79
+ cwd: os.tmpdir(),
80
+ system: withRepeatable ? REPEATABLE_SYSTEM : SYSTEM,
81
+ prompt: [
82
+ `What it was asked:\n${prompt.slice(0, 1500)}`,
83
+ final ? `What it ended with:\n${final.slice(-2000)}` : "",
84
+ ]
85
+ .filter(Boolean)
86
+ .join("\n\n"),
87
+ schema: withRepeatable ? REPEATABLE_SCHEMA : SCHEMA,
88
+ tools: [] as string[],
89
+ role: "helper" as const,
90
+ timeoutMs: 20_000,
91
+ });
92
+
93
+ export function parseDraft(out: unknown): RecipeDraft["draft"] {
94
+ const o = out as { brief?: unknown; summary?: unknown } | null;
95
+ if (typeof o?.brief !== "string" || !o.brief.trim()) return null;
96
+ return {
97
+ brief: o.brief.trim().slice(0, MAX_BRIEF),
98
+ summary: typeof o.summary === "string" ? o.summary.trim() : "",
99
+ };
100
+ }
101
+
102
+ export async function recipeDraft(
103
+ sessionId: string,
104
+ run: ClaudeRunner = runClaude,
105
+ // A brief already drafted for this session (a learning card's).
106
+ cached?: RecipeDraft["draft"]
107
+ ): Promise<RecipeDraft> {
108
+ const base = recipePrefill(sessionId);
109
+ if (cached) return { ...base, draft: cached };
110
+ let draft: RecipeDraft["draft"] = null;
111
+ try {
112
+ draft = parseDraft(
113
+ await run(draftRequest(base.prompt, finalMessage(sessionId)))
114
+ );
115
+ } catch (error) {
116
+ console.warn(
117
+ "[recipes] no drafted brief:",
118
+ error instanceof Error ? error.message : error
119
+ );
120
+ }
121
+ return { ...base, draft };
122
+ }
@@ -11,6 +11,7 @@ import { blockedReason } from "../tasks/state";
11
11
  import { runClaude, type ClaudeRunner } from "../orchestrator/claude-cli";
12
12
  import { releasePieces } from "../plans/release";
13
13
  import { queueEvent } from "../orchestrator/events";
14
+ import { sessionEnded } from "../experiments/hooks";
14
15
  import { untrusted } from "../orchestrator/untrusted";
15
16
  import {
16
17
  cleanOutputs,
@@ -172,8 +173,10 @@ function finish(
172
173
  run.id
173
174
  ).changes;
174
175
  idleSince.delete(run.id);
175
- if (changed)
176
+ if (changed) {
176
177
  tell(run, status, summary, result, session?.pr_number ?? run.pr_number);
178
+ if (run.session_id) void sessionEnded(run.session_id, "recipe-settled");
179
+ }
177
180
  return changed === 1;
178
181
  }
179
182
 
@@ -0,0 +1,22 @@
1
+ import { getDb } from "@/lib/db";
2
+
3
+ // The settings table: a key and a string, shared by every feature.
4
+ export function getSetting(key: string): string | null {
5
+ const row = getDb()
6
+ .prepare(`SELECT value FROM settings WHERE key = ?`)
7
+ .get(key) as { value: string } | undefined;
8
+ return row?.value ?? null;
9
+ }
10
+
11
+ export function putSetting(key: string, value: string | null): void {
12
+ if (value === null) {
13
+ getDb().prepare(`DELETE FROM settings WHERE key = ?`).run(key);
14
+ return;
15
+ }
16
+ getDb()
17
+ .prepare(
18
+ `INSERT INTO settings (key, value) VALUES (?, ?)
19
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
20
+ )
21
+ .run(key, value);
22
+ }
@@ -16,6 +16,7 @@ export const SETTINGS_SECTIONS = [
16
16
  "audits",
17
17
  "setup",
18
18
  "updates",
19
+ "experiments",
19
20
  ] as const;
20
21
 
21
22
  export type SettingsSection = (typeof SETTINGS_SECTIONS)[number];
@@ -26,6 +26,7 @@ import { canSignOff } from "./state";
26
26
  import { forgetPR, getTaskSession, prFor, projectPathFor } from "./session";
27
27
  import { dropSessionDatabase } from "../project-config/database";
28
28
  import { captureMergedTask } from "../session-logs";
29
+ import { sessionEnded } from "../experiments/hooks";
29
30
 
30
31
  // Never deleted, whatever a setting says.
31
32
  export const PROTECTED_BRANCHES = new Set(["main", "master", "HEAD"]);
@@ -163,6 +164,7 @@ export async function signOffTask(
163
164
  pushMerged(id);
164
165
  // Reads the plan file now: cleanup below removes the worktree.
165
166
  void captureMergedTask(id, { head: opts.head ?? pr!.head ?? null });
167
+ void sessionEnded(id, "merged");
166
168
  const head = opts.head ?? pr!.head;
167
169
  resolveMergedTaskAsks(
168
170
  id,
@@ -16,6 +16,7 @@ import { findPRStrict } from "./gh";
16
16
  import { currentBranch, git } from "../git-exec";
17
17
  import { lookupPR } from "./pr-poll";
18
18
  import { captureMergedTask } from "../session-logs";
19
+ import { sessionEnded } from "../experiments/hooks";
19
20
  import { getOrchestrator } from "../orchestrator/home";
20
21
  import {
21
22
  pushCiFailed,
@@ -211,6 +212,7 @@ export async function prFor(
211
212
  if (pr.state === "MERGED") {
212
213
  if (session.pr_status !== "merged") pushMerged(session.id);
213
214
  void captureMergedTask(session.id, { head: pr.head });
215
+ void sessionEnded(session.id, "merged");
214
216
  }
215
217
  }
216
218
  // Only an answer just read from GitHub: a cached one may predate a
@@ -1,4 +1,4 @@
1
- import { getDb } from "@/lib/db";
1
+ import { getSetting, putSetting } from "../settings-store";
2
2
 
3
3
  // Whether this server shows the update banner, and the checks behind it.
4
4
  // AGENTOS_UPDATE_BANNER=off pins it off, like the network settings.
@@ -17,22 +17,4 @@ export function bannerEnabled(env: Env = process.env): boolean {
17
17
  export const setBannerEnabled = (on: boolean) =>
18
18
  putSetting(KEY, on ? "1" : "0");
19
19
 
20
- export function getSetting(key: string): string | null {
21
- const row = getDb()
22
- .prepare(`SELECT value FROM settings WHERE key = ?`)
23
- .get(key) as { value: string } | undefined;
24
- return row?.value ?? null;
25
- }
26
-
27
- export function putSetting(key: string, value: string | null): void {
28
- if (value === null) {
29
- getDb().prepare(`DELETE FROM settings WHERE key = ?`).run(key);
30
- return;
31
- }
32
- getDb()
33
- .prepare(
34
- `INSERT INTO settings (key, value) VALUES (?, ?)
35
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
36
- )
37
- .run(key, value);
38
- }
20
+ export { getSetting, putSetting } from "../settings-store";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.556",
3
+ "version": "0.3.558",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"