@axiom-lattice/local-stores 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Local SQLite implementation of ScheduleStorage.
3
+ */
4
+
5
+ import { DatabaseWrapper } from "../database";
6
+ import {
7
+ ScheduleStorage,
8
+ ScheduledTaskDefinition,
9
+ ScheduledTaskStatus,
10
+ ScheduleExecutionType,
11
+ } from "@axiom-lattice/protocols";
12
+ import { ensureTable } from "../database";
13
+
14
+ const DDL = `
15
+ CREATE TABLE IF NOT EXISTS lt_scheduled_tasks (
16
+ task_id TEXT PRIMARY KEY,
17
+ task_type TEXT NOT NULL,
18
+ tenant_id TEXT NOT NULL,
19
+ payload TEXT NOT NULL DEFAULT '{}',
20
+ assistant_id TEXT,
21
+ thread_id TEXT,
22
+ execution_type TEXT NOT NULL,
23
+ execute_at TEXT,
24
+ delay_ms INTEGER,
25
+ cron_expression TEXT,
26
+ timezone TEXT,
27
+ next_run_at TEXT,
28
+ last_run_at TEXT,
29
+ status TEXT NOT NULL DEFAULT 'pending',
30
+ run_count INTEGER NOT NULL DEFAULT 0,
31
+ max_runs INTEGER,
32
+ retry_count INTEGER NOT NULL DEFAULT 0,
33
+ max_retries INTEGER NOT NULL DEFAULT 0,
34
+ last_error TEXT,
35
+ created_at TEXT NOT NULL,
36
+ updated_at TEXT NOT NULL,
37
+ expires_at TEXT,
38
+ metadata TEXT
39
+ );
40
+ CREATE INDEX IF NOT EXISTS idx_lt_st_status ON lt_scheduled_tasks(status);
41
+ CREATE INDEX IF NOT EXISTS idx_lt_st_type ON lt_scheduled_tasks(task_type);
42
+ CREATE INDEX IF NOT EXISTS idx_lt_st_tenant ON lt_scheduled_tasks(tenant_id);
43
+ `;
44
+
45
+ interface TaskRow {
46
+ task_id: string;
47
+ task_type: string;
48
+ tenant_id: string;
49
+ payload: string;
50
+ assistant_id: string | null;
51
+ thread_id: string | null;
52
+ execution_type: string;
53
+ execute_at: string | null;
54
+ delay_ms: number | null;
55
+ cron_expression: string | null;
56
+ timezone: string | null;
57
+ next_run_at: string | null;
58
+ last_run_at: string | null;
59
+ status: string;
60
+ run_count: number;
61
+ max_runs: number | null;
62
+ retry_count: number;
63
+ max_retries: number;
64
+ last_error: string | null;
65
+ created_at: string;
66
+ updated_at: string;
67
+ expires_at: string | null;
68
+ metadata: string | null;
69
+ }
70
+
71
+ export class LocalScheduleStorage implements ScheduleStorage {
72
+ private db: DatabaseWrapper;
73
+
74
+ constructor(db: DatabaseWrapper) {
75
+ this.db = db;
76
+ ensureTable(db, DDL);
77
+ }
78
+
79
+ async save(task: ScheduledTaskDefinition): Promise<void> {
80
+ this.db.prepare(
81
+ `INSERT INTO lt_scheduled_tasks (
82
+ task_id, task_type, tenant_id, payload, assistant_id, thread_id, execution_type,
83
+ execute_at, delay_ms, cron_expression, timezone, next_run_at, last_run_at,
84
+ status, run_count, max_runs, retry_count, max_retries, last_error,
85
+ created_at, updated_at, expires_at, metadata
86
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
87
+ ON CONFLICT(task_id) DO UPDATE SET
88
+ task_type = excluded.task_type, tenant_id = excluded.tenant_id,
89
+ payload = excluded.payload, assistant_id = excluded.assistant_id,
90
+ thread_id = excluded.thread_id, execution_type = excluded.execution_type,
91
+ execute_at = excluded.execute_at, delay_ms = excluded.delay_ms,
92
+ cron_expression = excluded.cron_expression, timezone = excluded.timezone,
93
+ next_run_at = excluded.next_run_at, last_run_at = excluded.last_run_at,
94
+ status = excluded.status, run_count = excluded.run_count,
95
+ max_runs = excluded.max_runs, retry_count = excluded.retry_count,
96
+ max_retries = excluded.max_retries, last_error = excluded.last_error,
97
+ updated_at = excluded.updated_at, expires_at = excluded.expires_at,
98
+ metadata = excluded.metadata`,
99
+ ).run(
100
+ task.taskId, task.taskType, task.tenantId, JSON.stringify(task.payload),
101
+ task.assistantId ?? null, task.threadId ?? null, task.executionType,
102
+ task.executeAt ? new Date(task.executeAt).toISOString() : null,
103
+ task.delayMs ?? null, task.cronExpression ?? null, task.timezone ?? null,
104
+ task.nextRunAt ? new Date(task.nextRunAt).toISOString() : null,
105
+ task.lastRunAt ? new Date(task.lastRunAt).toISOString() : null,
106
+ task.status, task.runCount, task.maxRuns ?? null,
107
+ task.retryCount, task.maxRetries, task.lastError ?? null,
108
+ new Date(task.createdAt).toISOString(), new Date(task.updatedAt).toISOString(),
109
+ task.expiresAt ? new Date(task.expiresAt).toISOString() : null,
110
+ task.metadata ? JSON.stringify(task.metadata) : null,
111
+ );
112
+ }
113
+
114
+ async get(taskId: string): Promise<ScheduledTaskDefinition | null> {
115
+ const row = this.db.prepare(
116
+ `SELECT * FROM lt_scheduled_tasks WHERE task_id = ?`,
117
+ ).get(taskId) as unknown as TaskRow | undefined;
118
+ return row ? mapRowToTask(row) : null;
119
+ }
120
+
121
+ async update(taskId: string, updates: Partial<ScheduledTaskDefinition>): Promise<void> {
122
+ const setClauses: string[] = [];
123
+ const values: unknown[] = [];
124
+
125
+ if (updates.taskType !== undefined) { setClauses.push("task_type = ?"); values.push(updates.taskType); }
126
+ if (updates.tenantId !== undefined) { setClauses.push("tenant_id = ?"); values.push(updates.tenantId); }
127
+ if (updates.payload !== undefined) { setClauses.push("payload = ?"); values.push(JSON.stringify(updates.payload)); }
128
+ if (updates.assistantId !== undefined) { setClauses.push("assistant_id = ?"); values.push(updates.assistantId ?? null); }
129
+ if (updates.threadId !== undefined) { setClauses.push("thread_id = ?"); values.push(updates.threadId ?? null); }
130
+ if (updates.executionType !== undefined) { setClauses.push("execution_type = ?"); values.push(updates.executionType); }
131
+ if (updates.executeAt !== undefined) { setClauses.push("execute_at = ?"); values.push(updates.executeAt ? new Date(updates.executeAt).toISOString() : null); }
132
+ if (updates.delayMs !== undefined) { setClauses.push("delay_ms = ?"); values.push(updates.delayMs ?? null); }
133
+ if (updates.cronExpression !== undefined) { setClauses.push("cron_expression = ?"); values.push(updates.cronExpression ?? null); }
134
+ if (updates.timezone !== undefined) { setClauses.push("timezone = ?"); values.push(updates.timezone ?? null); }
135
+ if (updates.nextRunAt !== undefined) { setClauses.push("next_run_at = ?"); values.push(updates.nextRunAt ? new Date(updates.nextRunAt).toISOString() : null); }
136
+ if (updates.lastRunAt !== undefined) { setClauses.push("last_run_at = ?"); values.push(updates.lastRunAt ? new Date(updates.lastRunAt).toISOString() : null); }
137
+ if (updates.status !== undefined) { setClauses.push("status = ?"); values.push(updates.status); }
138
+ if (updates.runCount !== undefined) { setClauses.push("run_count = ?"); values.push(updates.runCount); }
139
+ if (updates.maxRuns !== undefined) { setClauses.push("max_runs = ?"); values.push(updates.maxRuns ?? null); }
140
+ if (updates.retryCount !== undefined) { setClauses.push("retry_count = ?"); values.push(updates.retryCount); }
141
+ if (updates.maxRetries !== undefined) { setClauses.push("max_retries = ?"); values.push(updates.maxRetries); }
142
+ if (updates.lastError !== undefined) { setClauses.push("last_error = ?"); values.push(updates.lastError ?? null); }
143
+ if (updates.expiresAt !== undefined) { setClauses.push("expires_at = ?"); values.push(updates.expiresAt ? new Date(updates.expiresAt).toISOString() : null); }
144
+ if (updates.metadata !== undefined) { setClauses.push("metadata = ?"); values.push(updates.metadata ? JSON.stringify(updates.metadata) : null); }
145
+
146
+ if (setClauses.length === 0) return;
147
+
148
+ setClauses.push("updated_at = ?");
149
+ values.push(new Date().toISOString());
150
+ values.push(taskId);
151
+
152
+ this.db.prepare(
153
+ `UPDATE lt_scheduled_tasks SET ${setClauses.join(", ")} WHERE task_id = ?`,
154
+ ).run(...values);
155
+ }
156
+
157
+ async delete(taskId: string): Promise<void> {
158
+ this.db.prepare(`DELETE FROM lt_scheduled_tasks WHERE task_id = ?`).run(taskId);
159
+ }
160
+
161
+ async getActiveTasks(): Promise<ScheduledTaskDefinition[]> {
162
+ const rows = this.db.prepare(
163
+ `SELECT * FROM lt_scheduled_tasks WHERE status IN ('pending', 'paused') ORDER BY created_at ASC`,
164
+ ).all() as unknown as TaskRow[];
165
+ return rows.map(mapRowToTask);
166
+ }
167
+
168
+ async getTasksByType(taskType: string): Promise<ScheduledTaskDefinition[]> {
169
+ const rows = this.db.prepare(
170
+ `SELECT * FROM lt_scheduled_tasks WHERE task_type = ? ORDER BY created_at DESC`,
171
+ ).all(taskType) as unknown as TaskRow[];
172
+ return rows.map(mapRowToTask);
173
+ }
174
+
175
+ async getTasksByStatus(status: ScheduledTaskStatus): Promise<ScheduledTaskDefinition[]> {
176
+ const rows = this.db.prepare(
177
+ `SELECT * FROM lt_scheduled_tasks WHERE status = ? ORDER BY created_at DESC`,
178
+ ).all(status) as unknown as TaskRow[];
179
+ return rows.map(mapRowToTask);
180
+ }
181
+
182
+ async getTasksByExecutionType(executionType: ScheduleExecutionType): Promise<ScheduledTaskDefinition[]> {
183
+ const rows = this.db.prepare(
184
+ `SELECT * FROM lt_scheduled_tasks WHERE execution_type = ? ORDER BY created_at DESC`,
185
+ ).all(executionType) as unknown as TaskRow[];
186
+ return rows.map(mapRowToTask);
187
+ }
188
+
189
+ async getTasksByAssistantId(assistantId: string): Promise<ScheduledTaskDefinition[]> {
190
+ const rows = this.db.prepare(
191
+ `SELECT * FROM lt_scheduled_tasks WHERE assistant_id = ? ORDER BY created_at DESC`,
192
+ ).all(assistantId) as unknown as TaskRow[];
193
+ return rows.map(mapRowToTask);
194
+ }
195
+
196
+ async getTasksByThreadId(threadId: string): Promise<ScheduledTaskDefinition[]> {
197
+ const rows = this.db.prepare(
198
+ `SELECT * FROM lt_scheduled_tasks WHERE thread_id = ? ORDER BY created_at DESC`,
199
+ ).all(threadId) as unknown as TaskRow[];
200
+ return rows.map(mapRowToTask);
201
+ }
202
+
203
+ async getAllTasks(filters?: {
204
+ tenantId?: string; status?: ScheduledTaskStatus;
205
+ executionType?: ScheduleExecutionType; taskType?: string;
206
+ assistantId?: string; threadId?: string;
207
+ limit?: number; offset?: number;
208
+ }): Promise<ScheduledTaskDefinition[]> {
209
+ const conditions: string[] = [];
210
+ const values: unknown[] = [];
211
+
212
+ if (filters?.tenantId !== undefined) { conditions.push("tenant_id = ?"); values.push(filters.tenantId); }
213
+ if (filters?.status !== undefined) { conditions.push("status = ?"); values.push(filters.status); }
214
+ if (filters?.executionType !== undefined) { conditions.push("execution_type = ?"); values.push(filters.executionType); }
215
+ if (filters?.taskType !== undefined) { conditions.push("task_type = ?"); values.push(filters.taskType); }
216
+ if (filters?.assistantId !== undefined) { conditions.push("assistant_id = ?"); values.push(filters.assistantId); }
217
+ if (filters?.threadId !== undefined) { conditions.push("thread_id = ?"); values.push(filters.threadId); }
218
+
219
+ let query = `SELECT * FROM lt_scheduled_tasks`;
220
+ if (conditions.length > 0) query += ` WHERE ${conditions.join(" AND ")}`;
221
+ query += ` ORDER BY created_at DESC`;
222
+
223
+ if (filters?.limit !== undefined) { query += ` LIMIT ?`; values.push(filters.limit); }
224
+ if (filters?.offset !== undefined) { query += ` OFFSET ?`; values.push(filters.offset); }
225
+
226
+ const rows = this.db.prepare(query).all(...values) as unknown as TaskRow[];
227
+ return rows.map(mapRowToTask);
228
+ }
229
+
230
+ async countTasks(filters?: {
231
+ tenantId?: string; status?: ScheduledTaskStatus;
232
+ executionType?: ScheduleExecutionType; taskType?: string;
233
+ assistantId?: string; threadId?: string;
234
+ }): Promise<number> {
235
+ const conditions: string[] = [];
236
+ const values: unknown[] = [];
237
+
238
+ if (filters?.tenantId !== undefined) { conditions.push("tenant_id = ?"); values.push(filters.tenantId); }
239
+ if (filters?.status !== undefined) { conditions.push("status = ?"); values.push(filters.status); }
240
+ if (filters?.executionType !== undefined) { conditions.push("execution_type = ?"); values.push(filters.executionType); }
241
+ if (filters?.taskType !== undefined) { conditions.push("task_type = ?"); values.push(filters.taskType); }
242
+ if (filters?.assistantId !== undefined) { conditions.push("assistant_id = ?"); values.push(filters.assistantId); }
243
+ if (filters?.threadId !== undefined) { conditions.push("thread_id = ?"); values.push(filters.threadId); }
244
+
245
+ let query = `SELECT COUNT(*) as count FROM lt_scheduled_tasks`;
246
+ if (conditions.length > 0) query += ` WHERE ${conditions.join(" AND ")}`;
247
+
248
+ const row = this.db.prepare(query).get(...values) as { count: number };
249
+ return row.count;
250
+ }
251
+
252
+ async deleteOldTasks(olderThanMs: number): Promise<number> {
253
+ const cutoff = new Date(Date.now() - olderThanMs).toISOString();
254
+ const result = this.db.prepare(
255
+ `DELETE FROM lt_scheduled_tasks WHERE status IN ('completed', 'cancelled', 'failed') AND updated_at < ?`,
256
+ ).run(cutoff);
257
+ return result.changes;
258
+ }
259
+ }
260
+
261
+ function mapRowToTask(row: TaskRow): ScheduledTaskDefinition {
262
+ return {
263
+ taskId: row.task_id,
264
+ taskType: row.task_type,
265
+ tenantId: row.tenant_id,
266
+ payload: JSON.parse(row.payload || "{}"),
267
+ assistantId: row.assistant_id ?? undefined,
268
+ threadId: row.thread_id ?? undefined,
269
+ executionType: row.execution_type as ScheduleExecutionType,
270
+ executeAt: row.execute_at ? new Date(row.execute_at).getTime() : undefined,
271
+ delayMs: row.delay_ms ?? undefined,
272
+ cronExpression: row.cron_expression ?? undefined,
273
+ timezone: row.timezone ?? undefined,
274
+ nextRunAt: row.next_run_at ? new Date(row.next_run_at).getTime() : undefined,
275
+ lastRunAt: row.last_run_at ? new Date(row.last_run_at).getTime() : undefined,
276
+ status: row.status as ScheduledTaskStatus,
277
+ runCount: row.run_count,
278
+ maxRuns: row.max_runs ?? undefined,
279
+ retryCount: row.retry_count,
280
+ maxRetries: row.max_retries,
281
+ lastError: row.last_error ?? undefined,
282
+ createdAt: new Date(row.created_at).getTime(),
283
+ updatedAt: new Date(row.updated_at).getTime(),
284
+ expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : undefined,
285
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
286
+ };
287
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Local SQLite implementation of SkillStore.
3
+ */
4
+
5
+ import { DatabaseWrapper } from "../database";
6
+ import type {
7
+ SkillStore,
8
+ Skill,
9
+ CreateSkillRequest,
10
+ SkillStoreContext,
11
+ } from "@axiom-lattice/protocols";
12
+ import { ensureTable, nowISO, parseISO } from "../database";
13
+
14
+ const DDL = `
15
+ CREATE TABLE IF NOT EXISTS lt_skills (
16
+ id TEXT NOT NULL,
17
+ tenant_id TEXT NOT NULL,
18
+ name TEXT NOT NULL,
19
+ description TEXT NOT NULL,
20
+ license TEXT,
21
+ compatibility TEXT,
22
+ metadata TEXT DEFAULT '{}',
23
+ content TEXT,
24
+ sub_skills TEXT DEFAULT '[]',
25
+ created_at TEXT NOT NULL,
26
+ updated_at TEXT NOT NULL,
27
+ PRIMARY KEY (tenant_id, id)
28
+ );
29
+ `;
30
+
31
+ interface SkillRow {
32
+ id: string;
33
+ tenant_id: string;
34
+ name: string;
35
+ description: string;
36
+ license: string | null;
37
+ compatibility: string | null;
38
+ metadata: string;
39
+ content: string | null;
40
+ sub_skills: string;
41
+ created_at: string;
42
+ updated_at: string;
43
+ }
44
+
45
+ export class LocalSkillStore implements SkillStore {
46
+ private db: DatabaseWrapper;
47
+
48
+ constructor(db: DatabaseWrapper) {
49
+ this.db = db;
50
+ ensureTable(db, DDL);
51
+ }
52
+
53
+ async getAllSkills(tenantId: string, _context?: SkillStoreContext): Promise<Skill[]> {
54
+ const rows = this.db.prepare(
55
+ `SELECT * FROM lt_skills WHERE tenant_id = ? ORDER BY created_at DESC`,
56
+ ).all(tenantId) as unknown as SkillRow[];
57
+ return rows.map(mapRowToSkill);
58
+ }
59
+
60
+ async getSkillById(tenantId: string, id: string, _context?: SkillStoreContext): Promise<Skill | null> {
61
+ const row = this.db.prepare(
62
+ `SELECT * FROM lt_skills WHERE tenant_id = ? AND id = ?`,
63
+ ).get(tenantId, id) as unknown as SkillRow | undefined;
64
+ return row ? mapRowToSkill(row) : null;
65
+ }
66
+
67
+ async createSkill(tenantId: string, id: string, data: CreateSkillRequest, _context?: SkillStoreContext): Promise<Skill> {
68
+ const now = nowISO();
69
+
70
+ this.db.prepare(
71
+ `INSERT INTO lt_skills (id, tenant_id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at)
72
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
73
+ ON CONFLICT(tenant_id, id) DO UPDATE SET
74
+ name = excluded.name, description = excluded.description,
75
+ license = excluded.license, compatibility = excluded.compatibility,
76
+ metadata = excluded.metadata, content = excluded.content,
77
+ sub_skills = excluded.sub_skills, updated_at = excluded.updated_at`,
78
+ ).run(id, tenantId, data.name, data.description,
79
+ data.license || null, data.compatibility || null,
80
+ JSON.stringify(data.metadata || {}), data.content || null,
81
+ JSON.stringify(data.subSkills || []), now, now);
82
+
83
+ return {
84
+ id, tenantId, name: data.name, description: data.description,
85
+ license: data.license, compatibility: data.compatibility,
86
+ metadata: data.metadata || {}, content: data.content,
87
+ subSkills: data.subSkills,
88
+ createdAt: parseISO(now), updatedAt: parseISO(now),
89
+ };
90
+ }
91
+
92
+ async updateSkill(
93
+ tenantId: string, id: string, updates: Partial<CreateSkillRequest>, _context?: SkillStoreContext,
94
+ ): Promise<Skill | null> {
95
+ const existing = await this.getSkillById(tenantId, id);
96
+ if (!existing) return null;
97
+
98
+ const setClauses: string[] = [];
99
+ const values: unknown[] = [];
100
+
101
+ if (updates.name !== undefined) { setClauses.push("name = ?"); values.push(updates.name); }
102
+ if (updates.description !== undefined) { setClauses.push("description = ?"); values.push(updates.description); }
103
+ if (updates.license !== undefined) { setClauses.push("license = ?"); values.push(updates.license || null); }
104
+ if (updates.compatibility !== undefined) { setClauses.push("compatibility = ?"); values.push(updates.compatibility || null); }
105
+ if (updates.metadata !== undefined) { setClauses.push("metadata = ?"); values.push(JSON.stringify(updates.metadata || {})); }
106
+ if (updates.content !== undefined) { setClauses.push("content = ?"); values.push(updates.content || null); }
107
+ if (updates.subSkills !== undefined) { setClauses.push("sub_skills = ?"); values.push(JSON.stringify(updates.subSkills || [])); }
108
+
109
+ if (setClauses.length === 0) return existing;
110
+
111
+ const now = nowISO();
112
+ setClauses.push("updated_at = ?");
113
+ values.push(now);
114
+ values.push(tenantId, id);
115
+
116
+ this.db.prepare(
117
+ `UPDATE lt_skills SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ?`,
118
+ ).run(...values);
119
+
120
+ return this.getSkillById(tenantId, id);
121
+ }
122
+
123
+ async deleteSkill(tenantId: string, id: string, _context?: SkillStoreContext): Promise<boolean> {
124
+ const result = this.db.prepare(
125
+ `DELETE FROM lt_skills WHERE tenant_id = ? AND id = ?`,
126
+ ).run(tenantId, id);
127
+ return result.changes > 0;
128
+ }
129
+
130
+ async hasSkill(tenantId: string, id: string, _context?: SkillStoreContext): Promise<boolean> {
131
+ const row = this.db.prepare(
132
+ `SELECT 1 FROM lt_skills WHERE tenant_id = ? AND id = ? LIMIT 1`,
133
+ ).get(tenantId, id);
134
+ return row !== undefined;
135
+ }
136
+
137
+ async searchByMetadata(
138
+ tenantId: string, metadataKey: string, metadataValue: string, _context?: SkillStoreContext,
139
+ ): Promise<Skill[]> {
140
+ // SQLite doesn't have JSONB operators, so filter in JS
141
+ const all = await this.getAllSkills(tenantId);
142
+ return all.filter((s) => s.metadata?.[metadataKey] === metadataValue);
143
+ }
144
+
145
+ async filterByCompatibility(tenantId: string, compatibility: string, _context?: SkillStoreContext): Promise<Skill[]> {
146
+ const rows = this.db.prepare(
147
+ `SELECT * FROM lt_skills WHERE tenant_id = ? AND compatibility = ? ORDER BY created_at DESC`,
148
+ ).all(tenantId, compatibility) as unknown as SkillRow[];
149
+ return rows.map(mapRowToSkill);
150
+ }
151
+
152
+ async filterByLicense(tenantId: string, license: string, _context?: SkillStoreContext): Promise<Skill[]> {
153
+ const rows = this.db.prepare(
154
+ `SELECT * FROM lt_skills WHERE tenant_id = ? AND license = ? ORDER BY created_at DESC`,
155
+ ).all(tenantId, license) as unknown as SkillRow[];
156
+ return rows.map(mapRowToSkill);
157
+ }
158
+
159
+ async getSubSkills(tenantId: string, parentSkillName: string, _context?: SkillStoreContext): Promise<Skill[]> {
160
+ // SQLite doesn't have @> for JSON arrays, so filter in JS
161
+ const all = await this.getAllSkills(tenantId);
162
+ return all.filter((s) => s.subSkills?.includes(parentSkillName));
163
+ }
164
+
165
+ async listSkillResources?(_tenantId: string, _id: string, _context?: SkillStoreContext): Promise<string[]> {
166
+ return [];
167
+ }
168
+
169
+ async loadSkillResource?(_tenantId: string, _id: string, _resourcePath: string, _context?: SkillStoreContext): Promise<string | null> {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ function mapRowToSkill(row: SkillRow): Skill {
175
+ return {
176
+ id: row.id,
177
+ tenantId: row.tenant_id,
178
+ name: row.name,
179
+ description: row.description,
180
+ license: row.license || undefined,
181
+ compatibility: row.compatibility || undefined,
182
+ metadata: JSON.parse(row.metadata || "{}"),
183
+ content: row.content || undefined,
184
+ subSkills: JSON.parse(row.sub_skills || "[]"),
185
+ createdAt: parseISO(row.created_at),
186
+ updatedAt: parseISO(row.updated_at),
187
+ };
188
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Local SQLite implementation of TenantStore.
3
+ */
4
+
5
+ import { DatabaseWrapper } from "../database";
6
+ import type {
7
+ TenantStore,
8
+ Tenant,
9
+ TenantStatus,
10
+ CreateTenantRequest,
11
+ UpdateTenantRequest,
12
+ } from "@axiom-lattice/protocols";
13
+ import { ensureTable, nowISO, parseISO } from "../database";
14
+
15
+ const DDL = `
16
+ CREATE TABLE IF NOT EXISTS lt_tenants (
17
+ id TEXT PRIMARY KEY,
18
+ name TEXT NOT NULL,
19
+ description TEXT,
20
+ status TEXT NOT NULL DEFAULT 'active',
21
+ metadata TEXT DEFAULT '{}',
22
+ created_at TEXT NOT NULL,
23
+ updated_at TEXT NOT NULL
24
+ );
25
+ `;
26
+
27
+ interface TenantRow {
28
+ id: string;
29
+ name: string;
30
+ description: string | null;
31
+ status: string;
32
+ metadata: string;
33
+ created_at: string;
34
+ updated_at: string;
35
+ }
36
+
37
+ export class LocalTenantStore implements TenantStore {
38
+ private db: DatabaseWrapper;
39
+
40
+ constructor(db: DatabaseWrapper) {
41
+ this.db = db;
42
+ ensureTable(db, DDL);
43
+ }
44
+
45
+ async getAllTenants(): Promise<Tenant[]> {
46
+ const rows = this.db.prepare(
47
+ `SELECT * FROM lt_tenants ORDER BY created_at DESC`,
48
+ ).all() as unknown as TenantRow[];
49
+ return rows.map(mapRowToTenant);
50
+ }
51
+
52
+ async getTenantById(id: string): Promise<Tenant | null> {
53
+ const row = this.db.prepare(
54
+ `SELECT * FROM lt_tenants WHERE id = ?`,
55
+ ).get(id) as unknown as TenantRow | undefined;
56
+ return row ? mapRowToTenant(row) : null;
57
+ }
58
+
59
+ async createTenant(id: string, data: CreateTenantRequest): Promise<Tenant> {
60
+ const now = nowISO();
61
+ const status = data.status || "active";
62
+ const metadata = JSON.stringify(data.metadata || {});
63
+
64
+ this.db.prepare(
65
+ `INSERT INTO lt_tenants (id, name, description, status, metadata, created_at, updated_at)
66
+ VALUES (?, ?, ?, ?, ?, ?, ?)
67
+ ON CONFLICT(id) DO UPDATE SET
68
+ name = excluded.name,
69
+ description = excluded.description,
70
+ status = excluded.status,
71
+ metadata = excluded.metadata,
72
+ updated_at = excluded.updated_at`,
73
+ ).run(id, data.name, data.description || null, status, metadata, now, now);
74
+
75
+ return {
76
+ id,
77
+ name: data.name,
78
+ description: data.description,
79
+ status: status as TenantStatus,
80
+ metadata: data.metadata,
81
+ createdAt: parseISO(now),
82
+ updatedAt: parseISO(now),
83
+ };
84
+ }
85
+
86
+ async updateTenant(id: string, updates: UpdateTenantRequest): Promise<Tenant | null> {
87
+ const existing = await this.getTenantById(id);
88
+ if (!existing) return null;
89
+
90
+ const setClauses: string[] = [];
91
+ const values: unknown[] = [];
92
+
93
+ if (updates.name !== undefined) { setClauses.push("name = ?"); values.push(updates.name); }
94
+ if (updates.description !== undefined) { setClauses.push("description = ?"); values.push(updates.description || null); }
95
+ if (updates.status !== undefined) { setClauses.push("status = ?"); values.push(updates.status); }
96
+ if (updates.metadata !== undefined) { setClauses.push("metadata = ?"); values.push(JSON.stringify(updates.metadata)); }
97
+
98
+ if (setClauses.length === 0) return existing;
99
+
100
+ const now = nowISO();
101
+ setClauses.push("updated_at = ?");
102
+ values.push(now);
103
+ values.push(id);
104
+
105
+ this.db.prepare(
106
+ `UPDATE lt_tenants SET ${setClauses.join(", ")} WHERE id = ?`,
107
+ ).run(...values);
108
+
109
+ return this.getTenantById(id);
110
+ }
111
+
112
+ async deleteTenant(id: string): Promise<boolean> {
113
+ const result = this.db.prepare(`DELETE FROM lt_tenants WHERE id = ?`).run(id);
114
+ return result.changes > 0;
115
+ }
116
+ }
117
+
118
+ function mapRowToTenant(row: TenantRow): Tenant {
119
+ return {
120
+ id: row.id,
121
+ name: row.name,
122
+ description: row.description || undefined,
123
+ status: row.status as TenantStatus,
124
+ metadata: JSON.parse(row.metadata || "{}"),
125
+ createdAt: parseISO(row.created_at),
126
+ updatedAt: parseISO(row.updated_at),
127
+ };
128
+ }