@corenel/sidecar 0.5.1 → 0.6.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.
Files changed (2) hide show
  1. package/dist/cli.js +150 -16
  2. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -21444,6 +21444,15 @@ import { join as join18 } from "node:path";
21444
21444
  import { readFileSync } from "node:fs";
21445
21445
  import { spawn as spawn9 } from "node:child_process";
21446
21446
 
21447
+ // ../protocol/ports.ts
21448
+ function compareSessionKeys(a2, b2) {
21449
+ if (a2.mtime !== b2.mtime) return b2.mtime - a2.mtime;
21450
+ return a2.path < b2.path ? -1 : a2.path > b2.path ? 1 : 0;
21451
+ }
21452
+ function isAfterSessionKey(k2, before) {
21453
+ return compareSessionKeys(before, k2) < 0;
21454
+ }
21455
+
21447
21456
  // ../protocol/files.ts
21448
21457
  var PROMPD_INSTALL_DIR = ".prompd";
21449
21458
  var INSTALL_TYPE_DIRS = ["packages", "workflows", "skills", "templates"];
@@ -24043,7 +24052,7 @@ var NodeFileService = class {
24043
24052
  out.push({ name: e2.name, path, kind: "directory", children: await walk(absChild) });
24044
24053
  } else {
24045
24054
  const st2 = await fs3.stat(absChild).catch(() => null);
24046
- out.push({ name: e2.name, path, kind: "file", size: st2?.size, ext: extOf(e2.name) });
24055
+ out.push({ name: e2.name, path, kind: "file", size: st2?.size, mtime: st2?.mtimeMs, ext: extOf(e2.name) });
24047
24056
  }
24048
24057
  }
24049
24058
  return out;
@@ -24056,7 +24065,7 @@ var NodeFileService = class {
24056
24065
  async stat(path) {
24057
24066
  try {
24058
24067
  const st2 = await fs3.stat(this.abs(path));
24059
- return { path: normalizeFilePath(path), kind: st2.isDirectory() ? "directory" : "file", size: st2.size, ext: extOf(path) };
24068
+ return { path: normalizeFilePath(path), kind: st2.isDirectory() ? "directory" : "file", size: st2.size, mtime: st2.mtimeMs, ext: extOf(path) };
24060
24069
  } catch {
24061
24070
  return null;
24062
24071
  }
@@ -27215,6 +27224,8 @@ async function assertRealWithinRoot(roots, abs) {
27215
27224
  }
27216
27225
  var ROOT_PARAM = { type: "string", description: "which pre-approved root to resolve the path against (its absolute path; defaults to the primary root)" };
27217
27226
  var MAX_READ_BYTES = 16 * 1024 * 1024;
27227
+ var MAX_LIST_DEPTH = 64;
27228
+ var MAX_LIST_ENTRIES = 2e4;
27218
27229
  function nodeFsTools(roots) {
27219
27230
  return [
27220
27231
  {
@@ -27290,14 +27301,50 @@ function nodeFsTools(roots) {
27290
27301
  description: "List the entries of a directory on the sidecar machine, relative to a pre-approved root.",
27291
27302
  parameters: {
27292
27303
  type: "object",
27293
- properties: { path: { type: "string", description: 'directory relative to the chosen root (default ".")' }, root: ROOT_PARAM }
27304
+ properties: {
27305
+ path: { type: "string", description: 'directory relative to the chosen root (default ".")' },
27306
+ detail: { type: "boolean", description: "return one JSON object per line carrying size and mtime, instead of bare names" },
27307
+ depth: { type: "number", description: "with detail: how many directory levels to descend (default 1). Names come back relative to the listed directory." },
27308
+ limit: { type: "number", description: "with detail: stop after this many entries (default 4000)" },
27309
+ skip: { type: "array", items: { type: "string" }, description: "with detail: directory names never descended into (listed, but not walked)" },
27310
+ root: ROOT_PARAM
27311
+ }
27294
27312
  },
27295
27313
  namespace: "sidecar",
27296
27314
  async run(args) {
27297
27315
  const abs = confine(roots, args.root, String(args.path ?? "."));
27298
27316
  await assertRealWithinRoot(roots, abs);
27299
- const entries = await fs7.readdir(abs, { withFileTypes: true });
27300
- return entries.map((e2) => e2.isDirectory() ? `${e2.name}/` : e2.name).join("\n");
27317
+ if (!args.detail) {
27318
+ const entries = await fs7.readdir(abs, { withFileTypes: true });
27319
+ return entries.map((e2) => e2.isDirectory() ? `${e2.name}/` : e2.name).join("\n");
27320
+ }
27321
+ const depth = Math.min(
27322
+ typeof args.depth === "number" && args.depth > 0 ? Math.floor(args.depth) : 1,
27323
+ MAX_LIST_DEPTH
27324
+ );
27325
+ const limit2 = Math.min(
27326
+ typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 4e3,
27327
+ MAX_LIST_ENTRIES
27328
+ );
27329
+ const skip = new Set(Array.isArray(args.skip) ? args.skip.map((x2) => String(x2)) : []);
27330
+ const out = [];
27331
+ const walk = async (dirAbs, rel, left) => {
27332
+ if (out.length >= limit2) return;
27333
+ const entries = await fs7.readdir(dirAbs, { withFileTypes: true }).catch(() => []);
27334
+ for (const e2 of entries) {
27335
+ if (out.length >= limit2) return;
27336
+ const name = rel ? `${rel}/${e2.name}` : e2.name;
27337
+ if (e2.isDirectory()) {
27338
+ out.push(JSON.stringify({ n: name, d: 1 }));
27339
+ if (left > 1 && !skip.has(e2.name)) await walk(resolve2(dirAbs, e2.name), name, left - 1);
27340
+ continue;
27341
+ }
27342
+ const st2 = await fs7.lstat(resolve2(dirAbs, e2.name)).catch(() => null);
27343
+ out.push(JSON.stringify({ n: name, ...st2 ? { s: st2.size, m: Math.round(st2.mtimeMs) } : {} }));
27344
+ }
27345
+ };
27346
+ await walk(abs, "", depth);
27347
+ return out.join("\n");
27301
27348
  }
27302
27349
  },
27303
27350
  {
@@ -39950,6 +39997,20 @@ function reconcileDanglingToolCalls(messages) {
39950
39997
  }
39951
39998
 
39952
39999
  // ../harness/sessions/fileSessionStore.ts
40000
+ var READ_CONCURRENCY = 8;
40001
+ async function mapPool(items, limit2, fn) {
40002
+ const out = new Array(items.length);
40003
+ let next = 0;
40004
+ const worker = async () => {
40005
+ for (; ; ) {
40006
+ const i2 = next++;
40007
+ if (i2 >= items.length) return;
40008
+ out[i2] = await fn(items[i2]);
40009
+ }
40010
+ };
40011
+ await Promise.all(Array.from({ length: Math.min(limit2, items.length) }, worker));
40012
+ return out;
40013
+ }
39953
40014
  function uid(prefix) {
39954
40015
  return `${prefix}_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`;
39955
40016
  }
@@ -39959,7 +40020,7 @@ function now() {
39959
40020
  lastTs = t2 > lastTs ? t2 : lastTs + 1;
39960
40021
  return lastTs;
39961
40022
  }
39962
- var JsonlSessionStore = class {
40023
+ var JsonlSessionStore = class _JsonlSessionStore {
39963
40024
  constructor(getFiles, root = sessionsRoot) {
39964
40025
  this.getFiles = getFiles;
39965
40026
  this.root = root;
@@ -39996,19 +40057,92 @@ var JsonlSessionStore = class {
39996
40057
  }
39997
40058
  return ses;
39998
40059
  }
39999
- async list(surface) {
40060
+ /** The SESSION.jsonl nodes under the root.
40061
+ *
40062
+ * list(dir) shape differs per backend: MemoryFileService returns a tree
40063
+ * rooted at the WORKSPACE root (the top level is the ancestor chain, e.g.
40064
+ * ['.prompd']), while others return dir's children. Never trust the top
40065
+ * level — flatten and match the SESSION.jsonl files by path. */
40066
+ async sessionFiles() {
40000
40067
  const files = await this.files();
40001
40068
  const root = this.root();
40002
40069
  const nodes = await files.list(root).catch(() => []);
40003
- const sessionFiles = flattenFiles(nodes).filter((f2) => f2.name === "SESSION.jsonl" && f2.path.startsWith(root + "/"));
40004
- const out = [];
40005
- for (const f2 of sessionFiles) {
40006
- const text = await files.read(f2.path).catch(() => null);
40007
- const ses = text != null ? foldSession(text) : null;
40008
- if (ses) out.push(ses);
40009
- }
40010
- const scoped = surface ? out.filter((s2) => s2.surface === surface || surface === "agent" && !s2.surface) : out;
40011
- return scoped.sort((a2, b2) => b2.updatedAt - a2.updatedAt);
40070
+ return flattenFiles(nodes).filter((f2) => f2.name === "SESSION.jsonl" && f2.path.startsWith(root + "/"));
40071
+ }
40072
+ /** Fold the session at `path`, or null when it cannot be read or parsed. */
40073
+ async readSession(path) {
40074
+ const files = await this.files();
40075
+ const text = await files.read(path).catch(() => null);
40076
+ return text != null ? foldSession(text) : null;
40077
+ }
40078
+ static scoped(s2, surface) {
40079
+ return !surface || s2.surface === surface || surface === "agent" && !s2.surface;
40080
+ }
40081
+ async list(surface) {
40082
+ const nodes = await this.sessionFiles();
40083
+ const folded = await mapPool(nodes, READ_CONCURRENCY, (f2) => this.readSession(f2.path));
40084
+ const out = folded.filter((s2) => s2 !== null && _JsonlSessionStore.scoped(s2, surface));
40085
+ return out.sort((a2, b2) => b2.updatedAt - a2.updatedAt);
40086
+ }
40087
+ /**
40088
+ * One page of sessions, newest first, WITHOUT opening the whole history.
40089
+ *
40090
+ * TWO PATHS, chosen by whether the backend dated the listing:
40091
+ *
40092
+ * mtime present NodeFileService (so any sidecar workspace) and
40093
+ * DirectoryHandleFileService. Sort the directory listing by
40094
+ * mtime, cut the page, and read only those files. The
40095
+ * cursor is an mtime.
40096
+ *
40097
+ * mtime absent MemoryFileService, the virtual IndexedDB workspace, which
40098
+ * does not date its entries. There is no way to know which
40099
+ * sessions are newest without folding them, so this does
40100
+ * exactly what list() always did and pages the result. No
40101
+ * faster than before, but not WRONG — and its reads are
40102
+ * local and cheap, which is why that trade is acceptable.
40103
+ *
40104
+ * Correctness first: a partially-dated listing (some files with mtime, some
40105
+ * without) takes the slow path too, because sorting undefined against a real
40106
+ * timestamp would silently misorder the history.
40107
+ */
40108
+ async listPage(q2) {
40109
+ const nodes = await this.sessionFiles();
40110
+ const limit2 = Math.max(1, Math.floor(q2.limit));
40111
+ if (nodes.length > 0 && nodes.every((n2) => typeof n2.mtime === "number")) {
40112
+ const keyOf3 = (n2) => ({ mtime: n2.mtime, path: n2.path });
40113
+ const ordered = [...nodes].sort((a2, b2) => compareSessionKeys(keyOf3(a2), keyOf3(b2))).filter((n2) => q2.before === void 0 || isAfterSessionKey(keyOf3(n2), q2.before));
40114
+ const kept = [];
40115
+ for (let i2 = 0; i2 < ordered.length && kept.length <= limit2; i2 += limit2 + 1) {
40116
+ const batch = ordered.slice(i2, i2 + limit2 + 1);
40117
+ const folded = await mapPool(batch, READ_CONCURRENCY, async (n2) => {
40118
+ const s2 = await this.readSession(n2.path);
40119
+ return s2 ? { s: s2, key: keyOf3(n2) } : null;
40120
+ });
40121
+ for (const r2 of folded) if (r2 && _JsonlSessionStore.scoped(r2.s, q2.surface)) kept.push(r2);
40122
+ }
40123
+ const more2 = kept.length > limit2;
40124
+ const page2 = kept.slice(0, limit2);
40125
+ const last2 = page2[page2.length - 1];
40126
+ return {
40127
+ sessions: page2.map((r2) => r2.s),
40128
+ keys: page2.map((r2) => r2.key),
40129
+ ...more2 && last2 ? { cursor: last2.key } : {},
40130
+ more: more2
40131
+ };
40132
+ }
40133
+ const all = await this.list(q2.surface);
40134
+ const keyed = all.map((s2) => ({ s: s2, key: { mtime: s2.updatedAt, path: this.path(s2.id) } }));
40135
+ keyed.sort((a2, b2) => compareSessionKeys(a2.key, b2.key));
40136
+ const from = q2.before === void 0 ? keyed : keyed.filter((r2) => isAfterSessionKey(r2.key, q2.before));
40137
+ const page = from.slice(0, limit2);
40138
+ const more = from.length > limit2;
40139
+ const last = page[page.length - 1];
40140
+ return {
40141
+ sessions: page.map((r2) => r2.s),
40142
+ keys: page.map((r2) => r2.key),
40143
+ ...more && last ? { cursor: last.key } : {},
40144
+ more
40145
+ };
40012
40146
  }
40013
40147
  async save(session) {
40014
40148
  const at2 = now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corenel/sidecar",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "license": "Elastic-2.0",
6
6
  "description": "Prompd sidecar — a local capability server. Run it next to Prompd (prompd.app) so the browser app can reach your machine: local LLMs (Ollama/LM Studio/vLLM) without CORS, your filesystem, and (opt-in) shell.",
@@ -36,10 +36,10 @@
36
36
  "esbuild": "^0.21.5",
37
37
  "tsx": "^4.19.2",
38
38
  "typescript": "^5.9.3",
39
- "@corenel/protocol": "0.5.0",
39
+ "@corenel/protocol": "0.6.0",
40
40
  "@corenel/crew": "0.0.0",
41
- "@corenel/tools-node": "0.2.2",
42
- "@corenel/harness": "0.5.1"
41
+ "@corenel/harness": "0.6.0",
42
+ "@corenel/tools-node": "0.3.0"
43
43
  },
44
44
  "repository": {
45
45
  "type": "git",