@agentprojectcontext/apx 1.48.2 → 1.49.0

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": "@agentprojectcontext/apx",
3
- "version": "1.48.2",
3
+ "version": "1.49.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -12,22 +12,31 @@ import {
12
12
  agentSessionsDir,
13
13
  createAgentSessionFile,
14
14
  } from "#core/stores/sessions.js";
15
- import { collectAllSessions } from "#interfaces/cli/commands/sessions.js";
15
+ import { collectAllSessions, filterSessionsByQuery } from "#interfaces/cli/commands/sessions.js";
16
16
  import { pageEnvelope } from "./shared.js";
17
17
 
18
18
  export function register(app, { projects, project }) {
19
19
  // Cross-engine sessions (apx · claude · codex), newest first. Returns a
20
20
  // { meta, data } envelope (meta = pagination info, data = rows). Paginated
21
21
  // via ?limit & ?offset; with no limit, data is the full set as one page.
22
+ // Optional ?q= filters via the same core as `apx session find` (title match,
23
+ // + transcript content when ?deep=1) so terminal and web search are identical.
22
24
  app.get("/sessions", (req, res) => {
23
25
  const engineId = req.query.engine ? String(req.query.engine) : null;
26
+ const q = req.query.q ? String(req.query.q) : "";
27
+ const deep = req.query.deep === "1" || req.query.deep === "true";
24
28
  let rows = [];
25
29
  try {
26
30
  rows = collectAllSessions({}, { engineId });
27
31
  } catch (e) {
28
32
  return res.status(500).json({ error: e.message, meta: { total: 0, offset: 0, limit: null, pageSize: 0, page: 1, pageCount: 1 }, data: [] });
29
33
  }
30
- rows.sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
34
+ if (q.trim()) {
35
+ // filterSessionsByQuery already de-dupes and sorts newest-first.
36
+ rows = filterSessionsByQuery(rows, { query: q, deep });
37
+ } else {
38
+ rows.sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
39
+ }
31
40
  res.json(pageEnvelope(rows, req.query));
32
41
  });
33
42
 
@@ -774,6 +774,30 @@ function sessionContainsText(row, needle) {
774
774
  return text.toLowerCase().includes(needle);
775
775
  }
776
776
 
777
+ // Filter collected session rows by a free-text query. Always matches the
778
+ // title; with deep=true also scans transcript content (slower). De-dupes by
779
+ // engine:id and returns full rows + a `match` field ("title"|"content"),
780
+ // newest first. Shared core for the CLI (apx session find) and the daemon
781
+ // (GET /sessions?q=…) so terminal and web search behave identically.
782
+ export function filterSessionsByQuery(rows, { query, deep = false, limit = 0 } = {}) {
783
+ const needle = String(query || "").trim().toLowerCase();
784
+ if (!needle) return [];
785
+ const seen = new Set();
786
+ const matches = [];
787
+ for (const row of rows) {
788
+ const titleHit = String(row.title).toLowerCase().includes(needle);
789
+ let where = titleHit ? "title" : null;
790
+ if (!titleHit && deep && sessionContainsText(row, needle)) where = "content";
791
+ if (!where) continue;
792
+ const key = `${row.engine}:${row.id}`;
793
+ if (seen.has(key)) continue;
794
+ seen.add(key);
795
+ matches.push({ ...row, match: where });
796
+ }
797
+ matches.sort((a, b) => b.mtime - a.mtime);
798
+ return limit > 0 ? matches.slice(0, limit) : matches;
799
+ }
800
+
777
801
  export function cmdSessionFind(args, opts = {}) {
778
802
  const query = (args._ || []).join(" ").trim();
779
803
  if (!query) {
@@ -781,7 +805,6 @@ export function cmdSessionFind(args, opts = {}) {
781
805
  'apx session find: missing search text — e.g. apx session find "mejorar interfaz web"'
782
806
  );
783
807
  }
784
- const needle = query.toLowerCase();
785
808
  const deep = !!(args.flags.deep || args.flags.content);
786
809
  const asJson = !!args.flags.json;
787
810
  const engineFlag =
@@ -799,20 +822,7 @@ export function cmdSessionFind(args, opts = {}) {
799
822
 
800
823
  const dir = resolveTargetDir(args, opts);
801
824
  const rows = collectAllSessions(opts, { dir, engineId: engineFlag });
802
-
803
- const seen = new Set();
804
- const matches = [];
805
- for (const row of rows) {
806
- const titleHit = String(row.title).toLowerCase().includes(needle);
807
- let where = titleHit ? "title" : null;
808
- if (!titleHit && deep && sessionContainsText(row, needle)) where = "content";
809
- if (!where) continue;
810
- const key = `${row.engine}:${row.id}`;
811
- if (seen.has(key)) continue;
812
- seen.add(key);
813
- matches.push({ ...row, match: where });
814
- }
815
- matches.sort((a, b) => b.mtime - a.mtime);
825
+ const matches = filterSessionsByQuery(rows, { query, deep });
816
826
  const shown = matches.slice(0, limit);
817
827
 
818
828
  if (asJson) {