people-truth 1.0.0 → 1.1.3

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/.env.example ADDED
@@ -0,0 +1,7 @@
1
+ PEOPLE_TRUTH_PROVIDER="sqlite"
2
+ PEOPLE_TRUTH_SQLITE_PATH="$HOME/.people-truth/people-truth.sqlite"
3
+
4
+ # For Supabase:
5
+ # PEOPLE_TRUTH_PROVIDER="supabase"
6
+ # SUPABASE_URL="https://your-project-ref.supabase.co"
7
+ # SUPABASE_KEY="your-service-role-or-secret-key"
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # people-truth
2
2
 
3
- `people-truth` is a Supabase/Postgres-backed source of truth for stable person identities, names, mentions, affiliations, identifiers, and sourced facts.
3
+ `people-truth` is a portable source of truth for stable person identities, names, mentions, affiliations, identifiers, and sourced facts.
4
+
5
+ It can run in two modes:
6
+
7
+ - SQLite: local-first, no cloud dependency, good for personal agents and laptops.
8
+ - Supabase/Postgres: hosted and shareable, good for multi-device or team use.
4
9
 
5
10
  Supabase is treated as a replaceable Postgres runtime. The durable contract is the SQL migration plus the CLI behavior.
6
11
 
@@ -15,43 +20,79 @@ For local development:
15
20
 
16
21
  ```bash
17
22
  npm install
23
+ npm test
18
24
  npm run build
19
- npm run dev -- --help
20
25
  ```
21
26
 
22
- ## Configure
27
+ ## Quick Start: SQLite
23
28
 
24
- Create a local `.env` file:
29
+ SQLite is the default provider. This creates `.env`, initializes the local database, and verifies the schema:
25
30
 
26
31
  ```bash
27
- ptruth init --url "https://your-project-ref.supabase.co" --key "your-service-role-or-secret-key"
28
- ptruth status
32
+ ptruth setup --provider sqlite
33
+ ptruth db status
29
34
  ```
30
35
 
31
- The CLI reads `SUPABASE_URL` and `SUPABASE_KEY` from `.env`, or from `~/.codex/.env` when present.
36
+ By default, the database lives at:
32
37
 
33
- ## Database
38
+ ```text
39
+ ~/.people-truth/people-truth.sqlite
40
+ ```
41
+
42
+ Use a custom path when needed:
43
+
44
+ ```bash
45
+ ptruth setup --provider sqlite --sqlite-path ./people.sqlite --force
46
+ ```
47
+
48
+ ## Quick Start: Supabase
49
+
50
+ Create a Supabase project, then configure credentials:
51
+
52
+ ```bash
53
+ ptruth setup \
54
+ --provider supabase \
55
+ --url "https://your-project-ref.supabase.co" \
56
+ --key "your-service-role-or-secret-key"
57
+ ```
34
58
 
35
- Apply migrations to a linked Supabase project:
59
+ Check whether the required tables exist:
60
+
61
+ ```bash
62
+ ptruth db status
63
+ ```
64
+
65
+ If tables are missing, print the SQL and run it in the Supabase SQL editor:
66
+
67
+ ```bash
68
+ ptruth db schema --provider supabase
69
+ ```
70
+
71
+ From this repository, you can also link a Supabase project and push migrations:
36
72
 
37
73
  ```bash
38
74
  npx supabase link --project-ref your-project-ref
39
75
  npm run db:push
40
76
  ```
41
77
 
42
- The main files are:
78
+ ## Configuration
43
79
 
44
- - `supabase/migrations/20260611000000_initial_people_truth.sql`
45
- - `supabase/schema.sql`
80
+ The CLI reads `.env` first, then `~/.codex/.env` when present.
46
81
 
47
- The schema centers on:
82
+ SQLite:
48
83
 
49
- - `people`: stable UUID identity.
50
- - `person_names`: canonical names, aliases, pen names, former names, foreign names.
51
- - `person_identifiers`: external IDs and URLs.
52
- - `person_affiliations`: organization and role history.
53
- - `person_facts`: sourced predicate/value facts.
54
- - `sources`: provenance.
84
+ ```env
85
+ PEOPLE_TRUTH_PROVIDER="sqlite"
86
+ PEOPLE_TRUTH_SQLITE_PATH="$HOME/.people-truth/people-truth.sqlite"
87
+ ```
88
+
89
+ Supabase:
90
+
91
+ ```env
92
+ PEOPLE_TRUTH_PROVIDER="supabase"
93
+ SUPABASE_URL="https://your-project-ref.supabase.co"
94
+ SUPABASE_KEY="your-service-role-or-secret-key"
95
+ ```
55
96
 
56
97
  ## CLI
57
98
 
@@ -70,6 +111,28 @@ All commands support JSON output:
70
111
  ptruth people show "梁启超" -o json
71
112
  ```
72
113
 
114
+ ## Database
115
+
116
+ The schema centers on:
117
+
118
+ - `people`: stable UUID identity.
119
+ - `person_names`: canonical names, aliases, pen names, former names, foreign names.
120
+ - `person_identifiers`: external IDs and URLs.
121
+ - `person_affiliations`: organization and role history.
122
+ - `person_facts`: sourced predicate/value facts.
123
+ - `sources`: provenance.
124
+
125
+ Package SQL assets:
126
+
127
+ - `supabase/migrations/20260611000000_initial_people_truth.sql`
128
+ - `supabase/schema.sql`
129
+
130
+ For SQLite, the equivalent schema is embedded in the CLI and can be printed:
131
+
132
+ ```bash
133
+ ptruth db schema --provider sqlite
134
+ ```
135
+
73
136
  ## Mentions
74
137
 
75
138
  Use Markdown-compatible person mentions:
@@ -90,4 +153,4 @@ fix: handle duplicate person names
90
153
  feat!: change mention format
91
154
  ```
92
155
 
93
- CI runs typecheck/build/pack. Release publishes to npm when `NPM_TOKEN` is configured in GitHub repository secrets.
156
+ CI runs typecheck/tests/build/pack. Release publishes to npm when `NPM_TOKEN` is configured in GitHub repository secrets.
package/dist/cli.js CHANGED
@@ -1,33 +1,80 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { existsSync, writeFileSync } from "node:fs";
4
- import { resolve } from "node:path";
5
3
  import { addFact, listFacts } from "./facts.js";
6
- import { getConfig } from "./config.js";
4
+ import { defaultSqlitePath, readConfig } from "./config.js";
7
5
  import { addPerson, formatPersonMention, getPerson, searchPeople } from "./people.js";
8
6
  import { printResult } from "./output.js";
7
+ import { checkDatabase, schemaFor, setupDatabase, writeEnvFile } from "./setup.js";
9
8
  const program = new Command();
10
9
  program
11
10
  .name("people-truth")
12
- .description("Supabase-backed personal source of truth for people.")
11
+ .description("Portable source of truth for people, backed by SQLite or Supabase.")
13
12
  .option("-o, --output <mode>", "Output mode: table or json", "table");
14
13
  program
15
- .command("init")
16
- .description("Create a local .env file for Supabase credentials")
17
- .requiredOption("--url <url>", "Supabase project URL")
18
- .requiredOption("--key <key>", "Supabase service-role or secret key")
14
+ .command("setup")
15
+ .description("Configure people-truth and initialize the selected database")
16
+ .option("--provider <provider>", "sqlite or supabase", "sqlite")
17
+ .option("--url <url>", "Supabase project URL")
18
+ .option("--key <key>", "Supabase service-role or secret key")
19
+ .option("--sqlite-path <path>", "SQLite database path", defaultSqlitePath)
19
20
  .option("--path <path>", "Env file path", ".env")
20
21
  .option("--force", "Overwrite an existing env file", false)
22
+ .option("--skip-db", "Write config without initializing/checking database", false)
21
23
  .action(async (options) => {
22
24
  await run(async () => {
23
- const envPath = resolve(options.path);
24
- if (existsSync(envPath) && !options.force) {
25
- throw new Error(`${envPath} already exists. Re-run with --force to overwrite it.`);
25
+ const provider = parseProvider(options.provider);
26
+ const envPath = writeEnvFile({
27
+ provider,
28
+ path: options.path,
29
+ force: options.force,
30
+ supabaseUrl: options.url,
31
+ supabaseKey: options.key,
32
+ sqlitePath: options.sqlitePath
33
+ });
34
+ const result = {
35
+ ok: true,
36
+ provider,
37
+ envPath
38
+ };
39
+ if (provider === "sqlite")
40
+ result.sqlitePath = options.sqlitePath;
41
+ if (!options.skipDb) {
42
+ if (provider === "sqlite") {
43
+ result.database = await setupDatabase({ provider, sqlitePath: options.sqlitePath });
44
+ }
45
+ else {
46
+ result.database = await checkDatabase({
47
+ provider,
48
+ supabaseUrl: options.url,
49
+ supabaseKey: options.key
50
+ });
51
+ result.nextStep = "If tables are missing, run `ptruth db schema --provider supabase` and apply the SQL in Supabase.";
52
+ }
26
53
  }
27
- writeFileSync(envPath, `SUPABASE_URL="${options.url}"\nSUPABASE_KEY="${options.key}"\n`, {
28
- mode: 0o600
54
+ printResult(result, outputMode());
55
+ });
56
+ });
57
+ program
58
+ .command("init")
59
+ .description("Create a local .env file")
60
+ .option("--provider <provider>", "sqlite or supabase", "supabase")
61
+ .option("--url <url>", "Supabase project URL")
62
+ .option("--key <key>", "Supabase service-role or secret key")
63
+ .option("--sqlite-path <path>", "SQLite database path", defaultSqlitePath)
64
+ .option("--path <path>", "Env file path", ".env")
65
+ .option("--force", "Overwrite an existing env file", false)
66
+ .action(async (options) => {
67
+ await run(async () => {
68
+ const provider = parseProvider(options.provider);
69
+ const envPath = writeEnvFile({
70
+ provider,
71
+ path: options.path,
72
+ force: options.force,
73
+ supabaseUrl: options.url,
74
+ supabaseKey: options.key,
75
+ sqlitePath: options.sqlitePath
29
76
  });
30
- printResult({ ok: true, path: envPath }, outputMode());
77
+ printResult({ ok: true, provider, path: envPath }, outputMode());
31
78
  });
32
79
  });
33
80
  program
@@ -35,14 +82,41 @@ program
35
82
  .description("Check local CLI configuration")
36
83
  .action(async () => {
37
84
  await run(async () => {
38
- const config = getConfig();
85
+ const config = readConfig();
39
86
  printResult({
40
87
  ok: true,
41
- supabaseUrl: config.SUPABASE_URL,
42
- hasSupabaseKey: Boolean(config.SUPABASE_KEY)
88
+ provider: config.provider,
89
+ supabaseUrl: config.supabaseUrl,
90
+ hasSupabaseKey: Boolean(config.supabaseKey),
91
+ sqlitePath: config.sqlitePath
43
92
  }, outputMode());
44
93
  });
45
94
  });
95
+ const db = program.command("db").description("Manage database schema");
96
+ db.command("init")
97
+ .description("Initialize the configured database schema")
98
+ .action(async () => {
99
+ await run(async () => {
100
+ printResult(await setupDatabase(), outputMode());
101
+ });
102
+ });
103
+ db.command("status")
104
+ .description("Check whether required tables exist")
105
+ .action(async () => {
106
+ await run(async () => {
107
+ printResult(await checkDatabase(), outputMode());
108
+ });
109
+ });
110
+ db.command("schema")
111
+ .description("Print SQL schema for a provider")
112
+ .option("--provider <provider>", "sqlite or supabase")
113
+ .action(async (options) => {
114
+ await run(async () => {
115
+ const config = readConfig();
116
+ const provider = parseProvider(options.provider ?? config.provider);
117
+ printResult(schemaFor(provider), outputMode());
118
+ });
119
+ });
46
120
  const people = program.command("people").description("Manage people");
47
121
  people
48
122
  .command("add")
@@ -138,6 +212,11 @@ function outputMode() {
138
212
  const mode = program.opts().output;
139
213
  return mode === "json" ? "json" : "table";
140
214
  }
215
+ function parseProvider(value) {
216
+ if (value === "sqlite" || value === "supabase")
217
+ return value;
218
+ throw new Error("Provider must be sqlite or supabase.");
219
+ }
141
220
  async function run(fn) {
142
221
  try {
143
222
  await fn();
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare function getConfig(): {
2
- SUPABASE_URL: string;
3
- SUPABASE_KEY: string;
4
- };
1
+ import type { RuntimeConfig } from "./types.js";
2
+ export declare const defaultSqlitePath: string;
3
+ export declare function readConfig(): RuntimeConfig;
4
+ export declare function getConfig(): RuntimeConfig;
package/dist/config.js CHANGED
@@ -8,14 +8,54 @@ const codexEnvPath = join(homedir(), ".codex", ".env");
8
8
  if (existsSync(codexEnvPath)) {
9
9
  loadDotenv({ path: codexEnvPath, override: false });
10
10
  }
11
+ export const defaultSqlitePath = join(homedir(), ".people-truth", "people-truth.sqlite");
11
12
  const envSchema = z.object({
12
- SUPABASE_URL: z.string().url(),
13
- SUPABASE_KEY: z.string().min(1)
13
+ PEOPLE_TRUTH_PROVIDER: z.enum(["supabase", "sqlite"]).optional(),
14
+ PEOPLE_TRUTH_SQLITE_PATH: z.string().min(1).optional(),
15
+ SUPABASE_URL: z.string().url().optional(),
16
+ SUPABASE_KEY: z.string().min(1).optional()
14
17
  });
15
- export function getConfig() {
18
+ export function readConfig() {
16
19
  const parsed = envSchema.safeParse(process.env);
17
20
  if (!parsed.success) {
18
- throw new Error("Missing Supabase config. Set SUPABASE_URL and SUPABASE_KEY in .env or ~/.codex/.env.");
21
+ throw new Error("Invalid people-truth environment configuration.");
22
+ }
23
+ const env = parsed.data;
24
+ const provider = resolveProvider(env.PEOPLE_TRUTH_PROVIDER, env);
25
+ return {
26
+ provider,
27
+ supabaseUrl: env.SUPABASE_URL,
28
+ supabaseKey: env.SUPABASE_KEY,
29
+ sqlitePath: expandPath(env.PEOPLE_TRUTH_SQLITE_PATH ?? defaultSqlitePath)
30
+ };
31
+ }
32
+ export function getConfig() {
33
+ const config = readConfig();
34
+ if (config.provider === "supabase" && (!config.supabaseUrl || !config.supabaseKey)) {
35
+ throw new Error("Missing Supabase config. Run `ptruth setup --provider supabase` or set SUPABASE_URL and SUPABASE_KEY.");
19
36
  }
20
- return parsed.data;
37
+ if (config.provider === "sqlite" && !config.sqlitePath) {
38
+ throw new Error("Missing SQLite config. Run `ptruth setup --provider sqlite` or set PEOPLE_TRUTH_SQLITE_PATH.");
39
+ }
40
+ return config;
41
+ }
42
+ function resolveProvider(provider, env) {
43
+ if (provider)
44
+ return provider;
45
+ if (env.PEOPLE_TRUTH_SQLITE_PATH)
46
+ return "sqlite";
47
+ if (env.SUPABASE_URL || env.SUPABASE_KEY)
48
+ return "supabase";
49
+ return "sqlite";
50
+ }
51
+ function expandPath(value) {
52
+ if (value === "~")
53
+ return homedir();
54
+ if (value.startsWith("~/"))
55
+ return join(homedir(), value.slice(2));
56
+ if (value === "$HOME")
57
+ return homedir();
58
+ if (value.startsWith("$HOME/"))
59
+ return join(homedir(), value.slice(6));
60
+ return value;
21
61
  }
package/dist/facts.d.ts CHANGED
@@ -1,11 +1,3 @@
1
- export declare function addFact(input: {
2
- person: string;
3
- predicate: string;
4
- value: string;
5
- confidence: "confirmed" | "likely" | "unverified";
6
- sourceType?: string;
7
- sourceRef?: string;
8
- sourceTitle?: string;
9
- sourceUrl?: string;
10
- }): Promise<any>;
11
- export declare function listFacts(identifier: string): Promise<any>;
1
+ import type { AddFactInput } from "./types.js";
2
+ export declare function addFact(input: AddFactInput): Promise<unknown>;
3
+ export declare function listFacts(identifier: string): Promise<unknown[]>;
package/dist/facts.js CHANGED
@@ -1,44 +1,7 @@
1
- import { getPerson } from "./people.js";
2
- import { getSupabase } from "./supabase.js";
1
+ import { getStore } from "./store.js";
3
2
  export async function addFact(input) {
4
- const supabase = getSupabase();
5
- const person = await getPerson(input.person);
6
- if (!person)
7
- throw new Error(`Person not found: ${input.person}`);
8
- let sourceId = null;
9
- if (input.sourceType || input.sourceRef || input.sourceTitle || input.sourceUrl) {
10
- const { data: source, error: sourceError } = await supabase
11
- .from("sources")
12
- .insert({
13
- source_type: input.sourceType ?? "manual",
14
- source_ref: input.sourceRef,
15
- title: input.sourceTitle,
16
- url: input.sourceUrl
17
- })
18
- .select()
19
- .single();
20
- if (sourceError)
21
- throw new Error(sourceError.message);
22
- sourceId = source.id;
23
- }
24
- const { data, error } = await supabase
25
- .from("person_facts")
26
- .insert({
27
- person_id: person.id,
28
- predicate: input.predicate,
29
- value: input.value,
30
- confidence: input.confidence,
31
- source_id: sourceId
32
- })
33
- .select("*, sources(source_type, source_ref, title, url)")
34
- .single();
35
- if (error)
36
- throw new Error(error.message);
37
- return data;
3
+ return getStore().addFact(input);
38
4
  }
39
5
  export async function listFacts(identifier) {
40
- const person = await getPerson(identifier);
41
- if (!person)
42
- throw new Error(`Person not found: ${identifier}`);
43
- return person.person_facts ?? [];
6
+ return getStore().listFacts(identifier);
44
7
  }
package/dist/people.d.ts CHANGED
@@ -1,13 +1,5 @@
1
- export declare function addPerson(input: {
2
- name: string;
3
- aliases: string[];
4
- organization?: string;
5
- role?: string;
6
- location?: string;
7
- relationship?: string;
8
- bio?: string;
9
- note?: string;
10
- }): Promise<any>;
1
+ import type { AddPersonInput } from "./types.js";
2
+ export declare function addPerson(input: AddPersonInput): Promise<unknown>;
11
3
  export declare function searchPeople(query: string): Promise<unknown[]>;
12
4
  export declare function getPerson(identifier: string): Promise<any>;
13
5
  export declare function formatPersonMention(identifier: string, label?: string): Promise<string>;
package/dist/people.js CHANGED
@@ -1,101 +1,12 @@
1
- import { getSupabase } from "./supabase.js";
2
- const PERSON_SELECT = "*, person_names(name, name_type, locale, is_primary), person_identifiers(identifier_type, value, url, is_primary), person_affiliations(organization_name, role_title, start_date, end_date, note), person_facts(id, predicate, value, confidence, valid_from, valid_to, created_at, sources(source_type, source_ref, title, url))";
1
+ import { getStore } from "./store.js";
3
2
  export async function addPerson(input) {
4
- const supabase = getSupabase();
5
- const { data: person, error } = await supabase
6
- .from("people")
7
- .insert({
8
- canonical_name: input.name,
9
- display_name: input.name,
10
- bio: input.bio,
11
- note: input.note ?? ([input.location, input.relationship].filter(Boolean).join("; ") || undefined)
12
- })
13
- .select()
14
- .single();
15
- if (error)
16
- throw new Error(error.message);
17
- const names = Array.from(new Set([input.name, ...input.aliases].filter(Boolean)));
18
- if (names.length > 0) {
19
- const { error: nameError } = await supabase.from("person_names").insert(names.map((alias) => ({
20
- person_id: person.id,
21
- name: alias,
22
- name_type: alias === input.name ? "canonical" : "alias",
23
- is_primary: alias === input.name
24
- })));
25
- if (nameError)
26
- throw new Error(nameError.message);
27
- }
28
- if (input.organization) {
29
- const { error: affiliationError } = await supabase.from("person_affiliations").insert({
30
- person_id: person.id,
31
- organization_name: input.organization,
32
- role_title: input.role
33
- });
34
- if (affiliationError)
35
- throw new Error(affiliationError.message);
36
- }
37
- return getPerson(person.id);
3
+ return getStore().addPerson(input);
38
4
  }
39
5
  export async function searchPeople(query) {
40
- const supabase = getSupabase();
41
- const pattern = `%${query}%`;
42
- const select = "id, canonical_name, display_name, bio, note, person_names(name, name_type), person_affiliations(organization_name, role_title)";
43
- const { data: directMatches, error } = await supabase
44
- .from("people")
45
- .select(select)
46
- .or(`canonical_name.ilike.${pattern},display_name.ilike.${pattern},bio.ilike.${pattern},note.ilike.${pattern}`)
47
- .limit(20);
48
- if (error)
49
- throw new Error(error.message);
50
- const { data: nameMatches, error: nameError } = await supabase
51
- .from("person_names")
52
- .select(`people(${select})`)
53
- .ilike("name", pattern)
54
- .limit(20);
55
- if (nameError)
56
- throw new Error(nameError.message);
57
- const byId = new Map();
58
- for (const person of directMatches ?? []) {
59
- byId.set(person.id, person);
60
- }
61
- for (const row of nameMatches ?? []) {
62
- const person = Array.isArray(row.people) ? row.people[0] : row.people;
63
- if (person?.id)
64
- byId.set(person.id, person);
65
- }
66
- return Array.from(byId.values()).slice(0, 20);
6
+ return getStore().searchPeople(query);
67
7
  }
68
8
  export async function getPerson(identifier) {
69
- const supabase = getSupabase();
70
- const { data: personById, error: idError } = await supabase
71
- .from("people")
72
- .select(PERSON_SELECT)
73
- .eq("id", identifier)
74
- .maybeSingle();
75
- if (idError)
76
- throw new Error(idError.message);
77
- if (personById)
78
- return personById;
79
- const { data: nameMatches, error: aliasError } = await supabase
80
- .from("person_names")
81
- .select("person_id")
82
- .ilike("name", identifier)
83
- .limit(2);
84
- if (aliasError)
85
- throw new Error(aliasError.message);
86
- if (!nameMatches || nameMatches.length === 0)
87
- return null;
88
- if (nameMatches.length > 1) {
89
- throw new Error(`Multiple people match "${identifier}". Use a UUID instead.`);
90
- }
91
- const { data, error } = await supabase
92
- .from("people")
93
- .select(PERSON_SELECT)
94
- .eq("id", nameMatches[0].person_id)
95
- .single();
96
- if (error)
97
- throw new Error(error.message);
98
- return data;
9
+ return getStore().getPerson(identifier);
99
10
  }
100
11
  export async function formatPersonMention(identifier, label) {
101
12
  const person = await getPerson(identifier);
@@ -0,0 +1,5 @@
1
+ import type { Provider } from "./types.js";
2
+ export declare const requiredTables: string[];
3
+ export declare function getSchema(provider: Provider): string;
4
+ export declare function getSupabaseSchemaPath(): string;
5
+ export declare const sqliteSchema = "\npragma foreign_keys = on;\n\ncreate table if not exists people (\n id text primary key,\n canonical_name text not null,\n display_name text,\n sort_name text,\n avatar_url text,\n bio text,\n note text,\n created_at text not null default current_timestamp,\n updated_at text not null default current_timestamp\n);\n\ncreate table if not exists person_names (\n id text primary key,\n person_id text not null references people(id) on delete cascade,\n name text not null,\n name_type text not null default 'alias'\n check (name_type in ('canonical', 'display', 'alias', 'nickname', 'courtesy_name', 'pen_name', 'former_name', 'foreign_name', 'other')),\n locale text,\n is_primary integer not null default 0,\n created_at text not null default current_timestamp\n);\n\ncreate unique index if not exists person_names_unique_lower_name_type\n on person_names (person_id, lower(name), name_type);\n\ncreate unique index if not exists person_names_primary_per_type\n on person_names (person_id, name_type)\n where is_primary = 1;\n\ncreate index if not exists person_names_name_idx\n on person_names (name);\n\ncreate table if not exists person_identifiers (\n id text primary key,\n person_id text not null references people(id) on delete cascade,\n identifier_type text not null,\n value text not null,\n url text,\n is_primary integer not null default 0,\n created_at text not null default current_timestamp,\n unique (identifier_type, value)\n);\n\ncreate table if not exists person_affiliations (\n id text primary key,\n person_id text not null references people(id) on delete cascade,\n organization_name text not null,\n role_title text,\n start_date text,\n end_date text,\n note text,\n created_at text not null default current_timestamp\n);\n\ncreate table if not exists sources (\n id text primary key,\n source_type text not null default 'manual',\n source_ref text,\n title text,\n url text,\n captured_at text,\n created_at text not null default current_timestamp\n);\n\ncreate table if not exists person_facts (\n id text primary key,\n person_id text not null references people(id) on delete cascade,\n predicate text not null,\n value text not null,\n confidence text not null default 'unverified'\n check (confidence in ('confirmed', 'likely', 'unverified')),\n source_id text references sources(id) on delete set null,\n valid_from text,\n valid_to text,\n created_at text not null default current_timestamp,\n updated_at text not null default current_timestamp\n);\n\ncreate index if not exists people_canonical_name_idx\n on people (canonical_name);\n\ncreate index if not exists people_display_name_idx\n on people (display_name);\n\ncreate index if not exists person_identifiers_person_id\n on person_identifiers (person_id);\n\ncreate index if not exists person_affiliations_person_id\n on person_affiliations (person_id);\n\ncreate index if not exists person_facts_person_id_created_at\n on person_facts (person_id, created_at desc);\n";
package/dist/schema.js ADDED
@@ -0,0 +1,117 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export const requiredTables = [
5
+ "people",
6
+ "person_names",
7
+ "person_identifiers",
8
+ "person_affiliations",
9
+ "sources",
10
+ "person_facts"
11
+ ];
12
+ export function getSchema(provider) {
13
+ if (provider === "sqlite")
14
+ return sqliteSchema;
15
+ return readFileSync(getSupabaseSchemaPath(), "utf8");
16
+ }
17
+ export function getSupabaseSchemaPath() {
18
+ const here = dirname(fileURLToPath(import.meta.url));
19
+ return join(here, "..", "supabase", "schema.sql");
20
+ }
21
+ export const sqliteSchema = `
22
+ pragma foreign_keys = on;
23
+
24
+ create table if not exists people (
25
+ id text primary key,
26
+ canonical_name text not null,
27
+ display_name text,
28
+ sort_name text,
29
+ avatar_url text,
30
+ bio text,
31
+ note text,
32
+ created_at text not null default current_timestamp,
33
+ updated_at text not null default current_timestamp
34
+ );
35
+
36
+ create table if not exists person_names (
37
+ id text primary key,
38
+ person_id text not null references people(id) on delete cascade,
39
+ name text not null,
40
+ name_type text not null default 'alias'
41
+ check (name_type in ('canonical', 'display', 'alias', 'nickname', 'courtesy_name', 'pen_name', 'former_name', 'foreign_name', 'other')),
42
+ locale text,
43
+ is_primary integer not null default 0,
44
+ created_at text not null default current_timestamp
45
+ );
46
+
47
+ create unique index if not exists person_names_unique_lower_name_type
48
+ on person_names (person_id, lower(name), name_type);
49
+
50
+ create unique index if not exists person_names_primary_per_type
51
+ on person_names (person_id, name_type)
52
+ where is_primary = 1;
53
+
54
+ create index if not exists person_names_name_idx
55
+ on person_names (name);
56
+
57
+ create table if not exists person_identifiers (
58
+ id text primary key,
59
+ person_id text not null references people(id) on delete cascade,
60
+ identifier_type text not null,
61
+ value text not null,
62
+ url text,
63
+ is_primary integer not null default 0,
64
+ created_at text not null default current_timestamp,
65
+ unique (identifier_type, value)
66
+ );
67
+
68
+ create table if not exists person_affiliations (
69
+ id text primary key,
70
+ person_id text not null references people(id) on delete cascade,
71
+ organization_name text not null,
72
+ role_title text,
73
+ start_date text,
74
+ end_date text,
75
+ note text,
76
+ created_at text not null default current_timestamp
77
+ );
78
+
79
+ create table if not exists sources (
80
+ id text primary key,
81
+ source_type text not null default 'manual',
82
+ source_ref text,
83
+ title text,
84
+ url text,
85
+ captured_at text,
86
+ created_at text not null default current_timestamp
87
+ );
88
+
89
+ create table if not exists person_facts (
90
+ id text primary key,
91
+ person_id text not null references people(id) on delete cascade,
92
+ predicate text not null,
93
+ value text not null,
94
+ confidence text not null default 'unverified'
95
+ check (confidence in ('confirmed', 'likely', 'unverified')),
96
+ source_id text references sources(id) on delete set null,
97
+ valid_from text,
98
+ valid_to text,
99
+ created_at text not null default current_timestamp,
100
+ updated_at text not null default current_timestamp
101
+ );
102
+
103
+ create index if not exists people_canonical_name_idx
104
+ on people (canonical_name);
105
+
106
+ create index if not exists people_display_name_idx
107
+ on people (display_name);
108
+
109
+ create index if not exists person_identifiers_person_id
110
+ on person_identifiers (person_id);
111
+
112
+ create index if not exists person_affiliations_person_id
113
+ on person_affiliations (person_id);
114
+
115
+ create index if not exists person_facts_person_id_created_at
116
+ on person_facts (person_id, created_at desc);
117
+ `;
@@ -0,0 +1,19 @@
1
+ import type { Provider, RuntimeConfig } from "./types.js";
2
+ export interface WriteEnvInput {
3
+ provider: Provider;
4
+ path: string;
5
+ force?: boolean;
6
+ supabaseUrl?: string;
7
+ supabaseKey?: string;
8
+ sqlitePath?: string;
9
+ }
10
+ export declare function writeEnvFile(input: WriteEnvInput): string;
11
+ export declare function setupDatabase(config?: RuntimeConfig): Promise<{
12
+ ok: boolean;
13
+ missingTables: string[];
14
+ }>;
15
+ export declare function checkDatabase(config?: RuntimeConfig): Promise<{
16
+ ok: boolean;
17
+ missingTables: string[];
18
+ }>;
19
+ export declare function schemaFor(provider: Provider): string;
package/dist/setup.js ADDED
@@ -0,0 +1,37 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { mkdirSync } from "node:fs";
4
+ import { getSchema } from "./schema.js";
5
+ import { getStore } from "./store.js";
6
+ import { defaultSqlitePath, readConfig } from "./config.js";
7
+ export function writeEnvFile(input) {
8
+ const envPath = resolve(input.path);
9
+ if (existsSync(envPath) && !input.force) {
10
+ throw new Error(`${envPath} already exists. Re-run with --force to overwrite it.`);
11
+ }
12
+ const sqlitePath = input.sqlitePath ?? defaultSqlitePath;
13
+ const lines = [`PEOPLE_TRUTH_PROVIDER="${input.provider}"`];
14
+ if (input.provider === "supabase") {
15
+ if (!input.supabaseUrl || !input.supabaseKey) {
16
+ throw new Error("Supabase setup requires --url and --key.");
17
+ }
18
+ lines.push(`SUPABASE_URL="${input.supabaseUrl}"`, `SUPABASE_KEY="${input.supabaseKey}"`);
19
+ }
20
+ else {
21
+ lines.push(`PEOPLE_TRUTH_SQLITE_PATH="${sqlitePath}"`);
22
+ mkdirSync(dirname(sqlitePath), { recursive: true });
23
+ }
24
+ writeFileSync(envPath, `${lines.join("\n")}\n`, { mode: 0o600 });
25
+ return envPath;
26
+ }
27
+ export async function setupDatabase(config = readConfig()) {
28
+ const store = getStore(config);
29
+ await store.initializeSchema();
30
+ return store.checkSchema();
31
+ }
32
+ export async function checkDatabase(config = readConfig()) {
33
+ return getStore(config).checkSchema();
34
+ }
35
+ export function schemaFor(provider) {
36
+ return getSchema(provider);
37
+ }
@@ -0,0 +1,2 @@
1
+ import type { PeopleTruthStore, RuntimeConfig } from "./types.js";
2
+ export declare function getStore(config?: RuntimeConfig): PeopleTruthStore;
package/dist/store.js ADDED
@@ -0,0 +1,12 @@
1
+ import { getConfig } from "./config.js";
2
+ import { getSupabase } from "./supabase.js";
3
+ import { SqliteStore } from "./stores/sqlite-store.js";
4
+ import { SupabaseStore } from "./stores/supabase-store.js";
5
+ export function getStore(config = getConfig()) {
6
+ if (config.provider === "sqlite") {
7
+ if (!config.sqlitePath)
8
+ throw new Error("Missing SQLite database path.");
9
+ return new SqliteStore(config.sqlitePath);
10
+ }
11
+ return new SupabaseStore(getSupabase(config));
12
+ }
@@ -0,0 +1,20 @@
1
+ import type { AddFactInput, AddPersonInput, PeopleTruthStore } from "../types.js";
2
+ export declare class SqliteStore implements PeopleTruthStore {
3
+ readonly sqlitePath: string;
4
+ readonly provider = "sqlite";
5
+ private readonly db;
6
+ constructor(sqlitePath: string);
7
+ initializeSchema(): Promise<void>;
8
+ checkSchema(): Promise<{
9
+ ok: boolean;
10
+ missingTables: string[];
11
+ }>;
12
+ addPerson(input: AddPersonInput): Promise<any>;
13
+ searchPeople(query: string): Promise<any[]>;
14
+ getPerson(identifier: string): Promise<any>;
15
+ addFact(input: AddFactInput): Promise<any>;
16
+ listFacts(identifier: string): Promise<any>;
17
+ close(): void;
18
+ private getPersonById;
19
+ private getFactById;
20
+ }
@@ -0,0 +1,172 @@
1
+ import Database from "better-sqlite3";
2
+ import { mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { requiredTables, sqliteSchema } from "../schema.js";
6
+ export class SqliteStore {
7
+ sqlitePath;
8
+ provider = "sqlite";
9
+ db;
10
+ constructor(sqlitePath) {
11
+ this.sqlitePath = sqlitePath;
12
+ mkdirSync(dirname(sqlitePath), { recursive: true });
13
+ this.db = new Database(sqlitePath);
14
+ this.db.pragma("foreign_keys = ON");
15
+ }
16
+ async initializeSchema() {
17
+ this.db.exec(sqliteSchema);
18
+ }
19
+ async checkSchema() {
20
+ const existing = new Set(this.db
21
+ .prepare("select name from sqlite_master where type = 'table'")
22
+ .all()
23
+ .map((row) => row.name));
24
+ const missingTables = requiredTables.filter((table) => !existing.has(table));
25
+ return { ok: missingTables.length === 0, missingTables };
26
+ }
27
+ async addPerson(input) {
28
+ const id = randomUUID();
29
+ const note = input.note ?? ([input.location, input.relationship].filter(Boolean).join("; ") || undefined);
30
+ const createPerson = this.db.transaction(() => {
31
+ this.db
32
+ .prepare("insert into people (id, canonical_name, display_name, bio, note) values (?, ?, ?, ?, ?)")
33
+ .run(id, input.name, input.name, input.bio ?? null, note ?? null);
34
+ const insertName = this.db.prepare("insert into person_names (id, person_id, name, name_type, is_primary) values (?, ?, ?, ?, ?)");
35
+ const names = Array.from(new Set([input.name, ...input.aliases].filter(Boolean)));
36
+ for (const alias of names) {
37
+ insertName.run(randomUUID(), id, alias, alias === input.name ? "canonical" : "alias", alias === input.name ? 1 : 0);
38
+ }
39
+ if (input.organization) {
40
+ this.db
41
+ .prepare("insert into person_affiliations (id, person_id, organization_name, role_title) values (?, ?, ?, ?)")
42
+ .run(randomUUID(), id, input.organization, input.role ?? null);
43
+ }
44
+ });
45
+ createPerson();
46
+ return this.getPerson(id);
47
+ }
48
+ async searchPeople(query) {
49
+ const pattern = `%${query.toLowerCase()}%`;
50
+ const rows = this.db
51
+ .prepare(`
52
+ select distinct p.id
53
+ from people p
54
+ left join person_names pn on pn.person_id = p.id
55
+ where lower(p.canonical_name) like ?
56
+ or lower(coalesce(p.display_name, '')) like ?
57
+ or lower(coalesce(p.bio, '')) like ?
58
+ or lower(coalesce(p.note, '')) like ?
59
+ or lower(coalesce(pn.name, '')) like ?
60
+ order by p.created_at desc
61
+ limit 20
62
+ `)
63
+ .all(pattern, pattern, pattern, pattern, pattern);
64
+ const people = [];
65
+ for (const row of rows) {
66
+ const person = await this.getPerson(row.id);
67
+ if (person)
68
+ people.push(compactPerson(person));
69
+ }
70
+ return people;
71
+ }
72
+ async getPerson(identifier) {
73
+ const byId = this.getPersonById(identifier);
74
+ if (byId)
75
+ return byId;
76
+ const nameMatches = this.db
77
+ .prepare("select distinct person_id from person_names where lower(name) = lower(?) limit 2")
78
+ .all(identifier);
79
+ if (nameMatches.length === 0)
80
+ return null;
81
+ if (nameMatches.length > 1) {
82
+ throw new Error(`Multiple people match "${identifier}". Use a UUID instead.`);
83
+ }
84
+ return this.getPersonById(nameMatches[0].person_id);
85
+ }
86
+ async addFact(input) {
87
+ const person = await this.getPerson(input.person);
88
+ if (!person)
89
+ throw new Error(`Person not found: ${input.person}`);
90
+ const factId = randomUUID();
91
+ const addFact = this.db.transaction(() => {
92
+ let sourceId = null;
93
+ if (input.sourceType || input.sourceRef || input.sourceTitle || input.sourceUrl) {
94
+ sourceId = randomUUID();
95
+ this.db
96
+ .prepare("insert into sources (id, source_type, source_ref, title, url) values (?, ?, ?, ?, ?)")
97
+ .run(sourceId, input.sourceType ?? "manual", input.sourceRef ?? null, input.sourceTitle ?? null, input.sourceUrl ?? null);
98
+ }
99
+ this.db
100
+ .prepare("insert into person_facts (id, person_id, predicate, value, confidence, source_id) values (?, ?, ?, ?, ?, ?)")
101
+ .run(factId, person.id, input.predicate, input.value, input.confidence, sourceId);
102
+ });
103
+ addFact();
104
+ return this.getFactById(factId);
105
+ }
106
+ async listFacts(identifier) {
107
+ const person = await this.getPerson(identifier);
108
+ if (!person)
109
+ throw new Error(`Person not found: ${identifier}`);
110
+ return person.person_facts ?? [];
111
+ }
112
+ close() {
113
+ this.db.close();
114
+ }
115
+ getPersonById(id) {
116
+ const person = this.db.prepare("select * from people where id = ?").get(id);
117
+ if (!person)
118
+ return null;
119
+ person.person_names = this.db
120
+ .prepare("select name, name_type, locale, is_primary from person_names where person_id = ? order by is_primary desc, created_at asc")
121
+ .all(id)
122
+ .map(booleanizePrimary);
123
+ person.person_identifiers = this.db
124
+ .prepare("select identifier_type, value, url, is_primary from person_identifiers where person_id = ? order by is_primary desc, created_at asc")
125
+ .all(id)
126
+ .map(booleanizePrimary);
127
+ person.person_affiliations = this.db
128
+ .prepare("select organization_name, role_title, start_date, end_date, note from person_affiliations where person_id = ? order by created_at desc")
129
+ .all(id);
130
+ person.person_facts = this.db
131
+ .prepare(`
132
+ select pf.*, s.source_type, s.source_ref, s.title as source_title, s.url as source_url
133
+ from person_facts pf
134
+ left join sources s on s.id = pf.source_id
135
+ where pf.person_id = ?
136
+ order by pf.created_at desc
137
+ `)
138
+ .all(id)
139
+ .map(formatFact);
140
+ return person;
141
+ }
142
+ getFactById(id) {
143
+ const row = this.db
144
+ .prepare(`
145
+ select pf.*, s.source_type, s.source_ref, s.title as source_title, s.url as source_url
146
+ from person_facts pf
147
+ left join sources s on s.id = pf.source_id
148
+ where pf.id = ?
149
+ `)
150
+ .get(id);
151
+ return row ? formatFact(row) : null;
152
+ }
153
+ }
154
+ function booleanizePrimary(row) {
155
+ return { ...row, is_primary: Boolean(row.is_primary) };
156
+ }
157
+ function formatFact(row) {
158
+ const source = row.source_id && row.source_type
159
+ ? {
160
+ source_type: row.source_type,
161
+ source_ref: row.source_ref,
162
+ title: row.source_title,
163
+ url: row.source_url
164
+ }
165
+ : null;
166
+ const { source_type, source_ref, source_title, source_url, ...fact } = row;
167
+ return { ...fact, sources: source };
168
+ }
169
+ function compactPerson(person) {
170
+ const { person_identifiers, person_facts, ...rest } = person;
171
+ return rest;
172
+ }
@@ -0,0 +1,18 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ import type { AddFactInput, AddPersonInput, PeopleTruthStore } from "../types.js";
3
+ export declare class SupabaseStore implements PeopleTruthStore {
4
+ private readonly supabase;
5
+ readonly provider = "supabase";
6
+ constructor(supabase: SupabaseClient);
7
+ initializeSchema(): Promise<void>;
8
+ checkSchema(): Promise<{
9
+ ok: boolean;
10
+ missingTables: string[];
11
+ }>;
12
+ addPerson(input: AddPersonInput): Promise<any>;
13
+ searchPeople(query: string): Promise<unknown[]>;
14
+ getPerson(identifier: string): Promise<any>;
15
+ addFact(input: AddFactInput): Promise<any>;
16
+ listFacts(identifier: string): Promise<any>;
17
+ }
18
+ export declare function supabaseSchema(): string;
@@ -0,0 +1,159 @@
1
+ import { getSchema, requiredTables } from "../schema.js";
2
+ const PERSON_SELECT = "*, person_names(name, name_type, locale, is_primary), person_identifiers(identifier_type, value, url, is_primary), person_affiliations(organization_name, role_title, start_date, end_date, note), person_facts(id, predicate, value, confidence, valid_from, valid_to, created_at, sources(source_type, source_ref, title, url))";
3
+ export class SupabaseStore {
4
+ supabase;
5
+ provider = "supabase";
6
+ constructor(supabase) {
7
+ this.supabase = supabase;
8
+ }
9
+ async initializeSchema() {
10
+ throw new Error("Supabase schema cannot be applied through the Data API. Run `ptruth db schema --provider supabase` and apply it in Supabase SQL editor, or use `supabase db push` from the repository.");
11
+ }
12
+ async checkSchema() {
13
+ const missingTables = [];
14
+ for (const table of requiredTables) {
15
+ const { error } = await this.supabase.from(table).select("*", { count: "exact", head: true });
16
+ if (error)
17
+ missingTables.push(table);
18
+ }
19
+ return { ok: missingTables.length === 0, missingTables };
20
+ }
21
+ async addPerson(input) {
22
+ const { data: person, error } = await this.supabase
23
+ .from("people")
24
+ .insert({
25
+ canonical_name: input.name,
26
+ display_name: input.name,
27
+ bio: input.bio,
28
+ note: input.note ?? ([input.location, input.relationship].filter(Boolean).join("; ") || undefined)
29
+ })
30
+ .select()
31
+ .single();
32
+ if (error)
33
+ throw new Error(error.message);
34
+ const names = Array.from(new Set([input.name, ...input.aliases].filter(Boolean)));
35
+ if (names.length > 0) {
36
+ const { error: nameError } = await this.supabase.from("person_names").insert(names.map((alias) => ({
37
+ person_id: person.id,
38
+ name: alias,
39
+ name_type: alias === input.name ? "canonical" : "alias",
40
+ is_primary: alias === input.name
41
+ })));
42
+ if (nameError)
43
+ throw new Error(nameError.message);
44
+ }
45
+ if (input.organization) {
46
+ const { error: affiliationError } = await this.supabase.from("person_affiliations").insert({
47
+ person_id: person.id,
48
+ organization_name: input.organization,
49
+ role_title: input.role
50
+ });
51
+ if (affiliationError)
52
+ throw new Error(affiliationError.message);
53
+ }
54
+ return this.getPerson(person.id);
55
+ }
56
+ async searchPeople(query) {
57
+ const pattern = `%${query}%`;
58
+ const select = "id, canonical_name, display_name, bio, note, person_names(name, name_type), person_affiliations(organization_name, role_title)";
59
+ const { data: directMatches, error } = await this.supabase
60
+ .from("people")
61
+ .select(select)
62
+ .or(`canonical_name.ilike.${pattern},display_name.ilike.${pattern},bio.ilike.${pattern},note.ilike.${pattern}`)
63
+ .limit(20);
64
+ if (error)
65
+ throw new Error(error.message);
66
+ const { data: nameMatches, error: nameError } = await this.supabase
67
+ .from("person_names")
68
+ .select(`people(${select})`)
69
+ .ilike("name", pattern)
70
+ .limit(20);
71
+ if (nameError)
72
+ throw new Error(nameError.message);
73
+ const byId = new Map();
74
+ for (const person of directMatches ?? []) {
75
+ byId.set(person.id, person);
76
+ }
77
+ for (const row of nameMatches ?? []) {
78
+ const person = Array.isArray(row.people) ? row.people[0] : row.people;
79
+ if (person?.id)
80
+ byId.set(person.id, person);
81
+ }
82
+ return Array.from(byId.values()).slice(0, 20);
83
+ }
84
+ async getPerson(identifier) {
85
+ const { data: personById, error: idError } = await this.supabase
86
+ .from("people")
87
+ .select(PERSON_SELECT)
88
+ .eq("id", identifier)
89
+ .maybeSingle();
90
+ if (idError)
91
+ throw new Error(idError.message);
92
+ if (personById)
93
+ return personById;
94
+ const { data: nameMatches, error: aliasError } = await this.supabase
95
+ .from("person_names")
96
+ .select("person_id")
97
+ .ilike("name", identifier)
98
+ .limit(2);
99
+ if (aliasError)
100
+ throw new Error(aliasError.message);
101
+ if (!nameMatches || nameMatches.length === 0)
102
+ return null;
103
+ if (nameMatches.length > 1) {
104
+ throw new Error(`Multiple people match "${identifier}". Use a UUID instead.`);
105
+ }
106
+ const { data, error } = await this.supabase
107
+ .from("people")
108
+ .select(PERSON_SELECT)
109
+ .eq("id", nameMatches[0].person_id)
110
+ .single();
111
+ if (error)
112
+ throw new Error(error.message);
113
+ return data;
114
+ }
115
+ async addFact(input) {
116
+ const person = await this.getPerson(input.person);
117
+ if (!person)
118
+ throw new Error(`Person not found: ${input.person}`);
119
+ let sourceId = null;
120
+ if (input.sourceType || input.sourceRef || input.sourceTitle || input.sourceUrl) {
121
+ const { data: source, error: sourceError } = await this.supabase
122
+ .from("sources")
123
+ .insert({
124
+ source_type: input.sourceType ?? "manual",
125
+ source_ref: input.sourceRef,
126
+ title: input.sourceTitle,
127
+ url: input.sourceUrl
128
+ })
129
+ .select()
130
+ .single();
131
+ if (sourceError)
132
+ throw new Error(sourceError.message);
133
+ sourceId = source.id;
134
+ }
135
+ const { data, error } = await this.supabase
136
+ .from("person_facts")
137
+ .insert({
138
+ person_id: person.id,
139
+ predicate: input.predicate,
140
+ value: input.value,
141
+ confidence: input.confidence,
142
+ source_id: sourceId
143
+ })
144
+ .select("*, sources(source_type, source_ref, title, url)")
145
+ .single();
146
+ if (error)
147
+ throw new Error(error.message);
148
+ return data;
149
+ }
150
+ async listFacts(identifier) {
151
+ const person = await this.getPerson(identifier);
152
+ if (!person)
153
+ throw new Error(`Person not found: ${identifier}`);
154
+ return person.person_facts ?? [];
155
+ }
156
+ }
157
+ export function supabaseSchema() {
158
+ return getSchema("supabase");
159
+ }
@@ -1 +1,2 @@
1
- export declare function getSupabase(): import("@supabase/supabase-js").SupabaseClient<any, "public", "public", any, any>;
1
+ import type { RuntimeConfig } from "./types.js";
2
+ export declare function getSupabase(config?: RuntimeConfig): import("@supabase/supabase-js").SupabaseClient<any, "public", "public", any, any>;
package/dist/supabase.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { createClient } from "@supabase/supabase-js";
2
2
  import { getConfig } from "./config.js";
3
- export function getSupabase() {
4
- const { SUPABASE_URL, SUPABASE_KEY } = getConfig();
5
- return createClient(SUPABASE_URL, SUPABASE_KEY, {
3
+ export function getSupabase(config = getConfig()) {
4
+ if (!config.supabaseUrl || !config.supabaseKey) {
5
+ throw new Error("Missing Supabase config. Run `ptruth setup --provider supabase`.");
6
+ }
7
+ return createClient(config.supabaseUrl, config.supabaseKey, {
6
8
  auth: {
7
9
  persistSession: false,
8
10
  autoRefreshToken: false
@@ -0,0 +1,41 @@
1
+ export type Provider = "supabase" | "sqlite";
2
+ export type Confidence = "confirmed" | "likely" | "unverified";
3
+ export interface RuntimeConfig {
4
+ provider: Provider;
5
+ supabaseUrl?: string;
6
+ supabaseKey?: string;
7
+ sqlitePath?: string;
8
+ }
9
+ export interface AddPersonInput {
10
+ name: string;
11
+ aliases: string[];
12
+ organization?: string;
13
+ role?: string;
14
+ location?: string;
15
+ relationship?: string;
16
+ bio?: string;
17
+ note?: string;
18
+ }
19
+ export interface AddFactInput {
20
+ person: string;
21
+ predicate: string;
22
+ value: string;
23
+ confidence: Confidence;
24
+ sourceType?: string;
25
+ sourceRef?: string;
26
+ sourceTitle?: string;
27
+ sourceUrl?: string;
28
+ }
29
+ export interface PeopleTruthStore {
30
+ provider: Provider;
31
+ initializeSchema(): Promise<void>;
32
+ checkSchema(): Promise<{
33
+ ok: boolean;
34
+ missingTables: string[];
35
+ }>;
36
+ addPerson(input: AddPersonInput): Promise<unknown>;
37
+ searchPeople(query: string): Promise<unknown[]>;
38
+ getPerson(identifier: string): Promise<any | null>;
39
+ addFact(input: AddFactInput): Promise<unknown>;
40
+ listFacts(identifier: string): Promise<unknown[]>;
41
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "people-truth",
3
- "version": "1.0.0",
4
- "description": "A Supabase-backed personal source of truth for people and relationship facts.",
3
+ "version": "1.1.3",
4
+ "description": "A portable source of truth for people and relationship facts backed by SQLite or Supabase.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -20,6 +20,8 @@
20
20
  "dist",
21
21
  "supabase",
22
22
  "skills",
23
+ ".env.example",
24
+ "sample.env",
23
25
  "README.md",
24
26
  "LICENSE"
25
27
  ],
@@ -35,10 +37,12 @@
35
37
  "prepack": "npm run build",
36
38
  "release": "semantic-release",
37
39
  "start": "node dist/cli.js",
40
+ "test": "npm run build && node --test test/*.test.mjs",
38
41
  "typecheck": "tsc --noEmit"
39
42
  },
40
43
  "dependencies": {
41
44
  "@supabase/supabase-js": "^2.108.1",
45
+ "better-sqlite3": "^12.10.0",
42
46
  "commander": "^12.1.0",
43
47
  "dotenv": "^16.6.1",
44
48
  "zod": "^3.25.76"
@@ -50,6 +54,7 @@
50
54
  "@semantic-release/github": "^12.0.8",
51
55
  "@semantic-release/npm": "^13.1.5",
52
56
  "@semantic-release/release-notes-generator": "^14.1.1",
57
+ "@types/better-sqlite3": "^7.6.13",
53
58
  "@types/node": "^22.10.2",
54
59
  "conventional-changelog-conventionalcommits": "^9.3.1",
55
60
  "semantic-release": "^25.0.5",
package/sample.env ADDED
@@ -0,0 +1,7 @@
1
+ PEOPLE_TRUTH_PROVIDER="sqlite"
2
+ PEOPLE_TRUTH_SQLITE_PATH="$HOME/.people-truth/people-truth.sqlite"
3
+
4
+ # For Supabase:
5
+ # PEOPLE_TRUTH_PROVIDER="supabase"
6
+ # SUPABASE_URL="https://your-project-ref.supabase.co"
7
+ # SUPABASE_KEY="your-service-role-or-secret-key"
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: people-truth
3
- description: Use the people-truth CLI and Supabase/Postgres schema when Codex needs to create, query, migrate, or reason about stable person identities, names, markdown person mentions, affiliations, identifiers, and sourced person facts in this repository or an installed people-truth CLI.
3
+ description: Use the people-truth CLI and SQLite or Supabase/Postgres schema when Codex needs to create, query, migrate, or reason about stable person identities, names, markdown person mentions, affiliations, identifiers, and sourced person facts in this repository or an installed people-truth CLI.
4
4
  ---
5
5
 
6
6
  # People Truth
7
7
 
8
8
  ## Core Model
9
9
 
10
- Treat Supabase as a replaceable Postgres runtime. The durable contract is the migration SQL and CLI behavior in this repository.
10
+ Treat SQLite as the local-first default and Supabase as a replaceable hosted Postgres runtime. The durable contract is the schema and CLI behavior in this repository.
11
11
 
12
12
  - `people.id` is the stable UUID identity.
13
13
  - `person_names` stores canonical names, display names, aliases, pen names, former names, foreign names, and other human labels.
@@ -29,7 +29,7 @@ Preserve the display label as written. Resolve the `person:uuid` target when cur
29
29
  Use `ptruth` when installed globally, or `npm run dev --` inside the repository.
30
30
 
31
31
  ```bash
32
- ptruth init --url "$SUPABASE_URL" --key "$SUPABASE_KEY"
32
+ ptruth setup --provider sqlite
33
33
  ptruth status
34
34
  ptruth people add "梁启超" --alias "梁任公" --alias "饮冰室主人"
35
35
  ptruth people search "梁任公"
@@ -39,11 +39,19 @@ ptruth facts add "梁启超" wrote "饮冰室合集" --source manual --confidenc
39
39
  ptruth facts list "梁启超" -o json
40
40
  ```
41
41
 
42
+ For Supabase:
43
+
44
+ ```bash
45
+ ptruth setup --provider supabase --url "$SUPABASE_URL" --key "$SUPABASE_KEY"
46
+ ptruth db status
47
+ ptruth db schema --provider supabase
48
+ ```
49
+
42
50
  When writing automation or agent integrations, prefer `-o json` and parse stdout as JSON.
43
51
 
44
52
  ## Migration Workflow
45
53
 
46
- Use `supabase/migrations/*.sql` as the source of truth. Keep `supabase/schema.sql` as a readable flattened snapshot of the current schema.
54
+ Use `ptruth db init` for SQLite. Use `supabase/migrations/*.sql` as the Supabase source of truth. Keep `supabase/schema.sql` as a readable flattened snapshot of the hosted schema.
47
55
 
48
56
  For local or linked Supabase projects:
49
57