kl-sqlite-card-state 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 ADDED
@@ -0,0 +1,101 @@
1
+ # kl-sqlite-card-state
2
+
3
+ A first-party [kanban-lite](https://github.com/borgius/kanban-lite) package for a SQLite-backed `card.state` provider.
4
+
5
+ ## What it provides
6
+
7
+ This package implements the shared `card.state` contract used by `kanban-lite` for:
8
+
9
+ - actor-scoped unread cursor persistence
10
+ - explicit open-card state persistence
11
+ - parity with the built-in file-backed `card.state` backend for unread derivation inputs and read/open mutations when exercised through the SDK
12
+
13
+ Unread derivation itself remains SDK-owned; this package persists the actor/card/domain state that the SDK reads and writes.
14
+
15
+ ## Provider id
16
+
17
+ `sqlite`
18
+
19
+ ## Capability
20
+
21
+ - `card.state`
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install kl-sqlite-card-state
27
+ ```
28
+
29
+ ## Configure
30
+
31
+ Use the `sqlite` compatibility id under `plugins['card.state']`:
32
+
33
+ ```json
34
+ {
35
+ "version": 2,
36
+ "plugins": {
37
+ "card.state": {
38
+ "provider": "sqlite",
39
+ "options": {
40
+ "sqlitePath": ".kanban/card-state.db"
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ### Options
48
+
49
+ - `sqlitePath` — optional relative or absolute SQLite database path. Defaults to `.kanban/card-state.db`.
50
+
51
+ ## Exports
52
+
53
+ The package exports:
54
+
55
+ - `createCardStateProvider(context)`
56
+ - `default` → `createCardStateProvider`
57
+ - `SQLITE_CARD_STATE_PROVIDER_ID`
58
+ - `DEFAULT_SQLITE_CARD_STATE_PATH`
59
+
60
+ The factory returns a contract-compatible provider with the same four operations expected by the SDK capability loader:
61
+
62
+ - `getCardState(...)`
63
+ - `setCardState(...)`
64
+ - `getUnreadCursor(...)`
65
+ - `markUnreadReadThrough(...)`
66
+
67
+ ## Semantics
68
+
69
+ - unread state is scoped by `actorId + boardId + cardId`
70
+ - explicit open state is stored independently from unread cursor state
71
+ - reads are side-effect free until the SDK calls an explicit mutation (`markCardOpened()` / `markCardRead()`)
72
+ - auth-absent mode still uses the same stable default actor contract as the built-in backend because actor resolution lives in the SDK, not in the provider
73
+
74
+ ## Build output
75
+
76
+ The published CommonJS entrypoint is:
77
+
78
+ ```text
79
+ dist/index.cjs
80
+ ```
81
+
82
+ Declaration output is emitted to `dist/index.d.ts`.
83
+
84
+ ## Development
85
+
86
+ ```bash
87
+ npm install
88
+ npm run build
89
+ npm test
90
+ npm run test:integration
91
+ npm run typecheck
92
+ ```
93
+
94
+ From the repository root you can also run:
95
+
96
+ ```bash
97
+ pnpm --filter kl-sqlite-card-state build
98
+ pnpm --filter kl-sqlite-card-state test
99
+ pnpm --filter kl-sqlite-card-state test:integration
100
+ pnpm --filter kl-sqlite-card-state typecheck
101
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ DEFAULT_SQLITE_CARD_STATE_PATH: () => DEFAULT_SQLITE_CARD_STATE_PATH,
34
+ SQLITE_CARD_STATE_PROVIDER_ID: () => SQLITE_CARD_STATE_PROVIDER_ID,
35
+ createCardStateProvider: () => createCardStateProvider,
36
+ default: () => src_default
37
+ });
38
+ module.exports = __toCommonJS(src_exports);
39
+ var fs = __toESM(require("node:fs"));
40
+ var path = __toESM(require("node:path"));
41
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"));
42
+ var SQLITE_CARD_STATE_PROVIDER_ID = "sqlite";
43
+ var DEFAULT_SQLITE_CARD_STATE_PATH = ".kanban/card-state.db";
44
+ var SQLITE_CARD_STATE_MANIFEST = Object.freeze({
45
+ id: SQLITE_CARD_STATE_PROVIDER_ID,
46
+ provides: ["card.state"]
47
+ });
48
+ var SQLITE_SCHEMA_VERSION = 1;
49
+ var CREATE_SCHEMA_SQL = `
50
+ PRAGMA journal_mode = WAL;
51
+ PRAGMA foreign_keys = ON;
52
+
53
+ CREATE TABLE IF NOT EXISTS schema_version (
54
+ version INTEGER NOT NULL
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS card_state (
58
+ actor_id TEXT NOT NULL,
59
+ board_id TEXT NOT NULL,
60
+ card_id TEXT NOT NULL,
61
+ domain TEXT NOT NULL,
62
+ value_json TEXT NOT NULL,
63
+ updated_at TEXT NOT NULL,
64
+ PRIMARY KEY (actor_id, board_id, card_id, domain)
65
+ );
66
+
67
+ CREATE INDEX IF NOT EXISTS idx_card_state_lookup
68
+ ON card_state (actor_id, board_id, card_id, domain);
69
+ `;
70
+ function isRecord(value) {
71
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
72
+ }
73
+ function isCardStateCursor(value) {
74
+ return isRecord(value) && typeof value.cursor === "string" && (value.updatedAt === void 0 || typeof value.updatedAt === "string");
75
+ }
76
+ function resolveSqlitePath(context) {
77
+ const rawPath = typeof context.options?.["sqlitePath"] === "string" && context.options["sqlitePath"].trim().length > 0 ? context.options["sqlitePath"].trim() : DEFAULT_SQLITE_CARD_STATE_PATH;
78
+ return path.isAbsolute(rawPath) ? rawPath : path.join(context.workspaceRoot, rawPath);
79
+ }
80
+ function createDatabase(sqlitePath) {
81
+ fs.mkdirSync(path.dirname(sqlitePath), { recursive: true });
82
+ const db = new import_better_sqlite3.default(sqlitePath);
83
+ db.exec(CREATE_SCHEMA_SQL);
84
+ const versionRow = db.prepare("SELECT version FROM schema_version LIMIT 1").get();
85
+ if (!versionRow) {
86
+ db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(SQLITE_SCHEMA_VERSION);
87
+ } else if (versionRow.version !== SQLITE_SCHEMA_VERSION) {
88
+ db.prepare("UPDATE schema_version SET version = ?").run(SQLITE_SCHEMA_VERSION);
89
+ }
90
+ return db;
91
+ }
92
+ function getUpdatedAt(updatedAt) {
93
+ return updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
94
+ }
95
+ function parseValue(valueJson) {
96
+ try {
97
+ const parsed = JSON.parse(valueJson);
98
+ return isRecord(parsed) ? parsed : null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ function toCardStateRecord(input, row, value) {
104
+ return {
105
+ actorId: input.actorId,
106
+ boardId: input.boardId,
107
+ cardId: input.cardId,
108
+ domain: input.domain,
109
+ value,
110
+ updatedAt: row.updated_at
111
+ };
112
+ }
113
+ function createCardStateProvider(context) {
114
+ const sqlitePath = resolveSqlitePath(context);
115
+ const db = createDatabase(sqlitePath);
116
+ const selectState = db.prepare(`
117
+ SELECT actor_id, board_id, card_id, domain, value_json, updated_at
118
+ FROM card_state
119
+ WHERE actor_id = ? AND board_id = ? AND card_id = ? AND domain = ?
120
+ `);
121
+ const upsertState = db.prepare(`
122
+ INSERT INTO card_state (actor_id, board_id, card_id, domain, value_json, updated_at)
123
+ VALUES (?, ?, ?, ?, ?, ?)
124
+ ON CONFLICT(actor_id, board_id, card_id, domain)
125
+ DO UPDATE SET
126
+ value_json = excluded.value_json,
127
+ updated_at = excluded.updated_at
128
+ `);
129
+ return {
130
+ manifest: SQLITE_CARD_STATE_MANIFEST,
131
+ async getCardState(_input) {
132
+ const row = selectState.get(
133
+ _input.actorId,
134
+ _input.boardId,
135
+ _input.cardId,
136
+ _input.domain
137
+ );
138
+ if (!row)
139
+ return null;
140
+ const value = parseValue(row.value_json);
141
+ if (!value)
142
+ return null;
143
+ return toCardStateRecord(_input, row, value);
144
+ },
145
+ async setCardState(_input) {
146
+ const updatedAt = getUpdatedAt(_input.updatedAt);
147
+ upsertState.run(
148
+ _input.actorId,
149
+ _input.boardId,
150
+ _input.cardId,
151
+ _input.domain,
152
+ JSON.stringify(_input.value),
153
+ updatedAt
154
+ );
155
+ return {
156
+ actorId: _input.actorId,
157
+ boardId: _input.boardId,
158
+ cardId: _input.cardId,
159
+ domain: _input.domain,
160
+ value: _input.value,
161
+ updatedAt
162
+ };
163
+ },
164
+ async getUnreadCursor(_input) {
165
+ const record = await this.getCardState({ ..._input, domain: "unread" });
166
+ return record && isCardStateCursor(record.value) ? record.value : null;
167
+ },
168
+ async markUnreadReadThrough(_input) {
169
+ const updatedAt = getUpdatedAt(_input.cursor.updatedAt);
170
+ const value = {
171
+ cursor: _input.cursor.cursor,
172
+ updatedAt
173
+ };
174
+ upsertState.run(
175
+ _input.actorId,
176
+ _input.boardId,
177
+ _input.cardId,
178
+ "unread",
179
+ JSON.stringify(value),
180
+ updatedAt
181
+ );
182
+ return {
183
+ actorId: _input.actorId,
184
+ boardId: _input.boardId,
185
+ cardId: _input.cardId,
186
+ domain: "unread",
187
+ value,
188
+ updatedAt
189
+ };
190
+ }
191
+ };
192
+ }
193
+ var src_default = createCardStateProvider;
194
+ // Annotate the CommonJS export names for ESM import in node:
195
+ 0 && (module.exports = {
196
+ DEFAULT_SQLITE_CARD_STATE_PATH,
197
+ SQLITE_CARD_STATE_PROVIDER_ID,
198
+ createCardStateProvider
199
+ });
@@ -0,0 +1,66 @@
1
+ /** Shared plugin manifest shape for `card.state` capability providers. */
2
+ export interface CardStateProviderManifest {
3
+ readonly id: string;
4
+ readonly provides: readonly ['card.state'];
5
+ }
6
+ /** Opaque JSON-like payload stored for a card-state domain. */
7
+ export type CardStateValue = Record<string, unknown>;
8
+ /** Stable actor/card/domain lookup key used by card-state providers. */
9
+ export interface CardStateKey {
10
+ actorId: string;
11
+ boardId: string;
12
+ cardId: string;
13
+ domain: string;
14
+ }
15
+ /** Stored card-state record returned by provider operations. */
16
+ export interface CardStateRecord<TValue = CardStateValue> extends CardStateKey {
17
+ value: TValue;
18
+ updatedAt: string;
19
+ }
20
+ /** Write input for card-state domain mutations. */
21
+ export interface CardStateWriteInput<TValue = CardStateValue> extends CardStateKey {
22
+ value: TValue;
23
+ updatedAt?: string;
24
+ }
25
+ /** Unread cursor payload persisted by card-state providers. */
26
+ export interface CardStateCursor extends Record<string, unknown> {
27
+ cursor: string;
28
+ updatedAt?: string;
29
+ }
30
+ /** Lookup key for unread cursor state. */
31
+ export interface CardStateUnreadKey {
32
+ actorId: string;
33
+ boardId: string;
34
+ cardId: string;
35
+ }
36
+ /** Mutation input for marking unread state through a cursor. */
37
+ export interface CardStateReadThroughInput extends CardStateUnreadKey {
38
+ cursor: CardStateCursor;
39
+ }
40
+ /** Shared runtime context passed to and exposed for `card.state` providers. */
41
+ export interface CardStateModuleContext {
42
+ workspaceRoot: string;
43
+ kanbanDir: string;
44
+ provider: string;
45
+ backend: 'builtin' | 'external';
46
+ options?: Record<string, unknown>;
47
+ }
48
+ /** Contract for first-class `card.state` capability providers. */
49
+ export interface CardStateProvider {
50
+ readonly manifest: CardStateProviderManifest;
51
+ getCardState(input: CardStateKey): Promise<CardStateRecord | null>;
52
+ setCardState(input: CardStateWriteInput): Promise<CardStateRecord>;
53
+ getUnreadCursor(input: CardStateUnreadKey): Promise<CardStateCursor | null>;
54
+ markUnreadReadThrough(input: CardStateReadThroughInput): Promise<CardStateRecord<CardStateCursor>>;
55
+ }
56
+ export declare const SQLITE_CARD_STATE_PROVIDER_ID = "sqlite";
57
+ export declare const DEFAULT_SQLITE_CARD_STATE_PATH = ".kanban/card-state.db";
58
+ /**
59
+ * Creates the SQLite-backed `card.state` provider.
60
+ *
61
+ * Data is stored in a dedicated SQLite database so actor-scoped unread and
62
+ * explicit-open state can be shared across SDK instances without writing any
63
+ * per-user state into markdown cards or active-card UI storage.
64
+ */
65
+ export declare function createCardStateProvider(context: CardStateModuleContext): CardStateProvider;
66
+ export default createCardStateProvider;
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "kl-sqlite-card-state",
3
+ "version": "0.1.1",
4
+ "description": "kanban-lite card.state plugin for SQLite-backed actor-scoped card state",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/borgius/kl-sqlite-card-state.git"
10
+ },
11
+ "homepage": "https://github.com/borgius/kl-sqlite-card-state#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/borgius/kl-sqlite-card-state/issues"
14
+ },
15
+ "main": "dist/index.cjs",
16
+ "types": "dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "require": "./dist/index.cjs",
21
+ "default": "./dist/index.cjs"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "build": "esbuild src/index.ts --bundle --outfile=dist/index.cjs --format=cjs --platform=node --external:better-sqlite3 --external:kanban-lite --external:kanban-lite/sdk && tsc -p tsconfig.json --declaration --emitDeclarationOnly --outDir dist",
33
+ "clean": "rm -rf dist",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run src/index.test.ts",
36
+ "test:watch": "vitest",
37
+ "test:integration": "npm run build && vitest run src/sqlite-card-state.workspace.test.ts",
38
+ "release": "npm run clean && npm run build && npm version patch && npm publish",
39
+ "release:minor": "npm run clean && npm run build && npm version minor && npm publish",
40
+ "release:major": "npm run clean && npm run build && npm version major && npm publish"
41
+ },
42
+ "dependencies": {
43
+ "better-sqlite3": "^12.6.2"
44
+ },
45
+ "devDependencies": {
46
+ "@types/better-sqlite3": "^7.6.13",
47
+ "@types/node": "^20.10.0",
48
+ "esbuild": "^0.19.0",
49
+ "typescript": "^5.3.0",
50
+ "vitest": "^4.0.18"
51
+ },
52
+ "peerDependencies": {
53
+ "kanban-lite": ">=1.0.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "kanban-lite": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "keywords": [
61
+ "kanban-lite",
62
+ "kanban",
63
+ "card.state",
64
+ "sqlite",
65
+ "plugin"
66
+ ]
67
+ }