@aexol/spectral 0.9.172 → 0.9.174

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.
Files changed (33) hide show
  1. package/dist/commands/serve.d.ts.map +1 -1
  2. package/dist/commands/serve.js +1 -0
  3. package/dist/memory/config.d.ts +2 -0
  4. package/dist/memory/config.d.ts.map +1 -1
  5. package/dist/memory/config.js +25 -0
  6. package/dist/relay/dispatcher.d.ts +7 -2
  7. package/dist/relay/dispatcher.d.ts.map +1 -1
  8. package/dist/relay/dispatcher.js +25 -18
  9. package/dist/server/handlers/agent-settings.d.ts +6 -0
  10. package/dist/server/handlers/agent-settings.d.ts.map +1 -1
  11. package/dist/server/handlers/agent-settings.js +88 -1
  12. package/dist/server/handlers/paths-autocomplete.d.ts +4 -4
  13. package/dist/server/handlers/paths-autocomplete.d.ts.map +1 -1
  14. package/dist/server/handlers/paths-autocomplete.js +10 -10
  15. package/dist/server/handlers/projects.d.ts +6 -0
  16. package/dist/server/handlers/projects.d.ts.map +1 -1
  17. package/dist/server/handlers/projects.js +18 -0
  18. package/dist/server/handlers/sessions.d.ts +12 -0
  19. package/dist/server/handlers/sessions.d.ts.map +1 -1
  20. package/dist/server/handlers/sessions.js +68 -0
  21. package/dist/server/handlers/settings-lock.d.ts +19 -0
  22. package/dist/server/handlers/settings-lock.d.ts.map +1 -1
  23. package/dist/server/handlers/settings-lock.js +65 -1
  24. package/dist/server/handlers/settings.d.ts +4 -0
  25. package/dist/server/handlers/settings.d.ts.map +1 -1
  26. package/dist/server/handlers/settings.js +38 -2
  27. package/dist/server/sqlite-adapter.d.ts +17 -0
  28. package/dist/server/sqlite-adapter.d.ts.map +1 -1
  29. package/dist/server/sqlite-adapter.js +18 -0
  30. package/dist/server/storage.d.ts +44 -0
  31. package/dist/server/storage.d.ts.map +1 -1
  32. package/dist/server/storage.js +166 -8
  33. package/package.json +1 -1
@@ -190,8 +190,16 @@ export class SessionStore {
190
190
  db;
191
191
  closed = false;
192
192
  // Project statements
193
+ stmtListProjectsSQL = `
194
+ SELECT p.id, p.name, p.path, p.created_at, p.updated_at,
195
+ p.studio_project_id, p.studio_project_name, p.studio_team_id,
196
+ (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count
197
+ FROM projects p
198
+ ORDER BY p.updated_at DESC
199
+ `;
193
200
  stmtListProjects;
194
201
  stmtGetProject;
202
+ stmtCountSessionsByProject;
195
203
  stmtCreateProject;
196
204
  stmtUpdateProject;
197
205
  stmtSetProjectStudioBinding;
@@ -375,6 +383,7 @@ export class SessionStore {
375
383
  ORDER BY p.updated_at DESC
376
384
  `);
377
385
  this.stmtGetProject = this.db.prepare(`SELECT id, name, path, created_at, updated_at, studio_project_id, studio_project_name, studio_team_id FROM projects WHERE id = ?`);
386
+ this.stmtCountSessionsByProject = this.db.prepare(`SELECT COUNT(*) AS count FROM sessions WHERE project_id = ?`);
378
387
  this.stmtCreateProject = this.db.prepare(`INSERT INTO projects (id, name, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`);
379
388
  this.stmtUpdateProject = this.db.prepare(`UPDATE projects SET name = ?, path = ?, updated_at = ? WHERE id = ?`);
380
389
  this.stmtSetProjectStudioBinding = this.db.prepare(`UPDATE projects SET studio_project_id = ?, studio_project_name = ?, studio_team_id = ?, updated_at = ? WHERE id = ?`);
@@ -500,11 +509,9 @@ export class SessionStore {
500
509
  .all();
501
510
  return rows.map((r) => r.name);
502
511
  }
503
- // ----------------------------------------------------------------------
504
- // Projects
505
- // ----------------------------------------------------------------------
506
- listProjects() {
507
- return this.stmtListProjects.all().map((r) => {
512
+ /** Shared row→WireProject mapping for the synchronous and async list paths. */
513
+ mapProjectRowsWithCount(rows) {
514
+ return rows.map((r) => {
508
515
  const project = {
509
516
  id: r.id,
510
517
  name: r.name,
@@ -523,6 +530,21 @@ export class SessionStore {
523
530
  return applyBindingFields(project, rowBinding);
524
531
  });
525
532
  }
533
+ // ----------------------------------------------------------------------
534
+ // Projects
535
+ // ----------------------------------------------------------------------
536
+ listProjects() {
537
+ return this.mapProjectRowsWithCount(this.stmtListProjects.all());
538
+ }
539
+ /**
540
+ * Async twin of {@link listProjects}. Uses the adapter's `queryAll`
541
+ * trampoline so the Node adapter yields to the event loop before the
542
+ * potentially-large aggregate query runs. Wire shape is identical.
543
+ */
544
+ async listProjectsAsync() {
545
+ const rows = await this.db.queryAll(this.stmtListProjectsSQL);
546
+ return this.mapProjectRowsWithCount(rows);
547
+ }
526
548
  createProject(input) {
527
549
  const id = input.id ?? randomUUID();
528
550
  const name = input.name.trim() || "Untitled project";
@@ -543,15 +565,40 @@ export class SessionStore {
543
565
  const row = this.stmtGetProject.get(id);
544
566
  if (!row)
545
567
  return null;
546
- // Re-fetch with count to keep the wire shape consistent. Cheap.
547
- const withCount = this.stmtListProjects.all().find((r) => r.id === id);
568
+ const withCount = this.stmtCountSessionsByProject.get(id);
569
+ const project = {
570
+ id: row.id,
571
+ name: row.name,
572
+ path: row.path,
573
+ createdAt: row.created_at,
574
+ updatedAt: row.updated_at,
575
+ sessionCount: withCount?.count ?? 0,
576
+ };
577
+ const rowBinding = row.studio_project_id
578
+ ? {
579
+ projectId: row.studio_project_id,
580
+ ...(row.studio_project_name ? { name: row.studio_project_name } : {}),
581
+ ...(row.studio_team_id ? { teamId: row.studio_team_id } : {}),
582
+ }
583
+ : null;
584
+ return applyBindingFields(project, rowBinding);
585
+ }
586
+ /** Async twin of {@link getProject}. */
587
+ async getProjectAsync(id) {
588
+ const rows = await this.db.queryAll(`SELECT id, name, path, created_at, updated_at,
589
+ studio_project_id, studio_project_name, studio_team_id
590
+ FROM projects WHERE id = ?`, id);
591
+ const row = rows[0];
592
+ if (!row)
593
+ return null;
594
+ const countRows = await this.db.queryAll(`SELECT COUNT(*) AS count FROM sessions WHERE project_id = ?`, id);
548
595
  const project = {
549
596
  id: row.id,
550
597
  name: row.name,
551
598
  path: row.path,
552
599
  createdAt: row.created_at,
553
600
  updatedAt: row.updated_at,
554
- sessionCount: withCount?.session_count ?? 0,
601
+ sessionCount: countRows[0]?.count ?? 0,
555
602
  };
556
603
  const rowBinding = row.studio_project_id
557
604
  ? {
@@ -607,6 +654,22 @@ export class SessionStore {
607
654
  messageCount: r.message_count,
608
655
  }));
609
656
  }
657
+ /** Async twin of {@link listSessionsByProject}. */
658
+ async listSessionsByProjectAsync(projectId) {
659
+ const rows = await this.db.queryAll(`SELECT s.id, s.project_id, s.title, s.created_at, s.updated_at,
660
+ (SELECT COUNT(*) FROM messages m WHERE m.session_id = s.id) AS message_count
661
+ FROM sessions s
662
+ WHERE s.project_id = ?
663
+ ORDER BY s.updated_at DESC`, projectId);
664
+ return rows.map((r) => ({
665
+ id: r.id,
666
+ projectId: r.project_id,
667
+ title: r.title,
668
+ createdAt: r.created_at,
669
+ updatedAt: r.updated_at,
670
+ messageCount: r.message_count,
671
+ }));
672
+ }
610
673
  /**
611
674
  * Batched variant of {@link listSessionsByProject} for many projects at once.
612
675
  *
@@ -654,6 +717,42 @@ export class SessionStore {
654
717
  }
655
718
  return result;
656
719
  }
720
+ /**
721
+ * Async twin of {@link listSessionsByProjectIds}. Runs the same single
722
+ * LEFT JOIN + GROUP BY query through the adapter trampoline so large
723
+ * aggregate reads yield to the event loop. Returns an identical Map with
724
+ * every requested project id present (empty arrays for projects with no
725
+ * sessions) and identical per-project `updated_at DESC` ordering.
726
+ */
727
+ async listSessionsByProjectIdsAsync(projectIds) {
728
+ const result = new Map();
729
+ for (const id of projectIds)
730
+ result.set(id, []);
731
+ if (projectIds.length === 0)
732
+ return result;
733
+ const placeholders = projectIds.map(() => "?").join(", ");
734
+ const rows = await this.db.queryAll(`SELECT s.id, s.project_id, s.title, s.created_at, s.updated_at,
735
+ COUNT(m.id) AS message_count
736
+ FROM sessions s
737
+ LEFT JOIN messages m ON m.session_id = s.id
738
+ WHERE s.project_id IN (${placeholders})
739
+ GROUP BY s.id, s.project_id, s.title, s.created_at, s.updated_at
740
+ ORDER BY s.updated_at DESC, s.created_at DESC, s.id DESC`, ...projectIds);
741
+ for (const r of rows) {
742
+ const list = result.get(r.project_id);
743
+ if (!list)
744
+ continue;
745
+ list.push({
746
+ id: r.id,
747
+ projectId: r.project_id,
748
+ title: r.title,
749
+ createdAt: r.created_at,
750
+ updatedAt: r.updated_at,
751
+ messageCount: r.message_count,
752
+ });
753
+ }
754
+ return result;
755
+ }
657
756
  // ----------------------------------------------------------------------
658
757
  // Sessions
659
758
  // ----------------------------------------------------------------------
@@ -727,6 +826,34 @@ export class SessionStore {
727
826
  updatedAt: row.updated_at,
728
827
  };
729
828
  }
829
+ /** Async twin of {@link getSessionMeta}. */
830
+ async getSessionMetaAsync(id) {
831
+ const rows = await this.db.queryAll(`SELECT id, project_id, title, created_at, updated_at
832
+ FROM sessions WHERE id = ?`, id);
833
+ const row = rows[0];
834
+ if (!row)
835
+ return null;
836
+ return {
837
+ id: row.id,
838
+ projectId: row.project_id,
839
+ title: row.title,
840
+ createdAt: row.created_at,
841
+ updatedAt: row.updated_at,
842
+ };
843
+ }
844
+ /** Async twin of {@link getMessages} with a caller-supplied bound. */
845
+ async getMessagesTailAsync(sessionId, limit) {
846
+ const rows = await this.db.queryAll(`SELECT id, session_id, role, content, events_jsonl, images_json, credits_used, created_at
847
+ FROM (
848
+ SELECT id, session_id, role, content, events_jsonl, images_json, credits_used, created_at
849
+ FROM messages
850
+ WHERE session_id = ?
851
+ ORDER BY created_at DESC, id DESC
852
+ LIMIT ?
853
+ )
854
+ ORDER BY created_at ASC, id ASC`, sessionId, limit);
855
+ return this.mapMessageRows(rows);
856
+ }
730
857
  getMessages(sessionId) {
731
858
  return this.mapMessageRows(this.stmtListMessages.all(sessionId));
732
859
  }
@@ -738,9 +865,30 @@ export class SessionStore {
738
865
  return [];
739
866
  return this.mapMessageRows(this.stmtListMessagesBefore.all(sessionId, opts.beforeCreatedAt, opts.beforeCreatedAt, opts.beforeId ?? "\uffff", opts.limit));
740
867
  }
868
+ /** Async twin of {@link getMessagesBefore}. */
869
+ async getMessagesBeforeAsync(sessionId, opts) {
870
+ if (opts.beforeCreatedAt === null)
871
+ return [];
872
+ const rows = await this.db.queryAll(`SELECT id, session_id, role, content, events_jsonl, images_json, credits_used, created_at
873
+ FROM (
874
+ SELECT id, session_id, role, content, events_jsonl, images_json, credits_used, created_at
875
+ FROM messages
876
+ WHERE session_id = ?
877
+ AND (created_at < ? OR (created_at = ? AND id < ?))
878
+ ORDER BY created_at DESC, id DESC
879
+ LIMIT ?
880
+ )
881
+ ORDER BY created_at ASC, id ASC`, sessionId, opts.beforeCreatedAt, opts.beforeCreatedAt, opts.beforeId ?? "\uffff", opts.limit);
882
+ return this.mapMessageRows(rows);
883
+ }
741
884
  countMessagesBySession(sessionId) {
742
885
  return this.stmtCountMessagesBySession.get(sessionId)?.count ?? 0;
743
886
  }
887
+ /** Async twin of {@link countMessagesBySession}. */
888
+ async countMessagesBySessionAsync(sessionId) {
889
+ const rows = await this.db.queryAll(`SELECT COUNT(*) AS count FROM messages WHERE session_id = ?`, sessionId);
890
+ return rows[0]?.count ?? 0;
891
+ }
744
892
  mapMessageRows(rows) {
745
893
  return rows.map((r) => {
746
894
  const images = parseImagesJson(r.images_json);
@@ -952,6 +1100,11 @@ export class SessionStore {
952
1100
  getSessionActiveAgent(sessionId) {
953
1101
  return this.stmtGetSessionActiveAgent.get(sessionId)?.active_agent ?? null;
954
1102
  }
1103
+ /** Async twin of {@link getSessionActiveAgent}. */
1104
+ async getSessionActiveAgentAsync(sessionId) {
1105
+ const rows = await this.db.queryAll(`SELECT active_agent FROM sessions WHERE id = ?`, sessionId);
1106
+ return rows[0]?.active_agent ?? null;
1107
+ }
955
1108
  setSessionActiveAgent(sessionId, agentName) {
956
1109
  this.stmtSetSessionActiveAgent.run(agentName, sessionId);
957
1110
  }
@@ -1035,6 +1188,11 @@ export class SessionStore {
1035
1188
  const row = this.stmtGetForkCompactSource.get(sessionId);
1036
1189
  return row?.fork_compact_source_id ?? null;
1037
1190
  }
1191
+ /** Async twin of {@link getForkCompactSource}. */
1192
+ async getForkCompactSourceAsync(sessionId) {
1193
+ const rows = await this.db.queryAll(`SELECT fork_compact_source_id FROM sessions WHERE id = ?`, sessionId);
1194
+ return rows[0]?.fork_compact_source_id ?? null;
1195
+ }
1038
1196
  /**
1039
1197
  * Clear the fork-compact flag after compaction completes (or fails).
1040
1198
  * Idempotent-safe.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.9.172",
3
+ "version": "0.9.174",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,