@jini-ai/server 0.0.0-stage → 0.5.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,328 @@
1
+ /** @internal Column alias list used in every SELECT against the projects table. */
2
+ const PROJECT_COLS = `id, name,
3
+ pending_prompt AS pendingPrompt,
4
+ metadata_json AS metadataJson,
5
+ custom_instructions AS customInstructions,
6
+ created_at AS createdAt,
7
+ updated_at AS updatedAt`;
8
+ /**
9
+ * Returns all projects ordered by most-recently updated first.
10
+ * @param db - Open SQLite connection.
11
+ */
12
+ export function listProjects({ db }) {
13
+ const rows = db
14
+ .prepare(`SELECT ${PROJECT_COLS}
15
+ FROM projects
16
+ ORDER BY updated_at DESC`)
17
+ .all();
18
+ return rows.map(normalizeProject);
19
+ }
20
+ /**
21
+ * Returns the most recent run status for each project, keyed by projectId.
22
+ * Scans messages with a run_status column and collapses to one entry per project,
23
+ * so callers can render project-level status badges without iterating conversations.
24
+ * @param db - Open SQLite connection.
25
+ */
26
+ export function listLatestProjectRunStatuses({ db }) {
27
+ const rows = db
28
+ .prepare(`SELECT c.project_id AS projectId,
29
+ m.run_id AS runId,
30
+ m.run_status AS status,
31
+ COALESCE(m.ended_at, m.started_at, m.created_at) AS updatedAt
32
+ FROM messages m
33
+ JOIN conversations c ON c.id = m.conversation_id
34
+ WHERE m.run_status IS NOT NULL
35
+ ORDER BY updatedAt DESC`)
36
+ .all();
37
+ const latestByProject = new Map();
38
+ for (const row of rows) {
39
+ if (!latestByProject.has(row.projectId)) {
40
+ latestByProject.set(row.projectId, {
41
+ value: normalizeProjectRunStatus(row.status),
42
+ updatedAt: Number(row.updatedAt),
43
+ runId: row.runId ?? undefined,
44
+ });
45
+ }
46
+ }
47
+ return latestByProject;
48
+ }
49
+ /**
50
+ * Returns the most recent run status for each conversation, keyed by conversationId.
51
+ * Used to display per-conversation activity state in a project sidebar.
52
+ * @param db - Open SQLite connection.
53
+ */
54
+ export function listLatestConversationRunStatuses({ db }) {
55
+ const rows = db
56
+ .prepare(`SELECT m.conversation_id AS conversationId,
57
+ m.run_id AS runId,
58
+ m.run_status AS status,
59
+ COALESCE(m.ended_at, m.started_at, m.created_at) AS updatedAt,
60
+ m.position AS position
61
+ FROM messages m
62
+ WHERE m.run_status IS NOT NULL
63
+ ORDER BY updatedAt DESC, m.position DESC`)
64
+ .all();
65
+ const latestByConversation = new Map();
66
+ for (const row of rows) {
67
+ if (!latestByConversation.has(row.conversationId)) {
68
+ latestByConversation.set(row.conversationId, {
69
+ value: normalizeProjectRunStatus(row.status),
70
+ updatedAt: Number(row.updatedAt),
71
+ runId: row.runId ?? undefined,
72
+ });
73
+ }
74
+ }
75
+ return latestByConversation;
76
+ }
77
+ /**
78
+ * Returns the earliest run status for each conversation, keyed by conversationId.
79
+ * Useful for determining when a conversation's first agent turn began.
80
+ * @param db - Open SQLite connection.
81
+ */
82
+ export function listFirstConversationRunStatuses({ db }) {
83
+ const rows = db
84
+ .prepare(`SELECT m.conversation_id AS conversationId,
85
+ m.run_id AS runId,
86
+ m.run_status AS status,
87
+ COALESCE(m.ended_at, m.started_at, m.created_at) AS updatedAt,
88
+ m.position AS position
89
+ FROM messages m
90
+ WHERE m.run_status IS NOT NULL
91
+ AND m.run_id IS NOT NULL
92
+ ORDER BY m.position ASC`)
93
+ .all();
94
+ const firstByConversation = new Map();
95
+ for (const row of rows) {
96
+ if (!firstByConversation.has(row.conversationId)) {
97
+ firstByConversation.set(row.conversationId, {
98
+ value: normalizeProjectRunStatus(row.status),
99
+ updatedAt: Number(row.updatedAt),
100
+ runId: row.runId ?? undefined,
101
+ });
102
+ }
103
+ }
104
+ return firstByConversation;
105
+ }
106
+ /**
107
+ * Returns the most recent run status for each individual run, keyed by runId.
108
+ * Powers per-run status indicators in a conversation thread.
109
+ * @param db - Open SQLite connection.
110
+ */
111
+ export function listLatestRunStatuses({ db }) {
112
+ const rows = db
113
+ .prepare(`SELECT m.run_id AS runId,
114
+ m.run_status AS status,
115
+ COALESCE(m.ended_at, m.started_at, m.created_at) AS updatedAt,
116
+ m.position AS position
117
+ FROM messages m
118
+ WHERE m.run_status IS NOT NULL
119
+ AND m.run_id IS NOT NULL
120
+ ORDER BY updatedAt DESC, m.position DESC`)
121
+ .all();
122
+ const latestByRun = new Map();
123
+ for (const row of rows) {
124
+ if (!latestByRun.has(row.runId)) {
125
+ latestByRun.set(row.runId, {
126
+ value: normalizeProjectRunStatus(row.status),
127
+ updatedAt: Number(row.updatedAt),
128
+ runId: row.runId ?? undefined,
129
+ });
130
+ }
131
+ }
132
+ return latestByRun;
133
+ }
134
+ /**
135
+ * Returns the set of project IDs whose latest form-bearing assistant message contains a
136
+ * `<question-form>` or `<ask-question>` artifact and has not yet received a user reply.
137
+ * Form filtering deliberately precedes ranking: a newer plain assistant turn does not consume
138
+ * an outstanding question. Keep timestamp/position ordering for replies sharing a timestamp.
139
+ * This is the data source for an "awaiting input" badge on a project list.
140
+ * @param db - Open SQLite connection.
141
+ */
142
+ export function listProjectsAwaitingInput({ db }) {
143
+ const rows = db
144
+ .prepare(`SELECT latest.projectId
145
+ FROM (
146
+ SELECT c.project_id AS projectId,
147
+ m.conversation_id AS conversationId,
148
+ m.created_at AS createdAt,
149
+ m.position AS position,
150
+ ROW_NUMBER() OVER (
151
+ PARTITION BY c.project_id
152
+ ORDER BY m.created_at DESC, m.position DESC
153
+ ) AS rowNum
154
+ FROM messages m
155
+ JOIN conversations c ON c.id = m.conversation_id
156
+ WHERE m.role = 'assistant'
157
+ -- ask-question is an accepted alias for question-form (host UI parser
158
+ -- + agent open-tag matcher), so an alias-form turn must also count as
159
+ -- awaiting input.
160
+ AND (
161
+ LOWER(m.content) LIKE '%<question-form%'
162
+ OR LOWER(m.content) LIKE '%<ask-question%'
163
+ )
164
+ ) latest
165
+ WHERE latest.rowNum = 1
166
+ AND NOT EXISTS (
167
+ SELECT 1
168
+ FROM messages reply
169
+ WHERE reply.conversation_id = latest.conversationId
170
+ AND reply.role = 'user'
171
+ AND (
172
+ reply.created_at > latest.createdAt
173
+ OR (reply.created_at = latest.createdAt AND reply.position > latest.position)
174
+ )
175
+ )`)
176
+ .all();
177
+ return new Set(rows.map((row) => row.projectId));
178
+ }
179
+ /**
180
+ * Returns the set of conversation IDs whose latest form-bearing assistant message contains a
181
+ * `<question-form>` or `<ask-question>` artifact and has not yet received a user reply.
182
+ * As in the project query, a newer plain assistant turn leaves an unanswered form outstanding.
183
+ * @param db - Open SQLite connection.
184
+ */
185
+ export function listConversationsAwaitingInput({ db }) {
186
+ const rows = db
187
+ .prepare(`SELECT latest.conversationId
188
+ FROM (
189
+ SELECT m.conversation_id AS conversationId,
190
+ m.created_at AS createdAt,
191
+ m.position AS position,
192
+ ROW_NUMBER() OVER (
193
+ PARTITION BY m.conversation_id
194
+ ORDER BY m.created_at DESC, m.position DESC
195
+ ) AS rowNum
196
+ FROM messages m
197
+ WHERE m.role = 'assistant'
198
+ AND (
199
+ LOWER(m.content) LIKE '%<question-form%'
200
+ OR LOWER(m.content) LIKE '%<ask-question%'
201
+ )
202
+ ) latest
203
+ WHERE latest.rowNum = 1
204
+ AND NOT EXISTS (
205
+ SELECT 1
206
+ FROM messages reply
207
+ WHERE reply.conversation_id = latest.conversationId
208
+ AND reply.role = 'user'
209
+ AND (
210
+ reply.created_at > latest.createdAt
211
+ OR (reply.created_at = latest.createdAt AND reply.position > latest.position)
212
+ )
213
+ )`)
214
+ .all();
215
+ return new Set(rows.map((row) => row.conversationId));
216
+ }
217
+ /**
218
+ * Fetches a single project by id, or returns null if it does not exist.
219
+ * @param db - Open SQLite connection.
220
+ * @param id - Project id.
221
+ */
222
+ export function getProject({ db, id }) {
223
+ const row = db
224
+ .prepare(`SELECT ${PROJECT_COLS} FROM projects WHERE id = ?`)
225
+ .get(id);
226
+ return row ? normalizeProject(row) : null;
227
+ }
228
+ /**
229
+ * Inserts a new project row and returns the freshly read, normalized record.
230
+ * Metadata is serialized to JSON; all nullable fields default to null on omission.
231
+ * @param db - Open SQLite connection.
232
+ * @param project - Project fields to insert.
233
+ */
234
+ export function insertProject({ db, project: p }) {
235
+ db.prepare(`INSERT INTO projects
236
+ (id, name, pending_prompt, metadata_json, custom_instructions, created_at, updated_at)
237
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(p.id, p.name, p.pendingPrompt ?? null, p.metadata ? JSON.stringify(p.metadata) : null, p.customInstructions ?? null, p.createdAt, p.updatedAt);
238
+ return getProject({ db, id: p.id });
239
+ }
240
+ /**
241
+ * Applies a partial patch to an existing project, preserving all unpatched fields.
242
+ * Returns null when the project does not exist; otherwise returns the updated record.
243
+ * updatedAt defaults to Date.now() when the patch omits it.
244
+ * @param db - Open SQLite connection.
245
+ * @param id - Project id to update.
246
+ * @param patch - Fields to overwrite.
247
+ * @param optional.clock - Host clock port; defaults to Date.now() as before. An explicit updatedAt
248
+ * wins without reading the clock, so replayed/imported timestamps remain stable.
249
+ */
250
+ export function updateProject({ db, id, patch }, optional = {}) {
251
+ const existing = getProject({ db, id });
252
+ if (!existing)
253
+ return null;
254
+ const merged = {
255
+ ...existing,
256
+ ...patch,
257
+ updatedAt: typeof patch.updatedAt === 'number' ? patch.updatedAt : (optional.clock ?? Date).now(),
258
+ };
259
+ db.prepare(`UPDATE projects
260
+ SET name = ?,
261
+ pending_prompt = ?,
262
+ metadata_json = ?,
263
+ custom_instructions = ?,
264
+ updated_at = ?
265
+ WHERE id = ?`).run(merged.name, merged.pendingPrompt ?? null, merged.metadata ? JSON.stringify(merged.metadata) : null, merged.customInstructions ?? null, merged.updatedAt, id);
266
+ return getProject({ db, id });
267
+ }
268
+ /**
269
+ * Permanently removes a project row. Cascading deletes for related rows are
270
+ * handled at the SQLite schema level, not here.
271
+ * @param db - Open SQLite connection.
272
+ * @param id - Project id to delete.
273
+ */
274
+ export function deleteProject({ db, id }) {
275
+ db.prepare(`DELETE FROM projects WHERE id = ?`).run(id);
276
+ }
277
+ /** @internal Converts a raw SQLite row into the typed project shape, parsing
278
+ * metadataJson and casting numeric timestamps. */
279
+ function normalizeProject(row) {
280
+ let metadata;
281
+ if (row.metadataJson) {
282
+ try {
283
+ metadata = JSON.parse(row.metadataJson);
284
+ }
285
+ catch {
286
+ metadata = undefined;
287
+ }
288
+ }
289
+ return {
290
+ id: row.id,
291
+ name: row.name,
292
+ pendingPrompt: row.pendingPrompt ?? undefined,
293
+ metadata,
294
+ customInstructions: row.customInstructions ?? undefined,
295
+ createdAt: Number(row.createdAt),
296
+ updatedAt: Number(row.updatedAt),
297
+ };
298
+ }
299
+ /** @internal Maps raw DB run_status strings to the canonical client-facing enum,
300
+ * collapsing 'starting' → 'running' and 'cancelled' → 'canceled', and defaulting
301
+ * unknown values to 'not_started'. */
302
+ function normalizeProjectRunStatus(status) {
303
+ if (status === 'starting')
304
+ return 'running';
305
+ if (status === 'cancelled')
306
+ return 'canceled';
307
+ if (status === 'queued' ||
308
+ status === 'running' ||
309
+ status === 'succeeded' ||
310
+ status === 'failed' ||
311
+ status === 'canceled') {
312
+ return status;
313
+ }
314
+ return 'not_started';
315
+ }
316
+ /** Unchanged local host-project schema; no automatic migration at import. */
317
+ export const LEGACY_PROJECTS_DDL = ` CREATE TABLE IF NOT EXISTS projects (
318
+ id TEXT PRIMARY KEY,
319
+ name TEXT NOT NULL,
320
+ pending_prompt TEXT,
321
+ metadata_json TEXT,
322
+ custom_instructions TEXT,
323
+ created_at INTEGER NOT NULL,
324
+ updated_at INTEGER NOT NULL
325
+ );
326
+
327
+ `;
328
+ //# sourceMappingURL=sqlite.js.map
package/package.json CHANGED
@@ -1,6 +1,127 @@
1
1
  {
2
2
  "name": "@jini-ai/server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.5.1",
4
+ "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/AINSEP/Jini.git",
8
+ "directory": "packages/node-host"
9
+ },
10
+ "jini": {
11
+ "domain": "server",
12
+ "kind": "host",
13
+ "runtime": "node",
14
+ "admission": "locked",
15
+ "isolation": {
16
+ "entries": {
17
+ "./store/projects/sqlite": "storage"
18
+ }
19
+ }
20
+ },
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./storage": {
31
+ "types": "./dist/storage/index.d.ts",
32
+ "import": "./dist/storage/index.js",
33
+ "default": "./dist/storage/index.js"
34
+ },
35
+ "./storage/legacy/sqlite": {
36
+ "types": "./dist/storage/legacy/sqlite.d.ts",
37
+ "import": "./dist/storage/legacy/sqlite.js",
38
+ "default": "./dist/storage/legacy/sqlite.js"
39
+ },
40
+ "./store/projects/sqlite": {
41
+ "types": "./dist/store/projects/sqlite.d.ts",
42
+ "import": "./dist/store/projects/sqlite.js",
43
+ "default": "./dist/store/projects/sqlite.js"
44
+ }
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "CHANGELOG.md",
50
+ "LICENSE",
51
+ "!dist/**/*.test.js",
52
+ "!dist/**/*.test.d.ts",
53
+ "!dist/**/*.test.js.map",
54
+ "!dist/**/*.test.d.ts.map",
55
+ "!dist/**/__tests__/**",
56
+ "!dist/**/*.typecheck.*"
57
+ ],
58
+ "publishConfig": {
59
+ "access": "public",
60
+ "registry": "https://registry.npmjs.org"
61
+ },
62
+ "devDependencies": {
63
+ "@types/better-sqlite3": "^7.6.11",
64
+ "@types/express": "^4.17.21",
65
+ "@vitest/coverage-v8": "^2.1.9",
66
+ "better-sqlite3": "^13.0.0",
67
+ "express": "^4.21.0",
68
+ "@jini-ai/core": "0.4.2",
69
+ "@jini-ai/http-kit": "0.4.1",
70
+ "@jini-ai/daemon": "0.5.4",
71
+ "@jini-ai/agent-runtime": "0.4.2",
72
+ "@jini-ai/sidecar": "0.4.1",
73
+ "@jini-ai/db": "0.3.1",
74
+ "@jini-ai/chat": "0.5.5",
75
+ "@jini-ai/registry": "0.5.2"
76
+ },
77
+ "dependencies": {
78
+ "@jini-ai/core": "^0.4.2"
79
+ },
80
+ "peerDependencies": {
81
+ "better-sqlite3": "^13.0.0",
82
+ "@jini-ai/agent-runtime": "^0.4.1",
83
+ "@jini-ai/daemon": "^0.5.4",
84
+ "@jini-ai/http-kit": "^0.4.0",
85
+ "@jini-ai/sidecar": "^0.4.0",
86
+ "express": "^4.21.0",
87
+ "@jini-ai/db": "^0.3.0",
88
+ "@jini-ai/chat": "^0.5.4",
89
+ "@jini-ai/registry": "^0.5.1"
90
+ },
91
+ "peerDependenciesMeta": {
92
+ "better-sqlite3": {
93
+ "optional": true
94
+ },
95
+ "@jini-ai/agent-runtime": {
96
+ "optional": true
97
+ },
98
+ "@jini-ai/daemon": {
99
+ "optional": true
100
+ },
101
+ "@jini-ai/http-kit": {
102
+ "optional": true
103
+ },
104
+ "@jini-ai/sidecar": {
105
+ "optional": true
106
+ },
107
+ "express": {
108
+ "optional": true
109
+ },
110
+ "@jini-ai/db": {
111
+ "optional": true
112
+ },
113
+ "@jini-ai/chat": {
114
+ "optional": true
115
+ },
116
+ "@jini-ai/registry": {
117
+ "optional": true
118
+ }
119
+ },
120
+ "sideEffects": false,
121
+ "scripts": {
122
+ "build": "tsc -p tsconfig.tests.json && tsc -p tsconfig.json",
123
+ "typecheck": "tsc -p tsconfig.tests.json && tsc -p tsconfig.json --noEmit",
124
+ "test": "vitest run",
125
+ "test:coverage": "vitest run --coverage"
126
+ }
6
127
  }