botnote 0.1.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.
Files changed (79) hide show
  1. package/README.md +277 -0
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +320 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +61 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/db/client.d.ts +10 -0
  9. package/dist/db/client.js +16 -0
  10. package/dist/db/client.js.map +1 -0
  11. package/dist/db/migrate.d.ts +1 -0
  12. package/dist/db/migrate.js +57 -0
  13. package/dist/db/migrate.js.map +1 -0
  14. package/dist/db/migrations/0001_init.sql +89 -0
  15. package/dist/db/migrations/0002_tasks_due_at.sql +16 -0
  16. package/dist/db/migrations/0003_task_priority_and_sequence.sql +45 -0
  17. package/dist/db/migrations/0004_pinned.sql +8 -0
  18. package/dist/db/migrations/0005_nullable_title_and_parent.sql +5 -0
  19. package/dist/db/migrations/0006_project_color_icon.sql +3 -0
  20. package/dist/db/migrations/0007_tokens.sql +14 -0
  21. package/dist/db/migrations/0008_simplify_kinds_drop_actors.sql +21 -0
  22. package/dist/db/migrations/0009_sessions.sql +14 -0
  23. package/dist/db/migrations/0010_completed_at.sql +17 -0
  24. package/dist/db/migrations/0011_completed_at_backfill_fix.sql +14 -0
  25. package/dist/db/migrations/0012_completed_at_backfill_due.sql +16 -0
  26. package/dist/db/migrations/0013_due_at_noon_utc.sql +13 -0
  27. package/dist/db/schema.d.ts +811 -0
  28. package/dist/db/schema.js +112 -0
  29. package/dist/db/schema.js.map +1 -0
  30. package/dist/mcp/http-client.d.ts +154 -0
  31. package/dist/mcp/http-client.js +113 -0
  32. package/dist/mcp/http-client.js.map +1 -0
  33. package/dist/mcp/server.d.ts +7 -0
  34. package/dist/mcp/server.js +492 -0
  35. package/dist/mcp/server.js.map +1 -0
  36. package/dist/rest/routes.d.ts +3 -0
  37. package/dist/rest/routes.js +320 -0
  38. package/dist/rest/routes.js.map +1 -0
  39. package/dist/rest/server.d.ts +12 -0
  40. package/dist/rest/server.js +282 -0
  41. package/dist/rest/server.js.map +1 -0
  42. package/dist/service/embedding.d.ts +25 -0
  43. package/dist/service/embedding.js +112 -0
  44. package/dist/service/embedding.js.map +1 -0
  45. package/dist/service/entities.d.ts +22 -0
  46. package/dist/service/entities.js +174 -0
  47. package/dist/service/entities.js.map +1 -0
  48. package/dist/service/index.d.ts +8 -0
  49. package/dist/service/index.js +8 -0
  50. package/dist/service/index.js.map +1 -0
  51. package/dist/service/opening_brief.d.ts +14 -0
  52. package/dist/service/opening_brief.js +104 -0
  53. package/dist/service/opening_brief.js.map +1 -0
  54. package/dist/service/projects.d.ts +8 -0
  55. package/dist/service/projects.js +36 -0
  56. package/dist/service/projects.js.map +1 -0
  57. package/dist/service/search.d.ts +15 -0
  58. package/dist/service/search.js +101 -0
  59. package/dist/service/search.js.map +1 -0
  60. package/dist/service/sessions.d.ts +25 -0
  61. package/dist/service/sessions.js +89 -0
  62. package/dist/service/sessions.js.map +1 -0
  63. package/dist/service/tasks.d.ts +23 -0
  64. package/dist/service/tasks.js +130 -0
  65. package/dist/service/tasks.js.map +1 -0
  66. package/dist/service/tokens.d.ts +18 -0
  67. package/dist/service/tokens.js +41 -0
  68. package/dist/service/tokens.js.map +1 -0
  69. package/dist/service/types.d.ts +294 -0
  70. package/dist/service/types.js +132 -0
  71. package/dist/service/types.js.map +1 -0
  72. package/dist/version.d.ts +1 -0
  73. package/dist/version.js +13 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +73 -0
  76. package/web/dist/assets/index-Bddg7h_x.css +1 -0
  77. package/web/dist/assets/index-DYW6stVx.js +542 -0
  78. package/web/dist/favicon.svg +13 -0
  79. package/web/dist/index.html +17 -0
package/dist/cli.js ADDED
@@ -0,0 +1,61 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createDb } from "./db/client.js";
3
+ import { migrate } from "./db/migrate.js";
4
+ import { BotnoteHttpClient } from "./mcp/http-client.js";
5
+ import { buildMcpServer } from "./mcp/server.js";
6
+ import { EmbeddingService } from "./service/embedding.js";
7
+ import { buildServer } from "./rest/server.js";
8
+ import { VERSION } from "./version.js";
9
+ // Public botnote.net is the default so fresh remote installs (the common
10
+ // plugin-install case) work without extra config. The daemon host overrides
11
+ // via BOTNOTE_URL in its Codex / Claude Code config to skip the tunnel.
12
+ const DEFAULT_MCP_BASE_URL = "https://botnote.net";
13
+ async function runRest() {
14
+ const port = Number(process.env.BOTNOTE_PORT ?? 4280);
15
+ const host = process.env.BOTNOTE_HOST ?? "127.0.0.1";
16
+ const logLevel = process.env.BOTNOTE_LOG_LEVEL ?? "info";
17
+ await migrate();
18
+ const { db } = createDb();
19
+ const embedding = new EmbeddingService(db, { apiKey: process.env.OPENAI_API_KEY });
20
+ const app = await buildServer({ db, embedding, logLevel });
21
+ await app.listen({ port, host });
22
+ app.log.info(`botnote v${VERSION} listening on http://${host}:${port} (docs: /docs, embeddings ${embedding.isEnabled() ? "ON" : "OFF"})`);
23
+ const shutdown = async (sig) => {
24
+ app.log.info(`shutting down on ${sig}`);
25
+ await app.close();
26
+ process.exit(0);
27
+ };
28
+ process.on("SIGINT", () => shutdown("SIGINT"));
29
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
30
+ }
31
+ async function runMcp() {
32
+ // MCP stdio mode. The server is now an HTTP client of the botnote daemon
33
+ // (no DB connection here), so this entry point is package-portable: it can
34
+ // be installed via a Claude Code plugin on any machine and pointed at any
35
+ // daemon (tailnet IP, https://botnote.net, ...).
36
+ // Every log line must go to stderr so we don't corrupt the MCP framed JSON
37
+ // on stdout.
38
+ const baseUrl = process.env.BOTNOTE_URL ?? DEFAULT_MCP_BASE_URL;
39
+ const client = new BotnoteHttpClient({
40
+ baseUrl,
41
+ token: process.env.BOTNOTE_TOKEN || undefined,
42
+ version: VERSION
43
+ });
44
+ const server = buildMcpServer({ client, version: VERSION });
45
+ const transport = new StdioServerTransport();
46
+ await server.connect(transport);
47
+ process.stderr.write(`botnote MCP v${VERSION} ready (stdio) → ${baseUrl}\n`);
48
+ }
49
+ async function main() {
50
+ const mode = process.argv[2] ?? "rest";
51
+ if (mode === "mcp") {
52
+ await runMcp();
53
+ return;
54
+ }
55
+ await runRest();
56
+ }
57
+ main().catch((err) => {
58
+ console.error(err);
59
+ process.exit(1);
60
+ });
61
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import pkg from "pg";
2
+ import * as schema from "./schema.js";
3
+ export type Database = ReturnType<typeof createDb>;
4
+ export declare function createPool(connectionString?: string): pkg.Pool;
5
+ export declare function createDb(pool?: pkg.Pool): {
6
+ db: import("drizzle-orm/node-postgres").NodePgDatabase<typeof schema> & {
7
+ $client: import("pg").Pool;
8
+ };
9
+ pool: import("pg").Pool;
10
+ };
@@ -0,0 +1,16 @@
1
+ import { drizzle } from "drizzle-orm/node-postgres";
2
+ import pkg from "pg";
3
+ import * as schema from "./schema.js";
4
+ const { Pool } = pkg;
5
+ export function createPool(connectionString) {
6
+ const url = connectionString ?? process.env.DATABASE_URL;
7
+ if (!url) {
8
+ throw new Error("DATABASE_URL is required");
9
+ }
10
+ return new Pool({ connectionString: url, max: 10 });
11
+ }
12
+ export function createDb(pool) {
13
+ const p = pool ?? createPool();
14
+ return { db: drizzle(p, { schema }), pool: p };
15
+ }
16
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/db/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACpD,OAAO,GAAG,MAAM,IAAI,CAAC;AACrB,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC;AAIrB,MAAM,UAAU,UAAU,CAAC,gBAAyB;IAClD,MAAM,GAAG,GAAG,gBAAgB,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;IACzD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,IAAI,IAAI,CAAC,EAAE,gBAAgB,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAe;IACtC,MAAM,CAAC,GAAG,IAAI,IAAI,UAAU,EAAE,CAAC;IAC/B,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AACjD,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function migrate(): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createPool } from "./client.js";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const MIGRATIONS_DIR = join(__dirname, "migrations");
7
+ export async function migrate() {
8
+ const pool = createPool();
9
+ try {
10
+ await pool.query(`
11
+ CREATE TABLE IF NOT EXISTS _migrations (
12
+ name text PRIMARY KEY,
13
+ applied_at timestamptz NOT NULL DEFAULT now()
14
+ )
15
+ `);
16
+ const applied = new Set((await pool.query("SELECT name FROM _migrations")).rows.map((r) => r.name));
17
+ const files = readdirSync(MIGRATIONS_DIR)
18
+ .filter((f) => f.endsWith(".sql"))
19
+ .sort();
20
+ for (const file of files) {
21
+ if (applied.has(file)) {
22
+ console.log(`skip ${file} (already applied)`);
23
+ continue;
24
+ }
25
+ const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf8");
26
+ console.log(`apply ${file}`);
27
+ const client = await pool.connect();
28
+ try {
29
+ await client.query("BEGIN");
30
+ await client.query(sql);
31
+ await client.query("INSERT INTO _migrations (name) VALUES ($1)", [file]);
32
+ await client.query("COMMIT");
33
+ }
34
+ catch (err) {
35
+ await client.query("ROLLBACK");
36
+ throw err;
37
+ }
38
+ finally {
39
+ client.release();
40
+ }
41
+ }
42
+ console.log("migrations done");
43
+ }
44
+ finally {
45
+ await pool.end();
46
+ }
47
+ }
48
+ const isDirectInvocation = import.meta.url === `file://${process.argv[1]}` ||
49
+ process.argv[1]?.endsWith("/migrate.ts") ||
50
+ process.argv[1]?.endsWith("/migrate.js");
51
+ if (isDirectInvocation) {
52
+ migrate().catch((err) => {
53
+ console.error(err);
54
+ process.exit(1);
55
+ });
56
+ }
57
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../src/db/migrate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ -- botnote 0001 init schema
2
+ -- Idempotent: safe to re-run.
3
+
4
+ CREATE EXTENSION IF NOT EXISTS vector;
5
+
6
+ CREATE TABLE IF NOT EXISTS projects (
7
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
8
+ key text NOT NULL,
9
+ name text NOT NULL,
10
+ agents_md text NOT NULL DEFAULT '',
11
+ created_at timestamptz NOT NULL DEFAULT now(),
12
+ updated_at timestamptz NOT NULL DEFAULT now()
13
+ );
14
+ CREATE UNIQUE INDEX IF NOT EXISTS projects_key_idx ON projects(key);
15
+
16
+ CREATE TABLE IF NOT EXISTS actors (
17
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
18
+ name text NOT NULL,
19
+ kind text NOT NULL,
20
+ key text,
21
+ created_at timestamptz NOT NULL DEFAULT now()
22
+ );
23
+ CREATE UNIQUE INDEX IF NOT EXISTS actors_name_idx ON actors(name);
24
+ CREATE UNIQUE INDEX IF NOT EXISTS actors_key_idx ON actors(key);
25
+
26
+ CREATE TABLE IF NOT EXISTS entities (
27
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
28
+ project_id uuid REFERENCES projects(id) ON DELETE SET NULL,
29
+ kind text NOT NULL,
30
+ title text NOT NULL,
31
+ body text NOT NULL DEFAULT '',
32
+ tags text[] NOT NULL DEFAULT '{}'::text[],
33
+ status text NOT NULL DEFAULT 'open',
34
+ actor_id uuid REFERENCES actors(id),
35
+ actor_kind text NOT NULL DEFAULT 'human',
36
+ idempotency_key text,
37
+ parent_id uuid REFERENCES entities(id) ON DELETE SET NULL,
38
+ body_tsv tsvector GENERATED ALWAYS AS (
39
+ setweight(to_tsvector('simple', coalesce(title, '')), 'A') ||
40
+ setweight(to_tsvector('simple', coalesce(body, '')), 'B')
41
+ ) STORED,
42
+ body_vec vector(384),
43
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
44
+ created_at timestamptz NOT NULL DEFAULT now(),
45
+ updated_at timestamptz NOT NULL DEFAULT now()
46
+ );
47
+
48
+ CREATE INDEX IF NOT EXISTS entities_project_created_idx
49
+ ON entities(project_id, created_at DESC);
50
+ CREATE INDEX IF NOT EXISTS entities_project_kind_created_idx
51
+ ON entities(project_id, kind, created_at DESC);
52
+ CREATE INDEX IF NOT EXISTS entities_actor_created_idx
53
+ ON entities(actor_id, created_at DESC);
54
+ CREATE UNIQUE INDEX IF NOT EXISTS entities_idempotency_idx
55
+ ON entities(idempotency_key) WHERE idempotency_key IS NOT NULL;
56
+ CREATE INDEX IF NOT EXISTS entities_body_tsv_idx
57
+ ON entities USING GIN(body_tsv);
58
+ CREATE INDEX IF NOT EXISTS entities_body_vec_idx
59
+ ON entities USING hnsw (body_vec vector_cosine_ops);
60
+ CREATE INDEX IF NOT EXISTS entities_tags_idx
61
+ ON entities USING GIN(tags);
62
+
63
+ CREATE TABLE IF NOT EXISTS edges (
64
+ from_id uuid NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
65
+ to_id uuid NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
66
+ kind text NOT NULL,
67
+ created_at timestamptz NOT NULL DEFAULT now(),
68
+ PRIMARY KEY (from_id, to_id, kind)
69
+ );
70
+ CREATE INDEX IF NOT EXISTS edges_to_idx ON edges(to_id, kind);
71
+ CREATE INDEX IF NOT EXISTS edges_from_idx ON edges(from_id, kind);
72
+
73
+ -- updated_at trigger
74
+ CREATE OR REPLACE FUNCTION botnote_touch_updated_at() RETURNS trigger AS $$
75
+ BEGIN
76
+ NEW.updated_at = now();
77
+ RETURN NEW;
78
+ END;
79
+ $$ LANGUAGE plpgsql;
80
+
81
+ DROP TRIGGER IF EXISTS entities_touch_updated_at ON entities;
82
+ CREATE TRIGGER entities_touch_updated_at
83
+ BEFORE UPDATE ON entities
84
+ FOR EACH ROW EXECUTE FUNCTION botnote_touch_updated_at();
85
+
86
+ DROP TRIGGER IF EXISTS projects_touch_updated_at ON projects;
87
+ CREATE TRIGGER projects_touch_updated_at
88
+ BEFORE UPDATE ON projects
89
+ FOR EACH ROW EXECUTE FUNCTION botnote_touch_updated_at();
@@ -0,0 +1,16 @@
1
+ -- botnote 0002 — tasks need due dates for the Calendar view.
2
+ -- Idempotent: safe to re-run.
3
+
4
+ ALTER TABLE entities ADD COLUMN IF NOT EXISTS due_at timestamptz;
5
+
6
+ CREATE INDEX IF NOT EXISTS entities_due_at_idx
7
+ ON entities(due_at) WHERE due_at IS NOT NULL;
8
+
9
+ CREATE INDEX IF NOT EXISTS entities_kind_due_at_idx
10
+ ON entities(kind, due_at) WHERE due_at IS NOT NULL;
11
+
12
+ CREATE INDEX IF NOT EXISTS entities_task_due_idx
13
+ ON entities(due_at) WHERE kind = 'task' AND due_at IS NOT NULL;
14
+
15
+ CREATE INDEX IF NOT EXISTS entities_task_no_due_idx
16
+ ON entities(created_at DESC) WHERE kind = 'task' AND due_at IS NULL AND status NOT IN ('done', 'archived');
@@ -0,0 +1,45 @@
1
+ -- botnote 0003 — priority + per-project sequence id (BOT-12 style).
2
+ -- Idempotent.
3
+
4
+ ALTER TABLE entities ADD COLUMN IF NOT EXISTS priority text NOT NULL DEFAULT 'none';
5
+ ALTER TABLE entities ADD COLUMN IF NOT EXISTS sequence_id integer;
6
+
7
+ CREATE INDEX IF NOT EXISTS entities_project_seq_idx
8
+ ON entities(project_id, sequence_id) WHERE sequence_id IS NOT NULL;
9
+
10
+ -- per-project sequence trigger. Assign sequence_id on INSERT when project_id
11
+ -- is set (workspace-wide entities stay null).
12
+ CREATE OR REPLACE FUNCTION botnote_set_sequence_id() RETURNS trigger AS $$
13
+ BEGIN
14
+ IF NEW.sequence_id IS NULL AND NEW.project_id IS NOT NULL THEN
15
+ SELECT COALESCE(MAX(sequence_id), 0) + 1
16
+ INTO NEW.sequence_id
17
+ FROM entities
18
+ WHERE project_id = NEW.project_id;
19
+ END IF;
20
+ RETURN NEW;
21
+ END;
22
+ $$ LANGUAGE plpgsql;
23
+
24
+ DROP TRIGGER IF EXISTS entities_set_sequence_id ON entities;
25
+ CREATE TRIGGER entities_set_sequence_id
26
+ BEFORE INSERT ON entities
27
+ FOR EACH ROW EXECUTE FUNCTION botnote_set_sequence_id();
28
+
29
+ -- Backfill existing rows that have project_id but no sequence_id, ordered by created_at.
30
+ DO $$
31
+ DECLARE
32
+ rec record;
33
+ counters jsonb := '{}'::jsonb;
34
+ next_n integer;
35
+ BEGIN
36
+ FOR rec IN
37
+ SELECT id, project_id FROM entities
38
+ WHERE project_id IS NOT NULL AND sequence_id IS NULL
39
+ ORDER BY project_id, created_at
40
+ LOOP
41
+ next_n := COALESCE((counters->>rec.project_id::text)::int, 0) + 1;
42
+ counters := jsonb_set(counters, ARRAY[rec.project_id::text], to_jsonb(next_n));
43
+ UPDATE entities SET sequence_id = next_n WHERE id = rec.id;
44
+ END LOOP;
45
+ END $$;
@@ -0,0 +1,8 @@
1
+ -- botnote 0004 — pinned flag for project-level context surfacing.
2
+ -- Idempotent.
3
+
4
+ ALTER TABLE entities ADD COLUMN IF NOT EXISTS pinned boolean NOT NULL DEFAULT false;
5
+
6
+ CREATE INDEX IF NOT EXISTS entities_pinned_idx
7
+ ON entities(project_id, pinned)
8
+ WHERE pinned = true;
@@ -0,0 +1,5 @@
1
+ -- Allow title to be null (notes without a title use body-derived placeholder in UI).
2
+ ALTER TABLE entities ALTER COLUMN title DROP NOT NULL;
3
+
4
+ -- Index parent_id for fast lookups of related entities (e.g. notes linked to a task).
5
+ CREATE INDEX IF NOT EXISTS entities_parent_idx ON entities(parent_id) WHERE parent_id IS NOT NULL;
@@ -0,0 +1,3 @@
1
+ -- Per-project color + icon. Defaults match the Linear-ish indigo accent and a simple circle dot.
2
+ ALTER TABLE projects ADD COLUMN IF NOT EXISTS color text NOT NULL DEFAULT '#5e6ad2';
3
+ ALTER TABLE projects ADD COLUMN IF NOT EXISTS icon text NOT NULL DEFAULT 'circle';
@@ -0,0 +1,14 @@
1
+ -- API tokens. token_hash stores sha256 of the plaintext token; prefix is the first
2
+ -- 8 chars of the plaintext (for display). Auth middleware enforcement is off by
3
+ -- default (botnote currently expects tailnet-only access); these endpoints exist
4
+ -- so the UI can grant/revoke tokens and Boss can enable enforcement later.
5
+ CREATE TABLE IF NOT EXISTS tokens (
6
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
7
+ name text NOT NULL,
8
+ token_hash text NOT NULL UNIQUE,
9
+ prefix text NOT NULL,
10
+ last_used_at timestamptz,
11
+ created_at timestamptz NOT NULL DEFAULT now()
12
+ );
13
+
14
+ CREATE INDEX IF NOT EXISTS tokens_created_idx ON tokens(created_at DESC);
@@ -0,0 +1,21 @@
1
+ -- 0008: simplify entity kinds and drop the actors table.
2
+ --
3
+ -- Why: the original schema had 7 entity kinds (task, note, decision, doc,
4
+ -- comment, log, memory) and a separate `actors` table for writer identity.
5
+ -- In practice only task and note carry real semantic weight in the UI/agent
6
+ -- flows; the others are folded into note with the legacy kind preserved as a
7
+ -- tag so nothing is lost. Identity is now expressed by the existing
8
+ -- entities.actor_kind text column (human / agent / system) — the actors
9
+ -- lookup table never accumulated rows, so we drop it.
10
+
11
+ -- Fold legacy kinds into 'note'. The previous kind name is appended to tags
12
+ -- so we can still query e.g. tag='decision' or tag='memory' if needed.
13
+ UPDATE entities
14
+ SET tags = array_append(tags, kind), kind = 'note'
15
+ WHERE kind IN ('decision', 'doc', 'comment', 'log', 'memory');
16
+
17
+ -- Drop the FK first to avoid a dependency error, then the column, then the
18
+ -- table. None of these had any live rows referencing them.
19
+ ALTER TABLE entities DROP CONSTRAINT IF EXISTS entities_actor_id_fkey;
20
+ ALTER TABLE entities DROP COLUMN IF EXISTS actor_id;
21
+ DROP TABLE IF EXISTS actors;
@@ -0,0 +1,14 @@
1
+ -- Browser session cookies. Each row corresponds to one logged-in browser;
2
+ -- plaintext is the cookie value (random 32 bytes hex), DB stores sha256.
3
+
4
+ CREATE TABLE IF NOT EXISTS sessions (
5
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
6
+ token_hash text NOT NULL,
7
+ expires_at timestamptz NOT NULL,
8
+ user_agent text,
9
+ last_used_at timestamptz,
10
+ created_at timestamptz NOT NULL DEFAULT now()
11
+ );
12
+
13
+ CREATE UNIQUE INDEX IF NOT EXISTS sessions_token_hash_idx ON sessions(token_hash);
14
+ CREATE INDEX IF NOT EXISTS sessions_expires_at_idx ON sessions(expires_at);
@@ -0,0 +1,17 @@
1
+ -- Add completed_at column to entities. Set automatically on a status
2
+ -- transition into 'done' (and cleared when leaving). Backfill existing done
3
+ -- rows from updated_at as the best available approximation — Plane-migrated
4
+ -- tasks lost their original timestamp, so this clusters them on the migration
5
+ -- day rather than guessing. New transitions are exact.
6
+ ALTER TABLE entities ADD COLUMN IF NOT EXISTS completed_at timestamptz;
7
+
8
+ UPDATE entities
9
+ SET completed_at = updated_at
10
+ WHERE kind = 'task' AND status = 'done' AND completed_at IS NULL;
11
+
12
+ -- Partial index — only done rows have a non-null completed_at, so this stays
13
+ -- small and keeps range scans cheap when the calendar queries by completed
14
+ -- date.
15
+ CREATE INDEX IF NOT EXISTS entities_completed_at_idx
16
+ ON entities (completed_at)
17
+ WHERE completed_at IS NOT NULL;
@@ -0,0 +1,14 @@
1
+ -- Fix the 0010 backfill. The first pass used updated_at, which for tasks
2
+ -- migrated from Plane is the migration day — so every legacy done task piled
3
+ -- onto the same calendar cell. Re-backfill with COALESCE(due_at, updated_at)
4
+ -- so legacy done items render on their original due date (the pre-change
5
+ -- behavior), while freshly-completed tasks still get an exact stamp from the
6
+ -- service layer at status-transition time.
7
+ UPDATE entities
8
+ SET completed_at = COALESCE(due_at, updated_at)
9
+ WHERE kind = 'task'
10
+ AND status = 'done'
11
+ -- Only rows whose completed_at matches the 0010 backfill exactly (i.e.
12
+ -- never updated since). Real recent completions have completed_at != updated_at
13
+ -- once anything else touches the row, so this avoids stomping on real data.
14
+ AND completed_at = updated_at;
@@ -0,0 +1,16 @@
1
+ -- 0011's WHERE clause assumed completed_at = updated_at after 0010, but the
2
+ -- BEFORE-UPDATE trigger bumps updated_at on every UPDATE, so 0010 set
3
+ -- completed_at to OLD.updated_at and the trigger then advanced updated_at past
4
+ -- it. As a result 0011 matched nothing.
5
+ --
6
+ -- This pass forces COALESCE(due_at, updated_at) onto every done task whose
7
+ -- completed_at still falls before the 0010 backfill window — i.e. data the
8
+ -- service layer has not freshly written. Anything completed by the running
9
+ -- daemon after 0010 ran will have a strictly later completed_at and is left
10
+ -- alone.
11
+ UPDATE entities
12
+ SET completed_at = COALESCE(due_at, updated_at)
13
+ WHERE kind = 'task'
14
+ AND status = 'done'
15
+ AND completed_at IS NOT NULL
16
+ AND completed_at < '2026-06-03 23:56:00+00';
@@ -0,0 +1,13 @@
1
+ -- Date-only due dates were imported from Plane as `YYYY-MM-DD T00:00:00Z`,
2
+ -- which lands the previous calendar day in any UTC-negative timezone
3
+ -- (e.g. UTC midnight = 5pm Pacific the day before). Bump every midnight-UTC
4
+ -- due_at to noon-UTC so it stays on the intended calendar day in every
5
+ -- timezone Boss actually uses (UTC-12 .. UTC+11). The service layer applies
6
+ -- the same normalization to new writes so this stays clean going forward.
7
+ UPDATE entities
8
+ SET due_at = date_trunc('day', due_at AT TIME ZONE 'UTC') + INTERVAL '12 hours'
9
+ WHERE kind = 'task'
10
+ AND due_at IS NOT NULL
11
+ AND EXTRACT(HOUR FROM due_at AT TIME ZONE 'UTC') = 0
12
+ AND EXTRACT(MINUTE FROM due_at AT TIME ZONE 'UTC') = 0
13
+ AND EXTRACT(SECOND FROM due_at AT TIME ZONE 'UTC') = 0;