@saluzi/saluzi-edu 0.2.26 → 0.2.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saluzi/saluzi-edu",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "description": "Saluzi CLI - interactive AI coding assistant in the terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,121 @@
1
+ -- RCS Authentication & Session Schema
2
+ -- Phase 1: users, session_tokens, invitations
3
+ -- Phase 2: teams, team_members, team_environments, session_owners, session_shares, sessions
4
+
5
+ CREATE TABLE IF NOT EXISTS users (
6
+ id TEXT NOT NULL PRIMARY KEY,
7
+ username TEXT UNIQUE NOT NULL,
8
+ password_hash TEXT NOT NULL,
9
+ role TEXT NOT NULL CHECK (role IN ('admin', 'member', 'guest')),
10
+ created_at TEXT NOT NULL,
11
+ last_login_at TEXT
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS session_tokens (
15
+ token_hash TEXT NOT NULL PRIMARY KEY,
16
+ user_id TEXT NOT NULL REFERENCES users(id),
17
+ kind TEXT NOT NULL CHECK (kind IN ('access', 'refresh')),
18
+ expires_at TEXT NOT NULL,
19
+ revoked_at TEXT,
20
+ created_at TEXT NOT NULL
21
+ );
22
+
23
+ CREATE TABLE IF NOT EXISTS invitations (
24
+ token_hash TEXT NOT NULL PRIMARY KEY,
25
+ team_id TEXT,
26
+ role TEXT NOT NULL,
27
+ expires_at TEXT NOT NULL,
28
+ max_uses INTEGER DEFAULT 1,
29
+ uses INTEGER DEFAULT 0,
30
+ created_by TEXT NOT NULL REFERENCES users(id),
31
+ consumed_at TEXT,
32
+ consumed_by TEXT REFERENCES users(id)
33
+ );
34
+
35
+ -- Phase 2 tables
36
+
37
+ CREATE TABLE IF NOT EXISTS teams (
38
+ id TEXT PRIMARY KEY,
39
+ name TEXT NOT NULL,
40
+ slug TEXT UNIQUE NOT NULL,
41
+ description TEXT,
42
+ created_by TEXT NOT NULL REFERENCES users(id),
43
+ created_at TEXT NOT NULL,
44
+ updated_at TEXT NOT NULL,
45
+ deleted_at TEXT
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS team_members (
49
+ team_id TEXT NOT NULL REFERENCES teams(id),
50
+ user_id TEXT NOT NULL REFERENCES users(id),
51
+ role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member')),
52
+ added_at TEXT NOT NULL,
53
+ added_by TEXT NOT NULL REFERENCES users(id),
54
+ PRIMARY KEY (team_id, user_id)
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS team_environments (
58
+ team_id TEXT NOT NULL REFERENCES teams(id),
59
+ environment_id TEXT NOT NULL,
60
+ PRIMARY KEY (team_id, environment_id)
61
+ );
62
+
63
+ CREATE TABLE IF NOT EXISTS session_owners (
64
+ session_id TEXT NOT NULL,
65
+ owner_type TEXT NOT NULL CHECK (owner_type IN ('user', 'team')),
66
+ owner_id TEXT NOT NULL,
67
+ PRIMARY KEY (session_id, owner_type, owner_id)
68
+ );
69
+
70
+ CREATE TABLE IF NOT EXISTS session_shares (
71
+ id TEXT PRIMARY KEY,
72
+ session_id TEXT NOT NULL,
73
+ granted_to_user TEXT REFERENCES users(id),
74
+ granted_to_team TEXT REFERENCES teams(id),
75
+ permission TEXT NOT NULL CHECK (permission IN ('read', 'write')),
76
+ expires_at TEXT,
77
+ granted_by TEXT NOT NULL REFERENCES users(id),
78
+ granted_at TEXT NOT NULL
79
+ );
80
+
81
+ CREATE TABLE IF NOT EXISTS environments (
82
+ id TEXT PRIMARY KEY,
83
+ secret_hash TEXT NOT NULL,
84
+ team_id TEXT,
85
+ registered_by TEXT,
86
+ machine_name TEXT,
87
+ worker_type TEXT NOT NULL DEFAULT 'claude_code',
88
+ status TEXT NOT NULL DEFAULT 'active',
89
+ last_poll_at TEXT,
90
+ created_at TEXT NOT NULL,
91
+ owner_user_id TEXT,
92
+ claim_token TEXT,
93
+ claim_expires_at TEXT
94
+ );
95
+
96
+ CREATE TABLE IF NOT EXISTS sessions (
97
+ id TEXT PRIMARY KEY,
98
+ environment_id TEXT,
99
+ title TEXT,
100
+ status TEXT NOT NULL DEFAULT 'idle',
101
+ source TEXT NOT NULL DEFAULT 'remote-control',
102
+ visibility TEXT NOT NULL DEFAULT 'private',
103
+ permission_mode TEXT,
104
+ created_at TEXT NOT NULL,
105
+ updated_at TEXT NOT NULL
106
+ );
107
+
108
+ -- Performance indexes
109
+ CREATE INDEX IF NOT EXISTS idx_session_owners_owner ON session_owners(owner_type, owner_id);
110
+ CREATE INDEX IF NOT EXISTS idx_session_owners_session ON session_owners(session_id);
111
+ CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id);
112
+ CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id);
113
+ CREATE INDEX IF NOT EXISTS idx_session_shares_user ON session_shares(granted_to_user);
114
+ CREATE INDEX IF NOT EXISTS idx_session_shares_team ON session_shares(granted_to_team);
115
+ CREATE INDEX IF NOT EXISTS idx_session_shares_session ON session_shares(session_id);
116
+ CREATE INDEX IF NOT EXISTS idx_sessions_visibility ON sessions(visibility);
117
+ CREATE INDEX IF NOT EXISTS idx_sessions_environment ON sessions(environment_id);
118
+ CREATE INDEX IF NOT EXISTS idx_session_tokens_user ON session_tokens(user_id);
119
+ CREATE INDEX IF NOT EXISTS idx_environments_team ON environments(team_id);
120
+ CREATE INDEX IF NOT EXISTS idx_environments_owner_user ON environments(owner_user_id);
121
+ CREATE INDEX IF NOT EXISTS idx_environments_claim_token ON environments(claim_token);
@@ -42,9 +42,33 @@ const ResolvedDatabase: DatabaseCtor = (() => {
42
42
  }
43
43
 
44
44
  // Tier 3: libsql (optionalDependency — covers Node 20/21)
45
+ // libsql 0.5.x exports the Database constructor directly (better-sqlite3
46
+ // compatible API): `require('libsql')` returns a function, not an object
47
+ // with a `.Database` property. Check `typeof === 'function'` and call
48
+ // `new libsql(path)` directly.
49
+ //
50
+ // Parameter format compatibility: bun:sqlite and node:sqlite both accept
51
+ // `$name` named params, but libsql (better-sqlite3 lineage) only accepts
52
+ // `:name` and `@name` — `$name` silently fails to bind, causing
53
+ // "NOT NULL constraint failed" errors. Convert `$name` → `:name` in SQL
54
+ // and `{$name: v}` → `{name: v}` in param objects so RCS queries work
55
+ // unchanged across all three backends.
45
56
  try {
46
57
  const libsql = require('libsql')
47
- if (libsql?.Database) {
58
+ if (typeof libsql === 'function') {
59
+ const convertSql = (sql: string): string => sql.replace(/\$(\w+)/g, ':$1')
60
+ const convertParams = (params: unknown[]): unknown[] =>
61
+ params.map(p => {
62
+ if (p && typeof p === 'object' && !Array.isArray(p)) {
63
+ const obj = p as Record<string, unknown>
64
+ const out: Record<string, unknown> = {}
65
+ for (const k of Object.keys(obj)) {
66
+ out[k.startsWith('$') ? k.slice(1) : k] = obj[k]
67
+ }
68
+ return out
69
+ }
70
+ return p
71
+ })
48
72
  return class LibsqlCompat {
49
73
  private _db: {
50
74
  prepare(sql: string): {
@@ -56,17 +80,17 @@ const ResolvedDatabase: DatabaseCtor = (() => {
56
80
  close(): void
57
81
  }
58
82
  constructor(path: string) {
59
- this._db = new libsql.Database(path)
83
+ this._db = new libsql(path)
60
84
  }
61
85
  exec(sql: string): void {
62
86
  this._db.exec(sql)
63
87
  }
64
88
  query(sql: string) {
65
- const stmt = this._db.prepare(sql)
89
+ const stmt = this._db.prepare(convertSql(sql))
66
90
  return {
67
- get: (...params: unknown[]) => stmt.get(...params),
68
- all: (...params: unknown[]) => stmt.all(...params),
69
- run: (...params: unknown[]) => stmt.run(...params),
91
+ get: (...params: unknown[]) => stmt.get(...convertParams(params)),
92
+ all: (...params: unknown[]) => stmt.all(...convertParams(params)),
93
+ run: (...params: unknown[]) => stmt.run(...convertParams(params)),
70
94
  }
71
95
  }
72
96
  close(): void {
@@ -92,15 +116,20 @@ let _dbPath: string | null = null
92
116
  * Idempotent — uses CREATE TABLE IF NOT EXISTS for new tables, plus
93
117
  * ALTER TABLE ADD COLUMN for columns added to existing tables (gated by
94
118
  * PRAGMA table_info so re-running on an already-migrated DB is a no-op).
95
- * Reads schema.sql and executes as a single db.exec() call,
119
+ * Reads rcs-schema.sql and executes as a single db.exec() call,
96
120
  * which correctly handles semicolons inside CHECK constraints.
121
+ *
122
+ * File is named rcs-schema.sql (not schema.sql) to avoid collision with
123
+ * CodeGraph's schema.sql at the same dist/ path — both modules resolve
124
+ * their schema via __dirname, and the bundler places them in the same
125
+ * chunk directory.
97
126
  */
98
127
  export function migrateDatabase(db: Database): void {
99
- const schemaPath = resolve(__dirname, 'schema.sql')
128
+ const schemaPath = resolve(__dirname, 'rcs-schema.sql')
100
129
  const schema = readFileSync(schemaPath, 'utf8')
101
130
 
102
131
  db.exec('PRAGMA foreign_keys = ON')
103
- // Run ALTER TABLE migrations BEFORE db.exec(schema) — schema.sql's
132
+ // Run ALTER TABLE migrations BEFORE db.exec(schema) — rcs-schema.sql's
104
133
  // CREATE INDEX statements reference the new columns, so the columns
105
134
  // must exist before the indexes can be created. CREATE TABLE IF NOT
106
135
  // EXISTS skips existing tables (so the ALTER adds the missing columns
@@ -116,7 +145,7 @@ export function migrateDatabase(db: Database): void {
116
145
  * before owner_user_id / claim_token / claim_expires_at existed.
117
146
  * CREATE TABLE IF NOT EXISTS skips existing tables entirely, so without
118
147
  * this the new columns would never appear on upgraded DBs. Must run
119
- * BEFORE db.exec(schema) because schema.sql's CREATE INDEX statements
148
+ * BEFORE db.exec(schema) because rcs-schema.sql's CREATE INDEX statements
120
149
  * reference the new columns. On a fresh DB (table doesn't exist yet),
121
150
  * skip entirely — db.exec(schema) will create the table with all columns.
122
151
  */