bazilion 0.1.1 → 0.2.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.
@@ -0,0 +1,179 @@
1
+ -- Bazilion initial schema (consolidated, post-IPC + secrets-in-DB cleanup).
2
+ --
3
+ -- The daemon is the sole owner of `~/.bazilion/`. All state — entity
4
+ -- metadata, inbox, scheduler, web tokens, encrypted secrets, plaintext
5
+ -- config — lives in this single SQLite file. The only other file at the
6
+ -- bazilion home root is `auth.json`: a tiny bootstrap shared by the daemon
7
+ -- (which uses its `token` as the PBKDF2 seed for the `secrets` table) and
8
+ -- the CLI (which uses the same token as its loopback bearer).
9
+ --
10
+ -- On-disk layout owned by the daemon:
11
+ -- ~/.bazilion/bazilion.db
12
+ -- ~/.bazilion/groups/<slug>/ — group root, mounted as cwd; may be a
13
+ -- symlink for "agents working on my
14
+ -- existing project tree" use cases
15
+ -- /memory/ — qmd index for the group (shared by
16
+ -- all member agents)
17
+ -- ~/.bazilion/agents/<id>/ — agent's private home (NEVER inside
18
+ -- the group tree, so the cwd-rooted
19
+ -- coding tools can't see/clobber it)
20
+ -- /sessions/*.jsonl — pi transcripts
21
+ -- ~/.bazilion/profiles/<id>/ — profile templates
22
+ -- ~/.bazilion/skills/<name>/ — installed skills
23
+ -- ~/.bazilion/logs/ — daemon logs
24
+
25
+ -- Groups are collaboration contexts: one filesystem root, one USER.md, one
26
+ -- roster of member agents. The id IS the slug IS the directory name under
27
+ -- `~/.bazilion/groups/`. No `path` column — paths derive from id.
28
+ -- `user_md` is read-only context about the human (edited via web UI; never
29
+ -- exposed as a file the agent could clobber via `edit`/`write`).
30
+ CREATE TABLE groups (
31
+ id TEXT PRIMARY KEY,
32
+ name TEXT NOT NULL,
33
+ user_md TEXT NOT NULL DEFAULT '',
34
+ created_at INTEGER NOT NULL
35
+ );
36
+
37
+ -- Profiles are agent templates. `dir` is the on-disk directory for the
38
+ -- profile's SOUL.md / IDENTITY.md / etc. master files, derived from id.
39
+ -- `skills_mode = 'all'` attaches every installed skill at spawn;
40
+ -- `'selected'` attaches only those listed in `profile_default_skills`.
41
+ CREATE TABLE profiles (
42
+ id TEXT PRIMARY KEY,
43
+ name TEXT NOT NULL,
44
+ dir TEXT NOT NULL,
45
+ default_model TEXT NOT NULL,
46
+ skills_mode TEXT NOT NULL DEFAULT 'selected' CHECK (skills_mode IN ('all','selected')),
47
+ created_at INTEGER NOT NULL,
48
+ updated_at INTEGER NOT NULL
49
+ );
50
+
51
+ CREATE TABLE profile_default_skills (
52
+ profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
53
+ skill_name TEXT NOT NULL,
54
+ PRIMARY KEY (profile_id, skill_name)
55
+ );
56
+
57
+ -- Agents have exactly one group. `dir` is the agent's private home,
58
+ -- ~/.bazilion/agents/<id>/ — strictly outside the group tree so that
59
+ -- pi's coding tools (rooted at the group dir) can't reach into it.
60
+ -- `reasoning_level` feeds pi-ai's streamSimple `reasoning` option;
61
+ -- 'medium' is the sensible default.
62
+ CREATE TABLE agents (
63
+ id TEXT PRIMARY KEY,
64
+ profile_id TEXT NOT NULL REFERENCES profiles(id),
65
+ name TEXT NOT NULL,
66
+ model_override TEXT,
67
+ status TEXT NOT NULL CHECK (status IN ('idle','running','archived')),
68
+ dir TEXT NOT NULL,
69
+ reasoning_level TEXT NOT NULL DEFAULT 'medium',
70
+ group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE RESTRICT,
71
+ created_at INTEGER NOT NULL,
72
+ archived_at INTEGER
73
+ );
74
+
75
+ CREATE TABLE agent_skills (
76
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
77
+ skill_name TEXT NOT NULL,
78
+ attached_at INTEGER NOT NULL,
79
+ PRIMARY KEY (agent_id, skill_name)
80
+ );
81
+
82
+ -- Periodic / scheduled wake-up triggers. The scheduler picks rows whose
83
+ -- next-fire time has elapsed and kicks off a turn with `message` as the
84
+ -- user input.
85
+ --
86
+ -- kind='interval' → interval_sec holds seconds between fires (cron_expr NULL)
87
+ -- kind='cron' → cron_expr holds a 5-field cron expression (interval_sec NULL)
88
+ --
89
+ -- last_fired_at is bumped *before* the run kicks off so a daemon restart
90
+ -- mid-fire doesn't double-trigger.
91
+ CREATE TABLE agent_triggers (
92
+ id TEXT PRIMARY KEY,
93
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
94
+ kind TEXT NOT NULL CHECK (kind IN ('interval','cron')),
95
+ interval_sec INTEGER,
96
+ cron_expr TEXT,
97
+ message TEXT NOT NULL,
98
+ enabled INTEGER NOT NULL DEFAULT 1,
99
+ last_fired_at INTEGER,
100
+ created_at INTEGER NOT NULL
101
+ );
102
+ CREATE INDEX agent_triggers_agent ON agent_triggers(agent_id);
103
+ CREATE INDEX agent_triggers_enabled ON agent_triggers(enabled) WHERE enabled = 1;
104
+
105
+ -- Inter-agent inbox. The scheduler's auto-deliver loop polls
106
+ -- `messages_to_unread` to fan out wakeup turns; the messaging tools insert
107
+ -- here when one agent sends another a message.
108
+ CREATE TABLE messages (
109
+ id TEXT PRIMARY KEY,
110
+ from_agent_id TEXT NOT NULL REFERENCES agents(id),
111
+ to_agent_id TEXT NOT NULL REFERENCES agents(id),
112
+ reply_to TEXT REFERENCES messages(id),
113
+ payload TEXT NOT NULL,
114
+ created_at INTEGER NOT NULL,
115
+ read_at INTEGER
116
+ );
117
+ CREATE INDEX messages_to_unread ON messages(to_agent_id) WHERE read_at IS NULL;
118
+
119
+ -- Per-skill import provenance. Skills live on disk under ~/.bazilion/skills/<name>/;
120
+ -- this table records where each was imported from and when.
121
+ CREATE TABLE skill_meta (
122
+ name TEXT PRIMARY KEY,
123
+ source TEXT,
124
+ imported_at INTEGER
125
+ );
126
+
127
+ -- Per-client web tokens. Each row stores SHA-256(token) hex; the plaintext
128
+ -- is returned exactly once at creation time and never persisted in this
129
+ -- table. The CLI's bootstrap token is one row here — its plaintext lives
130
+ -- in `~/.bazilion/auth.json` so both the daemon (PBKDF2 seed for the
131
+ -- secrets table) and the CLI (loopback bearer) can read it.
132
+ -- `revoked_at` is a soft-delete marker to keep audit trail intact.
133
+ CREATE TABLE web_tokens (
134
+ id TEXT PRIMARY KEY,
135
+ label TEXT NOT NULL,
136
+ token_hash TEXT NOT NULL UNIQUE,
137
+ created_at INTEGER NOT NULL,
138
+ last_used_at INTEGER,
139
+ revoked_at INTEGER
140
+ );
141
+ CREATE INDEX web_tokens_active ON web_tokens(token_hash) WHERE revoked_at IS NULL;
142
+
143
+ -- Curated list of models per provider. Drives the model dropdowns in
144
+ -- profile creation and agent spawn/edit forms.
145
+ CREATE TABLE provider_models (
146
+ provider TEXT NOT NULL,
147
+ model TEXT NOT NULL,
148
+ added_at INTEGER NOT NULL,
149
+ PRIMARY KEY (provider, model)
150
+ );
151
+ CREATE INDEX idx_provider_models_provider ON provider_models (provider);
152
+
153
+ -- Admin-toggled enabled state per provider.
154
+ CREATE TABLE provider_state (
155
+ provider_id TEXT NOT NULL PRIMARY KEY,
156
+ enabled INTEGER NOT NULL DEFAULT 0,
157
+ updated_at INTEGER NOT NULL
158
+ );
159
+
160
+ -- Encrypted secrets. AES-256-GCM envelopes (salt+iv+tag+data hex JSON),
161
+ -- one row per env-var-shaped key (`ANTHROPIC_API_KEY`, `OPENAI_CODEX_OAUTH`, …).
162
+ -- The encryption key is derived from the bootstrap web token (PBKDF2-SHA256,
163
+ -- 100k iterations) — same crypto as the previous file-based secrets.enc,
164
+ -- now atomic with the rest of the DB.
165
+ CREATE TABLE secrets (
166
+ key TEXT PRIMARY KEY,
167
+ envelope TEXT NOT NULL,
168
+ updated_at INTEGER NOT NULL
169
+ );
170
+
171
+ -- Plaintext config. Same env-var-shaped keys as secrets, but for values
172
+ -- that don't need confidentiality (server URLs, region slugs, project IDs).
173
+ -- The application layer enforces a CONFIG_KEYS allowlist on writes so a
174
+ -- typo can't put a secret in this table.
175
+ CREATE TABLE config (
176
+ key TEXT PRIMARY KEY,
177
+ value TEXT NOT NULL,
178
+ updated_at INTEGER NOT NULL
179
+ );
@@ -0,0 +1,39 @@
1
+ -- Profile groups — preconfigured team templates. A ProfileGroup captures
2
+ -- the recipe for a team (ordered slots, each pointing at a Profile with
3
+ -- optional per-slot overrides) so an operator can replay it with a single
4
+ -- spawn call instead of clicking through agent-create N times per project.
5
+ --
6
+ -- Strictly additive: the single-profile `POST /api/agents` spawn path is
7
+ -- untouched. Profile groups bundle existing primitives (profiles, groups,
8
+ -- agents), they don't replace them.
9
+ --
10
+ -- Spawn semantics live in apps/daemon/src/core/profile-group/spawn.ts.
11
+ -- See docs/backlog/todo/BAZ-002-profile-groups.md for the full spec.
12
+
13
+ CREATE TABLE profile_groups (
14
+ id TEXT PRIMARY KEY, -- slug, e.g. "platform-team"
15
+ name TEXT NOT NULL, -- display name
16
+ user_md TEXT, -- optional starter USER.md; seeded only into a freshly-created target group (pre-existing groups are left alone — see Decision #5)
17
+ created_at INTEGER NOT NULL,
18
+ updated_at INTEGER NOT NULL
19
+ );
20
+
21
+ -- Member ordering uses an explicit `position` column rather than insertion
22
+ -- order so the spawn loop is deterministic and rollback can identify which
23
+ -- members succeeded. PK includes position so the same template can have
24
+ -- multiple members pointing at the same profile (e.g. "two reviewers" —
25
+ -- duplicate agent_name values are intentionally accepted; the spawn op
26
+ -- auto-suffixes collisions with `-2`, `-3`, ... at spawn time).
27
+ --
28
+ -- `ON DELETE RESTRICT` on profile_id prevents deleting a profile that a
29
+ -- profile-group member still references; the existing single-profile delete
30
+ -- keeps working unchanged because it never had a referrer before.
31
+ CREATE TABLE profile_group_members (
32
+ profile_group_id TEXT NOT NULL REFERENCES profile_groups(id) ON DELETE CASCADE,
33
+ position INTEGER NOT NULL,
34
+ profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE RESTRICT,
35
+ agent_name TEXT NOT NULL, -- e.g. "planner", "reviewer"
36
+ model_override TEXT, -- nullable; falls back to profile.default_model
37
+ reasoning_level TEXT, -- nullable; falls back to spawn default 'medium'
38
+ PRIMARY KEY (profile_group_id, position)
39
+ );
package/dist/worker.js CHANGED
@@ -32,7 +32,7 @@ import { existsSync as existsSync4, readdirSync } from "fs";
32
32
  import { join as join3 } from "path";
33
33
 
34
34
  // ../daemon/src/core/db/client.ts
35
- import { DatabaseSync } from "sqlite";
35
+ import { DatabaseSync } from "node:sqlite";
36
36
 
37
37
  // ../daemon/src/core/db/migrate.ts
38
38
  import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
@@ -76,6 +76,12 @@ import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
76
76
  import { writeFileSync as writeFileSync3 } from "fs";
77
77
  import { join as join7 } from "path";
78
78
 
79
+ // ../daemon/src/core/profile-group/spawn.ts
80
+ import { readdirSync as readdirSync3, rmSync as rmSync4 } from "fs";
81
+
82
+ // ../daemon/src/core/profile-group/rm-with-retry.ts
83
+ import { rmSync as rmSync3 } from "fs";
84
+
79
85
  // ../daemon/src/core/services.ts
80
86
  var SERVICES = [
81
87
  // --- LLM providers (configured via API keys / URLs) ---
@@ -446,7 +452,7 @@ import { createHash, randomBytes as randomBytes2, randomUUID as randomUUID4 } fr
446
452
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
447
453
 
448
454
  // ../daemon/src/core/skills/import.ts
449
- import { cpSync, existsSync as existsSync7, mkdtempSync, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2 } from "fs";
455
+ import { cpSync, existsSync as existsSync7, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync5, statSync as statSync2 } from "fs";
450
456
  import { tmpdir } from "os";
451
457
  import { basename, join as join8, resolve as resolve2, sep } from "path";
452
458
  import AdmZip from "adm-zip";
@@ -459,9 +465,9 @@ import { parse as parseYaml } from "yaml";
459
465
  import {
460
466
  existsSync as existsSync8,
461
467
  mkdirSync as mkdirSync4,
462
- readdirSync as readdirSync4,
468
+ readdirSync as readdirSync5,
463
469
  readFileSync as readFileSync6,
464
- rmSync as rmSync4,
470
+ rmSync as rmSync6,
465
471
  statSync as statSync3,
466
472
  writeFileSync as writeFileSync4
467
473
  } from "fs";
@@ -494,7 +500,7 @@ function safeKey(root, key) {
494
500
  }
495
501
  function walkMd(dir, prefix, out) {
496
502
  if (!existsSync8(dir)) return;
497
- for (const e of readdirSync4(dir, { withFileTypes: true })) {
503
+ for (const e of readdirSync5(dir, { withFileTypes: true })) {
498
504
  if (e.name.startsWith(".")) continue;
499
505
  const full = join9(dir, e.name);
500
506
  const key = prefix ? `${prefix}/${e.name}` : e.name;
@@ -569,7 +575,7 @@ function qmdBackend(root) {
569
575
  },
570
576
  async remove(key) {
571
577
  const path = safeKey(root, key);
572
- if (existsSync8(path)) rmSync4(path);
578
+ if (existsSync8(path)) rmSync6(path);
573
579
  const store = await getStore(root);
574
580
  await store.update();
575
581
  }
@@ -695,7 +701,7 @@ function piMessagesToProviderView(messages) {
695
701
  }
696
702
 
697
703
  // ../daemon/src/runtime/pi/session.ts
698
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
704
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync7, statSync as statSync5 } from "fs";
699
705
  import { basename as basename2, join as join13 } from "path";
700
706
  import {
701
707
  AuthStorage,
@@ -856,7 +862,7 @@ This group's USER.md is empty. As you learn STABLE facts about the human (prefer
856
862
  import { Type as Type2 } from "typebox";
857
863
 
858
864
  // ../daemon/src/runtime/tools/bootstrap.ts
859
- import { existsSync as existsSync10, rmSync as rmSync5 } from "fs";
865
+ import { existsSync as existsSync10, rmSync as rmSync7 } from "fs";
860
866
  import { join as join11 } from "path";
861
867
  function bootstrapTool(agentDir) {
862
868
  return {
@@ -868,7 +874,7 @@ function bootstrapTool(agentDir) {
868
874
  async invoke() {
869
875
  const path = join11(agentDir, "BOOTSTRAP.md");
870
876
  if (existsSync10(path)) {
871
- rmSync5(path);
877
+ rmSync7(path);
872
878
  return "BOOTSTRAP.md removed. Bootstrap is complete.";
873
879
  }
874
880
  return "BOOTSTRAP.md was already removed.";
@@ -877,7 +883,7 @@ function bootstrapTool(agentDir) {
877
883
  }
878
884
 
879
885
  // ../daemon/src/runtime/tools/home.ts
880
- import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
886
+ import { readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
881
887
  import { join as join12 } from "path";
882
888
  var HOME_FILES_READABLE = [
883
889
  "IDENTITY.md",
@@ -969,7 +975,7 @@ function homeTools(agentDir) {
969
975
  if (entries.length === 0) {
970
976
  const dirEntries = (() => {
971
977
  try {
972
- return readdirSync5(agentDir);
978
+ return readdirSync6(agentDir);
973
979
  } catch {
974
980
  return [];
975
981
  }
@@ -1754,13 +1760,7 @@ function webTools(opts) {
1754
1760
  extracted = { text: body };
1755
1761
  }
1756
1762
  if (isHtml && !firecrawlDisabled && extracted.text.length < FIRECRAWL_FALLBACK_THRESHOLD) {
1757
- const rescued = await firecrawlScrape(
1758
- result.finalUrl,
1759
- mode,
1760
- env,
1761
- fetchFn,
1762
- timeoutMs
1763
- );
1763
+ const rescued = await firecrawlScrape(result.finalUrl, mode, env, fetchFn, timeoutMs);
1764
1764
  if (rescued) {
1765
1765
  extracted = {
1766
1766
  ...rescued,
@@ -2019,7 +2019,7 @@ function createBazilionResourceLoader(appendSystemPrompt) {
2019
2019
  function findMostRecent(sessionDir) {
2020
2020
  if (!existsSync11(sessionDir)) return null;
2021
2021
  let newest = null;
2022
- for (const entry of readdirSync6(sessionDir)) {
2022
+ for (const entry of readdirSync7(sessionDir)) {
2023
2023
  if (!entry.endsWith(".jsonl")) continue;
2024
2024
  const path = join13(sessionDir, entry);
2025
2025
  try {