@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.
- package/dist/cli.js +150 -16
- 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: {
|
|
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
|
-
|
|
27300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40004
|
-
|
|
40005
|
-
|
|
40006
|
-
|
|
40007
|
-
|
|
40008
|
-
|
|
40009
|
-
|
|
40010
|
-
|
|
40011
|
-
|
|
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.
|
|
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.
|
|
39
|
+
"@corenel/protocol": "0.6.0",
|
|
40
40
|
"@corenel/crew": "0.0.0",
|
|
41
|
-
"@corenel/
|
|
42
|
-
"@corenel/
|
|
41
|
+
"@corenel/harness": "0.6.0",
|
|
42
|
+
"@corenel/tools-node": "0.3.0"
|
|
43
43
|
},
|
|
44
44
|
"repository": {
|
|
45
45
|
"type": "git",
|