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.
- package/README.md +277 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +320 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +61 -0
- package/dist/cli.js.map +1 -0
- package/dist/db/client.d.ts +10 -0
- package/dist/db/client.js +16 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/migrate.d.ts +1 -0
- package/dist/db/migrate.js +57 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/migrations/0001_init.sql +89 -0
- package/dist/db/migrations/0002_tasks_due_at.sql +16 -0
- package/dist/db/migrations/0003_task_priority_and_sequence.sql +45 -0
- package/dist/db/migrations/0004_pinned.sql +8 -0
- package/dist/db/migrations/0005_nullable_title_and_parent.sql +5 -0
- package/dist/db/migrations/0006_project_color_icon.sql +3 -0
- package/dist/db/migrations/0007_tokens.sql +14 -0
- package/dist/db/migrations/0008_simplify_kinds_drop_actors.sql +21 -0
- package/dist/db/migrations/0009_sessions.sql +14 -0
- package/dist/db/migrations/0010_completed_at.sql +17 -0
- package/dist/db/migrations/0011_completed_at_backfill_fix.sql +14 -0
- package/dist/db/migrations/0012_completed_at_backfill_due.sql +16 -0
- package/dist/db/migrations/0013_due_at_noon_utc.sql +13 -0
- package/dist/db/schema.d.ts +811 -0
- package/dist/db/schema.js +112 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/mcp/http-client.d.ts +154 -0
- package/dist/mcp/http-client.js +113 -0
- package/dist/mcp/http-client.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +492 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/rest/routes.d.ts +3 -0
- package/dist/rest/routes.js +320 -0
- package/dist/rest/routes.js.map +1 -0
- package/dist/rest/server.d.ts +12 -0
- package/dist/rest/server.js +282 -0
- package/dist/rest/server.js.map +1 -0
- package/dist/service/embedding.d.ts +25 -0
- package/dist/service/embedding.js +112 -0
- package/dist/service/embedding.js.map +1 -0
- package/dist/service/entities.d.ts +22 -0
- package/dist/service/entities.js +174 -0
- package/dist/service/entities.js.map +1 -0
- package/dist/service/index.d.ts +8 -0
- package/dist/service/index.js +8 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/opening_brief.d.ts +14 -0
- package/dist/service/opening_brief.js +104 -0
- package/dist/service/opening_brief.js.map +1 -0
- package/dist/service/projects.d.ts +8 -0
- package/dist/service/projects.js +36 -0
- package/dist/service/projects.js.map +1 -0
- package/dist/service/search.d.ts +15 -0
- package/dist/service/search.js +101 -0
- package/dist/service/search.js.map +1 -0
- package/dist/service/sessions.d.ts +25 -0
- package/dist/service/sessions.js +89 -0
- package/dist/service/sessions.js.map +1 -0
- package/dist/service/tasks.d.ts +23 -0
- package/dist/service/tasks.js +130 -0
- package/dist/service/tasks.js.map +1 -0
- package/dist/service/tokens.d.ts +18 -0
- package/dist/service/tokens.js +41 -0
- package/dist/service/tokens.js.map +1 -0
- package/dist/service/types.d.ts +294 -0
- package/dist/service/types.js +132 -0
- package/dist/service/types.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/package.json +73 -0
- package/web/dist/assets/index-Bddg7h_x.css +1 -0
- package/web/dist/assets/index-DYW6stVx.js +542 -0
- package/web/dist/favicon.svg +13 -0
- 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
|
package/dist/cli.js.map
ADDED
|
@@ -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,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;
|