@persistmemory/cli 0.1.0 → 0.1.2
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/bin.js +731 -68
- package/dist/bin.js.map +4 -4
- package/dist/index.js +731 -68
- package/dist/index.js.map +4 -4
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -344,7 +344,7 @@ var SignalFired = class extends Error {
|
|
|
344
344
|
};
|
|
345
345
|
function untilAborted(work, signal) {
|
|
346
346
|
work.catch(() => void 0);
|
|
347
|
-
return new Promise((
|
|
347
|
+
return new Promise((resolve7, reject) => {
|
|
348
348
|
if (signal.aborted) {
|
|
349
349
|
reject(new SignalFired());
|
|
350
350
|
return;
|
|
@@ -354,7 +354,7 @@ function untilAborted(work, signal) {
|
|
|
354
354
|
work.then(
|
|
355
355
|
(value) => {
|
|
356
356
|
signal.removeEventListener("abort", onAbort);
|
|
357
|
-
|
|
357
|
+
resolve7(value);
|
|
358
358
|
},
|
|
359
359
|
(error) => {
|
|
360
360
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -364,14 +364,14 @@ function untilAborted(work, signal) {
|
|
|
364
364
|
});
|
|
365
365
|
}
|
|
366
366
|
function defaultSleep(ms, signal) {
|
|
367
|
-
return new Promise((
|
|
367
|
+
return new Promise((resolve7, reject) => {
|
|
368
368
|
if (signal?.aborted) {
|
|
369
369
|
reject(new AbortError());
|
|
370
370
|
return;
|
|
371
371
|
}
|
|
372
372
|
const timer = setTimeout(() => {
|
|
373
373
|
signal?.removeEventListener("abort", onAbort);
|
|
374
|
-
|
|
374
|
+
resolve7();
|
|
375
375
|
}, ms);
|
|
376
376
|
function onAbort() {
|
|
377
377
|
clearTimeout(timer);
|
|
@@ -558,6 +558,58 @@ var Spaces = class {
|
|
|
558
558
|
async create(params, options) {
|
|
559
559
|
return this.#http.post("/api/v1/spaces", params, options);
|
|
560
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* Deletes a Space. You must say what happens to what is in it.
|
|
563
|
+
*
|
|
564
|
+
* There is no default, here or in the API, and that is deliberate: "delete
|
|
565
|
+
* this Space" means the label to some people and everything inside it to
|
|
566
|
+
* others, and a client that guessed would destroy or keep somebody's
|
|
567
|
+
* material without being asked.
|
|
568
|
+
*
|
|
569
|
+
* `delete` never destroys a memory that is filed in another Space as well —
|
|
570
|
+
* that one is detached and left alone. `deleted` and `kept` come back so you
|
|
571
|
+
* can say what actually happened.
|
|
572
|
+
*/
|
|
573
|
+
async delete(id, params, options) {
|
|
574
|
+
return this.#http.delete(
|
|
575
|
+
`/api/v1/spaces/${encodeURIComponent(id)}`,
|
|
576
|
+
params,
|
|
577
|
+
options
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Merges Spaces into a NEW one, leaving every source exactly as it was.
|
|
582
|
+
*
|
|
583
|
+
* Additive, not destructive: a memory ends up in the sources AND the result,
|
|
584
|
+
* every existing search over a source returns what it did before, and
|
|
585
|
+
* undoing it is deleting the Space this returns. A memory in two sources is
|
|
586
|
+
* filed once.
|
|
587
|
+
*/
|
|
588
|
+
async merge(params, options) {
|
|
589
|
+
return this.#http.post(
|
|
590
|
+
"/api/v1/spaces/merge",
|
|
591
|
+
params,
|
|
592
|
+
options
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* The Space this account files into when a capture names none.
|
|
597
|
+
*
|
|
598
|
+
* `{}` — an object with no `space` — means there is no default, which is the
|
|
599
|
+
* normal state rather than a gap. It is also what comes back after the Space
|
|
600
|
+
* somebody chose has been deleted.
|
|
601
|
+
*/
|
|
602
|
+
async getDefault(options) {
|
|
603
|
+
return this.#http.get("/api/v1/spaces/default", void 0, options);
|
|
604
|
+
}
|
|
605
|
+
/** `null` clears it. Not the same as omitting it, which is why the type says so. */
|
|
606
|
+
async setDefault(spaceId, options) {
|
|
607
|
+
return this.#http.patch(
|
|
608
|
+
"/api/v1/spaces/default",
|
|
609
|
+
{ spaceId },
|
|
610
|
+
options
|
|
611
|
+
);
|
|
612
|
+
}
|
|
561
613
|
/** Renaming, retention, and archiving - `archived` is a field, not a verb. */
|
|
562
614
|
async update(id, params, options) {
|
|
563
615
|
return this.#http.patch(`/api/v1/spaces/${encodeURIComponent(id)}`, params, options);
|
|
@@ -920,6 +972,42 @@ var Health = class {
|
|
|
920
972
|
return this.#http.get("/health/ready", void 0, options);
|
|
921
973
|
}
|
|
922
974
|
};
|
|
975
|
+
var Agent = class {
|
|
976
|
+
#http;
|
|
977
|
+
constructor(http) {
|
|
978
|
+
this.#http = http;
|
|
979
|
+
}
|
|
980
|
+
/** The row, including whether it finished and how large the result is. */
|
|
981
|
+
async request(id, options) {
|
|
982
|
+
return this.#http.get(
|
|
983
|
+
`/api/v1/agent/request/${encodeURIComponent(id)}`,
|
|
984
|
+
void 0,
|
|
985
|
+
options
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* A short-lived link to the bytes of a finished request.
|
|
990
|
+
*
|
|
991
|
+
* Returns the URL rather than the file, and that is a deliberate limit of
|
|
992
|
+
* this package rather than an oversight. The transport under every other
|
|
993
|
+
* method parses JSON, retries, and attaches the API key; none of those is
|
|
994
|
+
* right for a hundred-megabyte binary body, and building a second request
|
|
995
|
+
* path inside the SDK to serve one method is how a client ends up with two
|
|
996
|
+
* retry policies that differ only during an outage. Fetch the URL with
|
|
997
|
+
* whatever already streams in your runtime - it needs no credential, which
|
|
998
|
+
* is the whole reason it is signed.
|
|
999
|
+
*
|
|
1000
|
+
* Treat the URL as the file. It is a bearer credential for exactly one
|
|
1001
|
+
* object, it expires in minutes, and it should not be logged or stored.
|
|
1002
|
+
*/
|
|
1003
|
+
async downloadLink(id, options) {
|
|
1004
|
+
return this.#http.get(
|
|
1005
|
+
`/api/v1/agent/request/${encodeURIComponent(id)}/download`,
|
|
1006
|
+
void 0,
|
|
1007
|
+
options
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
923
1011
|
var PersistMemory = class {
|
|
924
1012
|
memories;
|
|
925
1013
|
search;
|
|
@@ -933,6 +1021,7 @@ var PersistMemory = class {
|
|
|
933
1021
|
conversations;
|
|
934
1022
|
integrations;
|
|
935
1023
|
health;
|
|
1024
|
+
agent;
|
|
936
1025
|
#http;
|
|
937
1026
|
constructor(options) {
|
|
938
1027
|
this.#http = new HttpClient(options);
|
|
@@ -948,6 +1037,7 @@ var PersistMemory = class {
|
|
|
948
1037
|
this.conversations = new Conversations(this.#http);
|
|
949
1038
|
this.integrations = new Integrations(this.#http);
|
|
950
1039
|
this.health = new Health(this.#http);
|
|
1040
|
+
this.agent = new Agent(this.#http);
|
|
951
1041
|
}
|
|
952
1042
|
/**
|
|
953
1043
|
* An escape hatch for an endpoint this package has not caught up with.
|
|
@@ -1289,7 +1379,8 @@ function shortDate(iso) {
|
|
|
1289
1379
|
}
|
|
1290
1380
|
|
|
1291
1381
|
// src/help.ts
|
|
1292
|
-
var VERSION = "0.1.
|
|
1382
|
+
var VERSION = "0.1.2";
|
|
1383
|
+
var PACKAGE = "@persistmemory/cli";
|
|
1293
1384
|
var HELP = `
|
|
1294
1385
|
pm \u2014 PersistMemory from your terminal
|
|
1295
1386
|
|
|
@@ -1297,7 +1388,9 @@ var HELP = `
|
|
|
1297
1388
|
|
|
1298
1389
|
GETTING STARTED
|
|
1299
1390
|
|
|
1391
|
+
pm setup sign in, then pick a Space for this folder
|
|
1300
1392
|
pm auth login sign in with your browser
|
|
1393
|
+
pm auth logout sign out on this machine
|
|
1301
1394
|
pm start a session and just ask
|
|
1302
1395
|
pm remember "we chose Postgres" capture something
|
|
1303
1396
|
pm search "what did we choose" ask for it back
|
|
@@ -1315,6 +1408,13 @@ var HELP = `
|
|
|
1315
1408
|
chat start an interactive session
|
|
1316
1409
|
chat --resume <id> pick up an earlier session
|
|
1317
1410
|
|
|
1411
|
+
setup sign in and choose this folder's Space
|
|
1412
|
+
setup --space "Acme" choose one without being asked
|
|
1413
|
+
setup --new-space "Acme" create one and use it
|
|
1414
|
+
|
|
1415
|
+
spaces list your Spaces
|
|
1416
|
+
spaces create <name> make a new one
|
|
1417
|
+
|
|
1318
1418
|
remember <text> capture text
|
|
1319
1419
|
remember - capture whatever is piped in
|
|
1320
1420
|
remember --file <path> capture a file's contents
|
|
@@ -1326,6 +1426,11 @@ var HELP = `
|
|
|
1326
1426
|
|
|
1327
1427
|
status is the service healthy
|
|
1328
1428
|
requests file requests waiting for you to approve
|
|
1429
|
+
requests get <id> write a finished one to a file here
|
|
1430
|
+
|
|
1431
|
+
update install the newest version
|
|
1432
|
+
uninstall remove pm from this machine
|
|
1433
|
+
delete delete every file pm has written here
|
|
1329
1434
|
|
|
1330
1435
|
FLAGS
|
|
1331
1436
|
|
|
@@ -1334,7 +1439,7 @@ var HELP = `
|
|
|
1334
1439
|
--profile <name> use a named account
|
|
1335
1440
|
--api-url <url> talk to a different server
|
|
1336
1441
|
--limit <n> how many results
|
|
1337
|
-
--space <a,b> restrict to Spaces
|
|
1442
|
+
--space <a,b> restrict to Spaces, by name or id
|
|
1338
1443
|
--quiet suppress notices
|
|
1339
1444
|
--version print the version
|
|
1340
1445
|
--help print this
|
|
@@ -1345,14 +1450,96 @@ var HELP = `
|
|
|
1345
1450
|
login. This is what CI should set.
|
|
1346
1451
|
PERSISTMEMORY_API_URL the server to talk to
|
|
1347
1452
|
PERSISTMEMORY_PROFILE which stored profile to use
|
|
1453
|
+
PERSISTMEMORY_SPACE Spaces to use when --space is not given
|
|
1348
1454
|
PERSISTMEMORY_HOME where config, credentials and session
|
|
1349
1455
|
transcripts live (default: ~/.persistmemory)
|
|
1350
1456
|
PERSISTMEMORY_CLIENT_ID override the OAuth client id, for a
|
|
1351
1457
|
self-hosted deployment
|
|
1458
|
+
PERSISTMEMORY_AUTO_UPDATE update without asking when a newer
|
|
1459
|
+
version is published
|
|
1460
|
+
PERSISTMEMORY_NO_UPDATE never check for updates
|
|
1352
1461
|
|
|
1353
1462
|
Docs: https://persistmemory.com/docs/cli
|
|
1354
1463
|
`;
|
|
1355
1464
|
|
|
1465
|
+
// src/update.ts
|
|
1466
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1467
|
+
import { dirname, join as join2 } from "node:path";
|
|
1468
|
+
var REGISTRY = "https://registry.npmjs.org/@persistmemory/cli/latest";
|
|
1469
|
+
var EVERY_MS = 24 * 60 * 60 * 1e3;
|
|
1470
|
+
function updateNotice(deps) {
|
|
1471
|
+
if (!wanted(deps)) return void 0;
|
|
1472
|
+
const cached = read(deps.file);
|
|
1473
|
+
if (!cached) return void 0;
|
|
1474
|
+
if (!isNewer(cached.latest, deps.current)) return void 0;
|
|
1475
|
+
return [
|
|
1476
|
+
`A newer PersistMemory CLI is available: ${deps.current} \u2192 ${cached.latest}`,
|
|
1477
|
+
`Run \`pm update\` to install it.`
|
|
1478
|
+
].join("\n");
|
|
1479
|
+
}
|
|
1480
|
+
async function refreshUpdateCache(deps) {
|
|
1481
|
+
if (!wanted(deps)) return;
|
|
1482
|
+
const now = (deps.now ?? Date.now)();
|
|
1483
|
+
const cached = read(deps.file);
|
|
1484
|
+
if (cached && now - cached.checkedAt < EVERY_MS) return;
|
|
1485
|
+
const call = deps.fetch ?? globalThis.fetch;
|
|
1486
|
+
try {
|
|
1487
|
+
const controller = new AbortController();
|
|
1488
|
+
const timer = setTimeout(() => controller.abort(), 1500);
|
|
1489
|
+
const response = await call(REGISTRY, {
|
|
1490
|
+
signal: controller.signal,
|
|
1491
|
+
headers: { accept: "application/vnd.npm.install-v1+json" }
|
|
1492
|
+
}).finally(() => clearTimeout(timer));
|
|
1493
|
+
if (!response.ok) return;
|
|
1494
|
+
const body = await response.json();
|
|
1495
|
+
if (typeof body.version !== "string") return;
|
|
1496
|
+
write(deps.file, { checkedAt: now, latest: body.version });
|
|
1497
|
+
} catch {
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function wanted(deps) {
|
|
1501
|
+
const env = deps.env ?? process.env;
|
|
1502
|
+
if (deps.quiet === true) return false;
|
|
1503
|
+
if (deps.isTty === false) return false;
|
|
1504
|
+
if (env["PERSISTMEMORY_NO_UPDATE"]) return false;
|
|
1505
|
+
if (env["CI"]) return false;
|
|
1506
|
+
return true;
|
|
1507
|
+
}
|
|
1508
|
+
function read(file) {
|
|
1509
|
+
try {
|
|
1510
|
+
if (!existsSync2(file)) return void 0;
|
|
1511
|
+
const parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
1512
|
+
if (typeof parsed.checkedAt !== "number" || typeof parsed.latest !== "string") {
|
|
1513
|
+
return void 0;
|
|
1514
|
+
}
|
|
1515
|
+
return { checkedAt: parsed.checkedAt, latest: parsed.latest };
|
|
1516
|
+
} catch {
|
|
1517
|
+
return void 0;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
function write(file, value) {
|
|
1521
|
+
try {
|
|
1522
|
+
mkdirSync2(dirname(file), { recursive: true });
|
|
1523
|
+
writeFileSync2(file, JSON.stringify(value), "utf8");
|
|
1524
|
+
} catch {
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function isNewer(candidate, current) {
|
|
1528
|
+
if (candidate.includes("-") || current.includes("-")) return false;
|
|
1529
|
+
const a = candidate.split(".").map(Number);
|
|
1530
|
+
const b = current.split(".").map(Number);
|
|
1531
|
+
if (a.some(Number.isNaN) || b.some(Number.isNaN)) return false;
|
|
1532
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
1533
|
+
const left = a[index] ?? 0;
|
|
1534
|
+
const right = b[index] ?? 0;
|
|
1535
|
+
if (left !== right) return left > right;
|
|
1536
|
+
}
|
|
1537
|
+
return false;
|
|
1538
|
+
}
|
|
1539
|
+
function updateCacheFile(home) {
|
|
1540
|
+
return join2(home, "update-check.json");
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1356
1543
|
// src/auth/oauth.ts
|
|
1357
1544
|
import { spawn } from "node:child_process";
|
|
1358
1545
|
import { timingSafeEqual } from "node:crypto";
|
|
@@ -1383,8 +1570,8 @@ async function startLoopback(options = {}) {
|
|
|
1383
1570
|
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
|
|
1384
1571
|
let resolveCallback;
|
|
1385
1572
|
let rejectCallback;
|
|
1386
|
-
const received = new Promise((
|
|
1387
|
-
resolveCallback =
|
|
1573
|
+
const received = new Promise((resolve7, reject) => {
|
|
1574
|
+
resolveCallback = resolve7;
|
|
1388
1575
|
rejectCallback = reject;
|
|
1389
1576
|
});
|
|
1390
1577
|
const server = createServer((request, response) => {
|
|
@@ -1410,9 +1597,9 @@ async function startLoopback(options = {}) {
|
|
|
1410
1597
|
response.end(donePage(callback));
|
|
1411
1598
|
resolveCallback?.(callback);
|
|
1412
1599
|
});
|
|
1413
|
-
await new Promise((
|
|
1600
|
+
await new Promise((resolve7, reject) => {
|
|
1414
1601
|
server.once("error", reject);
|
|
1415
|
-
server.listen(0, "127.0.0.1",
|
|
1602
|
+
server.listen(0, "127.0.0.1", resolve7);
|
|
1416
1603
|
});
|
|
1417
1604
|
const address = server.address();
|
|
1418
1605
|
if (address === null || typeof address === "string") {
|
|
@@ -1674,7 +1861,7 @@ function safeEqual(a, b) {
|
|
|
1674
1861
|
}
|
|
1675
1862
|
async function openBrowser(url) {
|
|
1676
1863
|
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
1677
|
-
await new Promise((
|
|
1864
|
+
await new Promise((resolve7, reject) => {
|
|
1678
1865
|
const child = spawn(command, args, {
|
|
1679
1866
|
stdio: "ignore",
|
|
1680
1867
|
// Detached so closing the terminal does not close the browser, and so
|
|
@@ -1683,7 +1870,7 @@ async function openBrowser(url) {
|
|
|
1683
1870
|
});
|
|
1684
1871
|
child.once("error", reject);
|
|
1685
1872
|
child.unref();
|
|
1686
|
-
|
|
1873
|
+
resolve7();
|
|
1687
1874
|
});
|
|
1688
1875
|
}
|
|
1689
1876
|
async function describe(response) {
|
|
@@ -1744,19 +1931,29 @@ async function currentCredential(resolved, deps) {
|
|
|
1744
1931
|
|
|
1745
1932
|
// src/context.ts
|
|
1746
1933
|
import { createInterface } from "node:readline";
|
|
1934
|
+
async function askOnTty(prompt) {
|
|
1935
|
+
return new Promise((resolve7) => {
|
|
1936
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
1937
|
+
readline.question(prompt, (answer3) => {
|
|
1938
|
+
readline.close();
|
|
1939
|
+
resolve7(answer3.trim());
|
|
1940
|
+
});
|
|
1941
|
+
readline.once("close", () => resolve7(""));
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1747
1944
|
var ETX = "";
|
|
1748
1945
|
var DELETE = "\x7F";
|
|
1749
1946
|
var BACKSPACE = "\b";
|
|
1750
1947
|
async function readSecretFromTty(prompt) {
|
|
1751
1948
|
const input = process.stdin;
|
|
1752
1949
|
if (!input.isTTY) {
|
|
1753
|
-
return new Promise((
|
|
1950
|
+
return new Promise((resolve7) => {
|
|
1754
1951
|
const readline = createInterface({ input });
|
|
1755
1952
|
readline.once("line", (line) => {
|
|
1756
1953
|
readline.close();
|
|
1757
|
-
|
|
1954
|
+
resolve7(line.trim());
|
|
1758
1955
|
});
|
|
1759
|
-
readline.once("close", () =>
|
|
1956
|
+
readline.once("close", () => resolve7(""));
|
|
1760
1957
|
});
|
|
1761
1958
|
}
|
|
1762
1959
|
process.stdout.write(prompt);
|
|
@@ -1764,21 +1961,21 @@ async function readSecretFromTty(prompt) {
|
|
|
1764
1961
|
input.setRawMode?.(true);
|
|
1765
1962
|
input.resume();
|
|
1766
1963
|
input.setEncoding("utf8");
|
|
1767
|
-
return new Promise((
|
|
1964
|
+
return new Promise((resolve7) => {
|
|
1768
1965
|
let value = "";
|
|
1769
|
-
const
|
|
1966
|
+
const finish2 = () => {
|
|
1770
1967
|
input.removeListener("data", onData);
|
|
1771
1968
|
input.setRawMode?.(previouslyRaw);
|
|
1772
1969
|
input.pause();
|
|
1773
1970
|
process.stdout.write("\n");
|
|
1774
|
-
|
|
1971
|
+
resolve7(value.trim());
|
|
1775
1972
|
};
|
|
1776
1973
|
const onData = (chunk) => {
|
|
1777
1974
|
for (const character of chunk) {
|
|
1778
1975
|
switch (character) {
|
|
1779
1976
|
case "\r":
|
|
1780
1977
|
case "\n":
|
|
1781
|
-
|
|
1978
|
+
finish2();
|
|
1782
1979
|
return;
|
|
1783
1980
|
case ETX:
|
|
1784
1981
|
input.setRawMode?.(previouslyRaw);
|
|
@@ -1808,12 +2005,12 @@ async function readStdin() {
|
|
|
1808
2005
|
// src/commands/agent.ts
|
|
1809
2006
|
import { hostname } from "node:os";
|
|
1810
2007
|
import { homedir as homedir2 } from "node:os";
|
|
1811
|
-
import { resolve as resolve3 } from "node:path";
|
|
1812
|
-
import { readFileSync as
|
|
2008
|
+
import { join as join4, resolve as resolve3 } from "node:path";
|
|
2009
|
+
import { readFileSync as readFileSync4, readdirSync, statSync as statSync2 } from "node:fs";
|
|
1813
2010
|
|
|
1814
2011
|
// src/files.ts
|
|
1815
|
-
import { existsSync as
|
|
1816
|
-
import { dirname, isAbsolute, join as
|
|
2012
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2013
|
+
import { dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "node:path";
|
|
1817
2014
|
var OutsideWorkspace = class extends Error {
|
|
1818
2015
|
constructor(path) {
|
|
1819
2016
|
super(`${path} is outside the directory this session was started in.`);
|
|
@@ -1822,22 +2019,24 @@ var OutsideWorkspace = class extends Error {
|
|
|
1822
2019
|
};
|
|
1823
2020
|
var TooLarge = class extends Error {
|
|
1824
2021
|
constructor(path, bytes, limit) {
|
|
1825
|
-
|
|
2022
|
+
const say = (value) => value >= 1024 * 1024 ? `${(value / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(value / 1024)} KB`;
|
|
2023
|
+
super(`${path} is ${say(bytes)}, over the ${say(limit)} limit.`);
|
|
1826
2024
|
this.name = "TooLarge";
|
|
1827
2025
|
}
|
|
1828
2026
|
};
|
|
1829
2027
|
var MAX_READ_BYTES = 512 * 1024;
|
|
2028
|
+
var MAX_TRANSFER_BYTES = 100 * 1024 * 1024;
|
|
1830
2029
|
function realLocation(absolute) {
|
|
1831
2030
|
let existing = absolute;
|
|
1832
2031
|
const trailing = [];
|
|
1833
|
-
while (!
|
|
1834
|
-
const parent =
|
|
2032
|
+
while (!existsSync3(existing)) {
|
|
2033
|
+
const parent = dirname2(existing);
|
|
1835
2034
|
if (parent === existing) return absolute;
|
|
1836
2035
|
trailing.unshift(existing.slice(parent.length + 1));
|
|
1837
2036
|
existing = parent;
|
|
1838
2037
|
}
|
|
1839
2038
|
try {
|
|
1840
|
-
return
|
|
2039
|
+
return join3(realpathSync(existing), ...trailing);
|
|
1841
2040
|
} catch {
|
|
1842
2041
|
return absolute;
|
|
1843
2042
|
}
|
|
@@ -1859,7 +2058,7 @@ function readWithin(root, path) {
|
|
|
1859
2058
|
if (stats.size > MAX_READ_BYTES) throw new TooLarge(path, stats.size, MAX_READ_BYTES);
|
|
1860
2059
|
return {
|
|
1861
2060
|
path: absolute,
|
|
1862
|
-
text:
|
|
2061
|
+
text: readFileSync3(absolute, "utf8"),
|
|
1863
2062
|
bytes: stats.size
|
|
1864
2063
|
};
|
|
1865
2064
|
}
|
|
@@ -1869,7 +2068,7 @@ function proposeWrite(root, path, contents) {
|
|
|
1869
2068
|
try {
|
|
1870
2069
|
const stats = statSync(absolute);
|
|
1871
2070
|
if (stats.isFile() && stats.size <= MAX_READ_BYTES) {
|
|
1872
|
-
existing =
|
|
2071
|
+
existing = readFileSync3(absolute, "utf8");
|
|
1873
2072
|
}
|
|
1874
2073
|
} catch {
|
|
1875
2074
|
}
|
|
@@ -1879,21 +2078,21 @@ function proposeWrite(root, path, contents) {
|
|
|
1879
2078
|
...existing !== void 0 ? { existing } : {}
|
|
1880
2079
|
};
|
|
1881
2080
|
}
|
|
1882
|
-
function commitWrite(
|
|
2081
|
+
function commitWrite(write3, confirmed) {
|
|
1883
2082
|
if (!confirmed) throw new Error("refusing to write without confirmation");
|
|
1884
|
-
|
|
2083
|
+
writeFileSync3(write3.path, write3.contents, "utf8");
|
|
1885
2084
|
}
|
|
1886
|
-
function summarise(
|
|
1887
|
-
if (
|
|
1888
|
-
const lines =
|
|
2085
|
+
function summarise(write3, maxLines = 40) {
|
|
2086
|
+
if (write3.existing === void 0) {
|
|
2087
|
+
const lines = write3.contents.split("\n");
|
|
1889
2088
|
const head = lines.slice(0, maxLines).map((line) => `+ ${line}`);
|
|
1890
2089
|
if (lines.length > maxLines) head.push(` \u2026 ${lines.length - maxLines} more lines`);
|
|
1891
|
-
return `create ${
|
|
2090
|
+
return `create ${write3.path} (${lines.length} lines)
|
|
1892
2091
|
${head.join("\n")}`;
|
|
1893
2092
|
}
|
|
1894
|
-
if (
|
|
1895
|
-
const before =
|
|
1896
|
-
const after =
|
|
2093
|
+
if (write3.existing === write3.contents) return `${write3.path} is already exactly this.`;
|
|
2094
|
+
const before = write3.existing.split("\n");
|
|
2095
|
+
const after = write3.contents.split("\n");
|
|
1897
2096
|
const changes = [];
|
|
1898
2097
|
let start = 0;
|
|
1899
2098
|
while (start < before.length && start < after.length && before[start] === after[start]) {
|
|
@@ -1910,7 +2109,7 @@ ${head.join("\n")}`;
|
|
|
1910
2109
|
for (const line of added.slice(0, maxLines)) changes.push(`+ ${line}`);
|
|
1911
2110
|
if (added.length > maxLines) changes.push(` \u2026 ${added.length - maxLines} more added`);
|
|
1912
2111
|
return [
|
|
1913
|
-
`edit ${
|
|
2112
|
+
`edit ${write3.path} (line ${start + 1}: -${removed.length} +${added.length})`,
|
|
1914
2113
|
...changes
|
|
1915
2114
|
].join("\n");
|
|
1916
2115
|
}
|
|
@@ -1940,19 +2139,41 @@ async function answer(context, apiUrl, token, roots, request) {
|
|
|
1940
2139
|
return { ok: false, error: error instanceof Error ? error.message : "refused" };
|
|
1941
2140
|
}
|
|
1942
2141
|
let bytes;
|
|
2142
|
+
let filename = request.path.split("/").pop() ?? "file";
|
|
2143
|
+
if (request.kind === "list_dir") {
|
|
2144
|
+
try {
|
|
2145
|
+
const stats = statSync2(located);
|
|
2146
|
+
if (!stats.isDirectory()) return { ok: false, error: `${request.path} is not a folder.` };
|
|
2147
|
+
const entries = readdirSync(located, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).slice(0, MAX_LISTED).map((entry) => {
|
|
2148
|
+
if (entry.isDirectory()) return `${entry.name}/`;
|
|
2149
|
+
try {
|
|
2150
|
+
return `${entry.name} ${sizeOf(join4(located, entry.name))}`;
|
|
2151
|
+
} catch {
|
|
2152
|
+
return entry.name;
|
|
2153
|
+
}
|
|
2154
|
+
}).sort();
|
|
2155
|
+
const listing = entries.length > 0 ? entries.join("\n") : "(empty)";
|
|
2156
|
+
bytes = Buffer.from(`${request.path}
|
|
2157
|
+
|
|
2158
|
+
${listing}
|
|
2159
|
+
`, "utf8");
|
|
2160
|
+
filename = `${request.path.split("/").filter(Boolean).pop() ?? "listing"}.txt`;
|
|
2161
|
+
const grant = await upload(apiUrl, token, filename, bytes);
|
|
2162
|
+
return grant;
|
|
2163
|
+
} catch (error) {
|
|
2164
|
+
return { ok: false, error: error instanceof Error ? error.message : "could not list it" };
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
1943
2167
|
try {
|
|
1944
2168
|
const stats = statSync2(located);
|
|
1945
2169
|
if (!stats.isFile()) return { ok: false, error: `${request.path} is not a file.` };
|
|
1946
|
-
|
|
1947
|
-
return {
|
|
1948
|
-
ok: false,
|
|
1949
|
-
error: new TooLarge(request.path, stats.size, MAX_READ_BYTES).message
|
|
1950
|
-
};
|
|
1951
|
-
}
|
|
1952
|
-
bytes = readFileSync3(located);
|
|
2170
|
+
bytes = readFileSync4(located);
|
|
1953
2171
|
} catch (error) {
|
|
1954
2172
|
return { ok: false, error: error instanceof Error ? error.message : "could not read it" };
|
|
1955
2173
|
}
|
|
2174
|
+
return upload(apiUrl, token, filename, bytes);
|
|
2175
|
+
}
|
|
2176
|
+
async function upload(apiUrl, token, filename, bytes) {
|
|
1956
2177
|
const grant = await fetch(`${apiUrl}/api/v1/agent/upload-url`, {
|
|
1957
2178
|
method: "POST",
|
|
1958
2179
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -1964,13 +2185,17 @@ async function answer(context, apiUrl, token, roots, request) {
|
|
|
1964
2185
|
// No content type is sent: this machine has a path, not a declaration.
|
|
1965
2186
|
// The server resolves it from the name against the one table that knows
|
|
1966
2187
|
// which types it can read, and tells us below what it decided.
|
|
1967
|
-
filename
|
|
2188
|
+
filename
|
|
1968
2189
|
})
|
|
1969
2190
|
});
|
|
1970
2191
|
if (!grant.ok) {
|
|
1971
2192
|
return { ok: false, error: await said(grant, "could not get an upload url") };
|
|
1972
2193
|
}
|
|
1973
|
-
const { uploadUrl, contentType } = await grant.json();
|
|
2194
|
+
const { uploadUrl, contentType, maxBytes } = await grant.json();
|
|
2195
|
+
const limit = maxBytes ?? MAX_TRANSFER_BYTES;
|
|
2196
|
+
if (bytes.length > limit) {
|
|
2197
|
+
return { ok: false, error: new TooLarge(filename, bytes.length, limit).message };
|
|
2198
|
+
}
|
|
1974
2199
|
const put = await fetch(uploadUrl, {
|
|
1975
2200
|
method: "PUT",
|
|
1976
2201
|
// The type the grant was signed for. Anything else is refused.
|
|
@@ -1982,6 +2207,13 @@ async function answer(context, apiUrl, token, roots, request) {
|
|
|
1982
2207
|
if (!stored.attachToken) return { ok: false, error: "the upload returned no reference" };
|
|
1983
2208
|
return { ok: true, attachToken: stored.attachToken, bytes: bytes.length };
|
|
1984
2209
|
}
|
|
2210
|
+
var MAX_LISTED = 200;
|
|
2211
|
+
function sizeOf(path) {
|
|
2212
|
+
const size = statSync2(path).size;
|
|
2213
|
+
if (size < 1024) return `${size} B`;
|
|
2214
|
+
if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`;
|
|
2215
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
2216
|
+
}
|
|
1985
2217
|
async function agentCommand(context) {
|
|
1986
2218
|
const credential = context.resolved.credential;
|
|
1987
2219
|
if (!credential) {
|
|
@@ -2091,7 +2323,10 @@ async function agentCommand(context) {
|
|
|
2091
2323
|
}
|
|
2092
2324
|
|
|
2093
2325
|
// src/commands/requests.ts
|
|
2326
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2327
|
+
import { resolve as resolve4 } from "node:path";
|
|
2094
2328
|
async function requestsCommand(context) {
|
|
2329
|
+
if (context.args.words[1] === "get") return collectCommand(context);
|
|
2095
2330
|
const credential = context.resolved.credential;
|
|
2096
2331
|
if (!credential) {
|
|
2097
2332
|
context.error("Sign in first: pm auth login");
|
|
@@ -2129,6 +2364,218 @@ async function requestsCommand(context) {
|
|
|
2129
2364
|
context.print("They cannot be approved from here \u2014 see `pm help requests`.");
|
|
2130
2365
|
return 0;
|
|
2131
2366
|
}
|
|
2367
|
+
async function collectCommand(context) {
|
|
2368
|
+
const credential = context.resolved.credential;
|
|
2369
|
+
if (!credential) {
|
|
2370
|
+
context.error("Sign in first: pm auth login");
|
|
2371
|
+
return 1;
|
|
2372
|
+
}
|
|
2373
|
+
const id = context.args.words[2];
|
|
2374
|
+
if (!id) {
|
|
2375
|
+
context.error("Which request? `pm requests` lists them with their ids.");
|
|
2376
|
+
return 2;
|
|
2377
|
+
}
|
|
2378
|
+
const apiUrl = context.resolved.apiUrl.replace(/\/+$/, "");
|
|
2379
|
+
const link = await fetch(
|
|
2380
|
+
`${apiUrl}/api/v1/agent/request/${encodeURIComponent(id)}/download`,
|
|
2381
|
+
{ headers: { authorization: `Bearer ${credential.token}` } }
|
|
2382
|
+
);
|
|
2383
|
+
if (!link.ok) {
|
|
2384
|
+
const body = await link.json().catch(() => void 0);
|
|
2385
|
+
context.error(body?.error?.message ?? `Could not prepare that file (${link.status}).`);
|
|
2386
|
+
return 1;
|
|
2387
|
+
}
|
|
2388
|
+
const { downloadUrl, filename } = await link.json();
|
|
2389
|
+
const file = await fetch(downloadUrl);
|
|
2390
|
+
if (!file.ok) {
|
|
2391
|
+
context.error(`The download refused it (${file.status}). Links expire in minutes \u2014 try again.`);
|
|
2392
|
+
return 1;
|
|
2393
|
+
}
|
|
2394
|
+
const name = stringFlag(context.args, "output", "o") ?? filename;
|
|
2395
|
+
const target = resolve4(name);
|
|
2396
|
+
if (existsSync4(target)) {
|
|
2397
|
+
context.error(`${target} already exists. Pass --output to write somewhere else.`);
|
|
2398
|
+
return 1;
|
|
2399
|
+
}
|
|
2400
|
+
writeFileSync4(target, new Uint8Array(await file.arrayBuffer()));
|
|
2401
|
+
context.print(`Wrote ${target}`);
|
|
2402
|
+
return 0;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
// src/workspace.ts
|
|
2406
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2407
|
+
import { dirname as dirname3, join as join5, resolve as resolvePath } from "node:path";
|
|
2408
|
+
var WORKSPACE_FILE = ".persistmemory.json";
|
|
2409
|
+
function findWorkspace(from = process.cwd()) {
|
|
2410
|
+
let dir = resolvePath(from);
|
|
2411
|
+
for (; ; ) {
|
|
2412
|
+
const file = join5(dir, WORKSPACE_FILE);
|
|
2413
|
+
if (existsSync5(file)) {
|
|
2414
|
+
const config = readWorkspace(file);
|
|
2415
|
+
if (config) return { file, dir, config };
|
|
2416
|
+
}
|
|
2417
|
+
const parent = dirname3(dir);
|
|
2418
|
+
if (parent === dir) return void 0;
|
|
2419
|
+
dir = parent;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
function readWorkspace(file) {
|
|
2423
|
+
try {
|
|
2424
|
+
const parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
2425
|
+
const space = parsed.space;
|
|
2426
|
+
if (space && typeof space === "object" && typeof space.id === "string" && space.id !== "" && typeof space.name === "string") {
|
|
2427
|
+
return { space: { id: space.id, name: space.name } };
|
|
2428
|
+
}
|
|
2429
|
+
return {};
|
|
2430
|
+
} catch {
|
|
2431
|
+
return void 0;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
function writeWorkspace(dir, config) {
|
|
2435
|
+
const file = join5(dir, WORKSPACE_FILE);
|
|
2436
|
+
writeFileSync5(file, `${JSON.stringify(config, null, 2)}
|
|
2437
|
+
`, "utf8");
|
|
2438
|
+
return file;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// src/commands/setup.ts
|
|
2442
|
+
async function setupCommand(context) {
|
|
2443
|
+
const { print, error } = context;
|
|
2444
|
+
print("");
|
|
2445
|
+
print(" PersistMemory setup");
|
|
2446
|
+
print("");
|
|
2447
|
+
if (!context.resolved.credential) {
|
|
2448
|
+
print(" You are not signed in yet. Opening your browser.");
|
|
2449
|
+
print("");
|
|
2450
|
+
const code = await login(context);
|
|
2451
|
+
if (code !== 0) {
|
|
2452
|
+
error("Setup stopped: signing in did not finish.");
|
|
2453
|
+
return code;
|
|
2454
|
+
}
|
|
2455
|
+
} else {
|
|
2456
|
+
const how = context.resolved.credential.kind === "api-key" ? "an API key" : "your browser sign-in";
|
|
2457
|
+
print(` Already signed in to ${context.resolved.apiUrl} with ${how}.`);
|
|
2458
|
+
}
|
|
2459
|
+
let client;
|
|
2460
|
+
let spaces;
|
|
2461
|
+
try {
|
|
2462
|
+
client = await context.client();
|
|
2463
|
+
const page = await client.spaces.list({ limit: 200 }).first();
|
|
2464
|
+
spaces = page.data;
|
|
2465
|
+
} catch (caught) {
|
|
2466
|
+
error(
|
|
2467
|
+
`Signed in, but the server would not answer: ${caught instanceof Error ? caught.message : String(caught)}`
|
|
2468
|
+
);
|
|
2469
|
+
error("Run `pm auth login` to sign in again.");
|
|
2470
|
+
return 1;
|
|
2471
|
+
}
|
|
2472
|
+
print(" Signed in and the server answered. \u2713");
|
|
2473
|
+
print("");
|
|
2474
|
+
return chooseSpace(context, client, spaces);
|
|
2475
|
+
}
|
|
2476
|
+
async function chooseSpace(context, client, spaces) {
|
|
2477
|
+
const { print, error } = context;
|
|
2478
|
+
const found = findWorkspace();
|
|
2479
|
+
const current = found?.config.space;
|
|
2480
|
+
const named = stringFlag(context.args, "space");
|
|
2481
|
+
const creating = stringFlag(context.args, "new-space");
|
|
2482
|
+
if (creating !== void 0) {
|
|
2483
|
+
const space = await create(client, creating);
|
|
2484
|
+
return finish(context, space, current);
|
|
2485
|
+
}
|
|
2486
|
+
if (named !== void 0) {
|
|
2487
|
+
const match = byName(spaces, named);
|
|
2488
|
+
if (!match) {
|
|
2489
|
+
error(`No Space called "${named}". Use --new-space to create it.`);
|
|
2490
|
+
return 1;
|
|
2491
|
+
}
|
|
2492
|
+
return finish(context, match, current);
|
|
2493
|
+
}
|
|
2494
|
+
const interactive = context.args.flags["yes"] !== true && context.isTty;
|
|
2495
|
+
if (!interactive) {
|
|
2496
|
+
print(" No Space chosen. Memories will go to your account's default.");
|
|
2497
|
+
print(` Pass --space "<name>" or --new-space "<name>" to pick one.`);
|
|
2498
|
+
return 0;
|
|
2499
|
+
}
|
|
2500
|
+
if (current) {
|
|
2501
|
+
print(` This folder currently uses the Space "${current.name}".`);
|
|
2502
|
+
print("");
|
|
2503
|
+
}
|
|
2504
|
+
print(" Which Space should this folder use?");
|
|
2505
|
+
print("");
|
|
2506
|
+
spaces.forEach((space, index) => {
|
|
2507
|
+
const mark = current?.id === space.id ? " (current)" : "";
|
|
2508
|
+
const count = space.memoryCount === void 0 ? "" : `, ${space.memoryCount} memories`;
|
|
2509
|
+
print(` ${index + 1}. ${space.name}${mark} ${space.kind}${count}`);
|
|
2510
|
+
});
|
|
2511
|
+
const createIndex = spaces.length + 1;
|
|
2512
|
+
const noneIndex = spaces.length + 2;
|
|
2513
|
+
print(` ${createIndex}. Create a new Space`);
|
|
2514
|
+
print(` ${noneIndex}. No Space \u2014 use everything in my account`);
|
|
2515
|
+
print("");
|
|
2516
|
+
const fallback = current ? "keep the current one" : String(createIndex);
|
|
2517
|
+
const answer3 = await context.ask(` Choose 1-${noneIndex} [${fallback}]: `);
|
|
2518
|
+
if (answer3 === "") {
|
|
2519
|
+
if (current) {
|
|
2520
|
+
print("");
|
|
2521
|
+
print(` Keeping "${current.name}".`);
|
|
2522
|
+
return 0;
|
|
2523
|
+
}
|
|
2524
|
+
return finish(context, await askForNewSpace(context, client), current);
|
|
2525
|
+
}
|
|
2526
|
+
const choice = Number(answer3);
|
|
2527
|
+
if (!Number.isInteger(choice) || choice < 1 || choice > noneIndex) {
|
|
2528
|
+
error(`"${answer3}" is not one of the choices. Nothing was changed.`);
|
|
2529
|
+
return 2;
|
|
2530
|
+
}
|
|
2531
|
+
if (choice === noneIndex) {
|
|
2532
|
+
if (found) {
|
|
2533
|
+
writeWorkspace(found.dir, {});
|
|
2534
|
+
print("");
|
|
2535
|
+
print(` Cleared the Space in ${WORKSPACE_FILE}. This folder now uses everything.`);
|
|
2536
|
+
} else {
|
|
2537
|
+
print("");
|
|
2538
|
+
print(" No Space. This folder uses everything in your account.");
|
|
2539
|
+
}
|
|
2540
|
+
return 0;
|
|
2541
|
+
}
|
|
2542
|
+
if (choice === createIndex) {
|
|
2543
|
+
return finish(context, await askForNewSpace(context, client), current);
|
|
2544
|
+
}
|
|
2545
|
+
return finish(context, spaces[choice - 1], current);
|
|
2546
|
+
}
|
|
2547
|
+
async function askForNewSpace(context, client) {
|
|
2548
|
+
for (; ; ) {
|
|
2549
|
+
const name = await context.ask(" Name for the new Space: ");
|
|
2550
|
+
if (name !== "") return create(client, name);
|
|
2551
|
+
context.print(" A Space needs a name.");
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
async function create(client, name) {
|
|
2555
|
+
return client.spaces.create({ name, kind: "project" });
|
|
2556
|
+
}
|
|
2557
|
+
function byName(spaces, name) {
|
|
2558
|
+
const wanted2 = name.trim().toLowerCase();
|
|
2559
|
+
return spaces.find((one) => one.name.trim().toLowerCase() === wanted2) ?? spaces.find((one) => one.id === name);
|
|
2560
|
+
}
|
|
2561
|
+
function finish(context, space, previous) {
|
|
2562
|
+
const file = writeWorkspace(process.cwd(), {
|
|
2563
|
+
space: { id: space.id, name: space.name }
|
|
2564
|
+
});
|
|
2565
|
+
context.print("");
|
|
2566
|
+
context.print(` This folder now uses the Space "${space.name}".`);
|
|
2567
|
+
if (previous && previous.id !== space.id) {
|
|
2568
|
+
context.print(` It used to use "${previous.name}". Existing memories were not moved.`);
|
|
2569
|
+
}
|
|
2570
|
+
context.print(` Saved to ${file}`);
|
|
2571
|
+
context.print("");
|
|
2572
|
+
context.print(" Try it:");
|
|
2573
|
+
context.print("");
|
|
2574
|
+
context.print(' pm remember "we chose Postgres for the ledger"');
|
|
2575
|
+
context.print(' pm search "what did we choose"');
|
|
2576
|
+
context.print("");
|
|
2577
|
+
return 0;
|
|
2578
|
+
}
|
|
2132
2579
|
|
|
2133
2580
|
// src/commands/auth.ts
|
|
2134
2581
|
async function authCommand(context) {
|
|
@@ -2181,12 +2628,25 @@ async function login(context) {
|
|
|
2181
2628
|
});
|
|
2182
2629
|
context.print(`
|
|
2183
2630
|
Signed in to ${apiUrl} as profile "${profile}".`);
|
|
2184
|
-
return 0;
|
|
2631
|
+
if (context.args.flags["no-setup"] === true || !context.isTty) return 0;
|
|
2632
|
+
return continueToSpace(context);
|
|
2185
2633
|
} catch (error) {
|
|
2186
2634
|
context.error(message(error));
|
|
2187
2635
|
return 1;
|
|
2188
2636
|
}
|
|
2189
2637
|
}
|
|
2638
|
+
async function continueToSpace(context) {
|
|
2639
|
+
try {
|
|
2640
|
+
const client = await context.client();
|
|
2641
|
+
const { data } = await client.spaces.list({ limit: 200 }).first();
|
|
2642
|
+
return await chooseSpace(context, client, data);
|
|
2643
|
+
} catch (caught) {
|
|
2644
|
+
context.print("");
|
|
2645
|
+
context.print(`Could not load your Spaces: ${message(caught)}`);
|
|
2646
|
+
context.print("You are signed in. Run `pm setup` to choose a Space.");
|
|
2647
|
+
return 0;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2190
2650
|
function logout(context) {
|
|
2191
2651
|
const profile = context.flags.profile ?? context.resolved.profile;
|
|
2192
2652
|
const forgotten = clearLogin(context.paths, profile);
|
|
@@ -2241,18 +2701,110 @@ function message(error) {
|
|
|
2241
2701
|
return error instanceof Error ? error.message : String(error);
|
|
2242
2702
|
}
|
|
2243
2703
|
|
|
2704
|
+
// src/commands/maintain.ts
|
|
2705
|
+
import { existsSync as existsSync6, rmSync } from "node:fs";
|
|
2706
|
+
import { spawnSync } from "node:child_process";
|
|
2707
|
+
import { dirname as dirname4, resolve as resolve6 } from "node:path";
|
|
2708
|
+
import { fileURLToPath } from "node:url";
|
|
2709
|
+
async function updateCommand(context) {
|
|
2710
|
+
const manager = installer();
|
|
2711
|
+
if (!manager) {
|
|
2712
|
+
context.error(
|
|
2713
|
+
"This copy was not installed by npm, so `pm update` cannot replace it.\nRe-run the installer: curl -fsSL https://persistmemory.com/install.sh | sh"
|
|
2714
|
+
);
|
|
2715
|
+
return 1;
|
|
2716
|
+
}
|
|
2717
|
+
context.print(`Installing the newest ${PACKAGE}\u2026`);
|
|
2718
|
+
const result = spawnSync(manager, ["install", "-g", `${PACKAGE}@latest`], {
|
|
2719
|
+
stdio: "inherit"
|
|
2720
|
+
});
|
|
2721
|
+
if (result.status !== 0) {
|
|
2722
|
+
context.error(
|
|
2723
|
+
"That did not work. If it failed on permissions, do NOT re-run it with sudo \u2014\npoint npm at a directory you own instead:\n npm config set prefix ~/.npm-global\n export PATH=$HOME/.npm-global/bin:$PATH"
|
|
2724
|
+
);
|
|
2725
|
+
return result.status ?? 1;
|
|
2726
|
+
}
|
|
2727
|
+
context.print("Done. `pm --version` will show the new one.");
|
|
2728
|
+
return 0;
|
|
2729
|
+
}
|
|
2730
|
+
async function uninstallCommand(context) {
|
|
2731
|
+
const manager = installer();
|
|
2732
|
+
if (!manager) {
|
|
2733
|
+
context.error(
|
|
2734
|
+
`This copy was not installed by npm, so it cannot uninstall itself.
|
|
2735
|
+
Delete the file it runs from: ${processPath()}`
|
|
2736
|
+
);
|
|
2737
|
+
return 1;
|
|
2738
|
+
}
|
|
2739
|
+
const alsoData = context.args.flags["purge"] === true || (context.isTty ? /^y(es)?$/i.test(
|
|
2740
|
+
await context.ask(`Also delete your credentials and settings in ${context.paths.dir}? [y/N] `)
|
|
2741
|
+
) : false);
|
|
2742
|
+
const result = spawnSync(manager, ["uninstall", "-g", PACKAGE], { stdio: "inherit" });
|
|
2743
|
+
if (result.status !== 0) return result.status ?? 1;
|
|
2744
|
+
if (alsoData) removeEverything(context);
|
|
2745
|
+
context.print("Removed. Your memories are untouched \u2014 this only removed the program.");
|
|
2746
|
+
return 0;
|
|
2747
|
+
}
|
|
2748
|
+
async function deleteCommand(context) {
|
|
2749
|
+
if (!existsSync6(context.paths.dir)) {
|
|
2750
|
+
context.print(`Nothing to delete: ${context.paths.dir} does not exist.`);
|
|
2751
|
+
return 0;
|
|
2752
|
+
}
|
|
2753
|
+
if (context.args.flags["yes"] !== true) {
|
|
2754
|
+
if (!context.isTty) {
|
|
2755
|
+
context.error("Refusing to delete without confirmation. Pass --yes.");
|
|
2756
|
+
return 2;
|
|
2757
|
+
}
|
|
2758
|
+
context.print("");
|
|
2759
|
+
context.print(`This deletes everything in ${context.paths.dir}:`);
|
|
2760
|
+
context.print(" \u2022 the credential you signed in with");
|
|
2761
|
+
context.print(" \u2022 your profiles and settings");
|
|
2762
|
+
context.print(" \u2022 transcripts of your `pm` sessions");
|
|
2763
|
+
context.print("");
|
|
2764
|
+
context.print("Your account and your memories are NOT touched. To delete those,");
|
|
2765
|
+
context.print("go to https://persistmemory.com/settings.");
|
|
2766
|
+
context.print("");
|
|
2767
|
+
const answer3 = await context.ask("Type 'delete' to confirm: ");
|
|
2768
|
+
if (answer3.trim().toLowerCase() !== "delete") {
|
|
2769
|
+
context.print("Nothing was deleted.");
|
|
2770
|
+
return 1;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
removeEverything(context);
|
|
2774
|
+
context.print(`Deleted ${context.paths.dir}.`);
|
|
2775
|
+
return 0;
|
|
2776
|
+
}
|
|
2777
|
+
function removeEverything(context) {
|
|
2778
|
+
const dir = resolve6(context.paths.dir);
|
|
2779
|
+
if (dir === "/" || dir.split("/").filter(Boolean).length < 2) {
|
|
2780
|
+
context.error(`Refusing to delete ${dir}: that does not look like a data directory.`);
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2784
|
+
}
|
|
2785
|
+
function installer() {
|
|
2786
|
+
return processPath().includes(`node_modules`) ? "npm" : void 0;
|
|
2787
|
+
}
|
|
2788
|
+
function processPath() {
|
|
2789
|
+
try {
|
|
2790
|
+
return resolve6(dirname4(fileURLToPath(import.meta.url)));
|
|
2791
|
+
} catch {
|
|
2792
|
+
return process.argv[1] ?? "";
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2244
2796
|
// src/commands/session.ts
|
|
2245
2797
|
import { createInterface as createInterface2 } from "node:readline";
|
|
2246
2798
|
import { randomUUID } from "node:crypto";
|
|
2247
2799
|
import { relative as relative2 } from "node:path";
|
|
2248
2800
|
|
|
2249
2801
|
// src/events.ts
|
|
2250
|
-
import { appendFileSync, existsSync as
|
|
2251
|
-
import { join as
|
|
2802
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
2803
|
+
import { join as join6 } from "node:path";
|
|
2252
2804
|
function openSessionLog(paths, id) {
|
|
2253
|
-
const directory =
|
|
2254
|
-
|
|
2255
|
-
const path =
|
|
2805
|
+
const directory = join6(paths.dir, "sessions");
|
|
2806
|
+
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
2807
|
+
const path = join6(directory, `${id}.jsonl`);
|
|
2256
2808
|
return {
|
|
2257
2809
|
id,
|
|
2258
2810
|
path,
|
|
@@ -2264,8 +2816,8 @@ function openSessionLog(paths, id) {
|
|
|
2264
2816
|
}
|
|
2265
2817
|
},
|
|
2266
2818
|
read() {
|
|
2267
|
-
if (!
|
|
2268
|
-
return
|
|
2819
|
+
if (!existsSync7(path)) return [];
|
|
2820
|
+
return readFileSync6(path, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
|
|
2269
2821
|
try {
|
|
2270
2822
|
return [JSON.parse(line)];
|
|
2271
2823
|
} catch {
|
|
@@ -2342,9 +2894,9 @@ async function sessionCommand(context) {
|
|
|
2342
2894
|
context.print(` Ask anything. /help for commands, /exit to leave.
|
|
2343
2895
|
`);
|
|
2344
2896
|
const readline = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2345
|
-
const ask = (prompt) => new Promise((
|
|
2346
|
-
readline.question(prompt,
|
|
2347
|
-
readline.once("close", () =>
|
|
2897
|
+
const ask = (prompt) => new Promise((resolve7) => {
|
|
2898
|
+
readline.question(prompt, resolve7);
|
|
2899
|
+
readline.once("close", () => resolve7(void 0));
|
|
2348
2900
|
});
|
|
2349
2901
|
const root = process.cwd();
|
|
2350
2902
|
let running = true;
|
|
@@ -2444,7 +2996,7 @@ async function handleInput(args) {
|
|
|
2444
2996
|
return true;
|
|
2445
2997
|
}
|
|
2446
2998
|
case "write":
|
|
2447
|
-
await
|
|
2999
|
+
await write2({ ...args, path: argument });
|
|
2448
3000
|
return true;
|
|
2449
3001
|
default:
|
|
2450
3002
|
context.error(` Unknown command /${command ?? ""}. Try /help.`);
|
|
@@ -2495,7 +3047,7 @@ ${reply}
|
|
|
2495
3047
|
context.error(" Note: answered without the semantic index, so this may be narrower than usual.");
|
|
2496
3048
|
}
|
|
2497
3049
|
}
|
|
2498
|
-
async function
|
|
3050
|
+
async function write2(args) {
|
|
2499
3051
|
const { context, state, log, root } = args;
|
|
2500
3052
|
if (!args.path) {
|
|
2501
3053
|
context.error(" /write needs a path.");
|
|
@@ -2528,7 +3080,43 @@ async function write(args) {
|
|
|
2528
3080
|
}
|
|
2529
3081
|
|
|
2530
3082
|
// src/commands/memory.ts
|
|
2531
|
-
import { readFileSync as
|
|
3083
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
3084
|
+
|
|
3085
|
+
// src/spaces.ts
|
|
3086
|
+
async function spacesFor(context, client, env = process.env) {
|
|
3087
|
+
const asked = listFlag(context.args, "space", "spaces") ?? splitList(env["PERSISTMEMORY_SPACE"]) ?? workspaceSpace();
|
|
3088
|
+
if (!asked || asked.length === 0) return void 0;
|
|
3089
|
+
if (asked.every(looksLikeId)) return asked;
|
|
3090
|
+
const { data } = await client.spaces.list({ limit: 200 }).first();
|
|
3091
|
+
return asked.map((one) => looksLikeId(one) ? one : resolveName(one, data));
|
|
3092
|
+
}
|
|
3093
|
+
function workspaceSpace() {
|
|
3094
|
+
const found = findWorkspace();
|
|
3095
|
+
return found?.config.space ? [found.config.space.id] : void 0;
|
|
3096
|
+
}
|
|
3097
|
+
function splitList(raw) {
|
|
3098
|
+
if (!raw) return void 0;
|
|
3099
|
+
const items = raw.split(",").map((one) => one.trim()).filter((one) => one.length > 0);
|
|
3100
|
+
return items.length > 0 ? items : void 0;
|
|
3101
|
+
}
|
|
3102
|
+
function looksLikeId(value) {
|
|
3103
|
+
return value.startsWith("space_");
|
|
3104
|
+
}
|
|
3105
|
+
function resolveName(name, spaces) {
|
|
3106
|
+
const wanted2 = name.trim().toLowerCase();
|
|
3107
|
+
const matches = spaces.filter((one) => one.name.trim().toLowerCase() === wanted2);
|
|
3108
|
+
if (matches.length === 1) return matches[0].id;
|
|
3109
|
+
if (matches.length === 0) {
|
|
3110
|
+
throw new Error(
|
|
3111
|
+
`No Space called "${name}". Run \`pm list spaces\` to see yours, or \`pm spaces create "${name}"\` to make it.`
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
throw new Error(
|
|
3115
|
+
`More than one Space is called "${name}": ${matches.map((one) => one.id).join(", ")}. Name it by id.`
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// src/commands/memory.ts
|
|
2532
3120
|
var memoryColumns = [
|
|
2533
3121
|
{ header: "id", value: (m) => m.id },
|
|
2534
3122
|
{ header: "type", value: (m) => m.type },
|
|
@@ -2556,7 +3144,7 @@ async function rememberCommand(context) {
|
|
|
2556
3144
|
let text;
|
|
2557
3145
|
if (file) {
|
|
2558
3146
|
try {
|
|
2559
|
-
text =
|
|
3147
|
+
text = readFileSync7(file, "utf8");
|
|
2560
3148
|
} catch {
|
|
2561
3149
|
context.error(`Could not read ${file}.`);
|
|
2562
3150
|
return 1;
|
|
@@ -2571,7 +3159,7 @@ async function rememberCommand(context) {
|
|
|
2571
3159
|
return 2;
|
|
2572
3160
|
}
|
|
2573
3161
|
const client = await context.client();
|
|
2574
|
-
const spaceIds =
|
|
3162
|
+
const spaceIds = await spacesFor(context, client);
|
|
2575
3163
|
const title = stringFlag(context.args, "title");
|
|
2576
3164
|
const result = await client.memories.remember(
|
|
2577
3165
|
{
|
|
@@ -2615,7 +3203,7 @@ async function searchCommand(context) {
|
|
|
2615
3203
|
return 2;
|
|
2616
3204
|
}
|
|
2617
3205
|
const client = await context.client();
|
|
2618
|
-
const spaceIds =
|
|
3206
|
+
const spaceIds = await spacesFor(context, client);
|
|
2619
3207
|
const response = await client.search.query({
|
|
2620
3208
|
query,
|
|
2621
3209
|
...limit !== void 0 ? { limit } : {},
|
|
@@ -2652,10 +3240,11 @@ async function listMemoriesCommand(context) {
|
|
|
2652
3240
|
return 2;
|
|
2653
3241
|
}
|
|
2654
3242
|
const client = await context.client();
|
|
3243
|
+
const spaceIds = await spacesFor(context, client);
|
|
2655
3244
|
const page = client.memories.list({
|
|
2656
3245
|
...limit !== void 0 ? { limit } : {},
|
|
2657
3246
|
...listFlag(context.args, "type") ? { type: listFlag(context.args, "type") } : {},
|
|
2658
|
-
...
|
|
3247
|
+
...spaceIds ? { spaceIds: [...spaceIds] } : {}
|
|
2659
3248
|
});
|
|
2660
3249
|
const rows = context.args.flags["all"] ? await page.all(limit ?? 1e3) : (await page.first()).data;
|
|
2661
3250
|
if (rows.length === 0) {
|
|
@@ -2711,6 +3300,36 @@ function hash(text) {
|
|
|
2711
3300
|
}
|
|
2712
3301
|
return value.toString(16).padStart(8, "0");
|
|
2713
3302
|
}
|
|
3303
|
+
async function createSpaceCommand(context) {
|
|
3304
|
+
const name = context.args.words.slice(2).join(" ").trim();
|
|
3305
|
+
if (name === "") {
|
|
3306
|
+
context.error('A Space needs a name. Try `pm spaces create "Acme"`.');
|
|
3307
|
+
return 2;
|
|
3308
|
+
}
|
|
3309
|
+
const kind = stringFlag(context.args, "kind") ?? "project";
|
|
3310
|
+
const description = stringFlag(context.args, "description");
|
|
3311
|
+
const client = await context.client();
|
|
3312
|
+
const space = await client.spaces.create({
|
|
3313
|
+
name,
|
|
3314
|
+
kind,
|
|
3315
|
+
...description ? { description } : {}
|
|
3316
|
+
});
|
|
3317
|
+
if (context.flags.output !== "table") {
|
|
3318
|
+
context.print(renderOne(space, spaceFields, { format: context.flags.output }));
|
|
3319
|
+
return 0;
|
|
3320
|
+
}
|
|
3321
|
+
context.print(`Created "${space.name}".`);
|
|
3322
|
+
context.print(` id ${space.id}`);
|
|
3323
|
+
context.print(` kind ${space.kind}`);
|
|
3324
|
+
context.print("");
|
|
3325
|
+
context.print(`Use it here with: pm setup --space "${space.name}"`);
|
|
3326
|
+
return 0;
|
|
3327
|
+
}
|
|
3328
|
+
var spaceFields = [
|
|
3329
|
+
{ header: "id", value: (s) => s.id },
|
|
3330
|
+
{ header: "name", value: (s) => s.name },
|
|
3331
|
+
{ header: "kind", value: (s) => s.kind }
|
|
3332
|
+
];
|
|
2714
3333
|
|
|
2715
3334
|
// src/index.ts
|
|
2716
3335
|
async function run(deps) {
|
|
@@ -2762,7 +3381,9 @@ async function run(deps) {
|
|
|
2762
3381
|
client: () => clientFor(resolved, session),
|
|
2763
3382
|
print,
|
|
2764
3383
|
error,
|
|
2765
|
-
readSecret: deps.readSecret ?? readSecretFromTty
|
|
3384
|
+
readSecret: deps.readSecret ?? readSecretFromTty,
|
|
3385
|
+
ask: deps.ask ?? askOnTty,
|
|
3386
|
+
isTty: deps.isTty ?? process.stdin.isTTY ?? false
|
|
2766
3387
|
};
|
|
2767
3388
|
if (args.words.length === 0) {
|
|
2768
3389
|
if (!(deps.isTty ?? process.stdin.isTTY ?? false)) {
|
|
@@ -2772,16 +3393,52 @@ async function run(deps) {
|
|
|
2772
3393
|
return sessionCommand(context);
|
|
2773
3394
|
}
|
|
2774
3395
|
try {
|
|
2775
|
-
|
|
3396
|
+
await offerUpdate(context, deps);
|
|
3397
|
+
const code = await dispatch(context);
|
|
3398
|
+
void refreshUpdateCache(updateDeps(context, deps));
|
|
3399
|
+
return code;
|
|
2776
3400
|
} catch (caught) {
|
|
2777
3401
|
return report(caught, error);
|
|
2778
3402
|
}
|
|
2779
3403
|
}
|
|
3404
|
+
async function offerUpdate(context, deps) {
|
|
3405
|
+
const verb = context.args.words[0];
|
|
3406
|
+
if (verb === "update" || verb === "upgrade" || verb === "uninstall") return;
|
|
3407
|
+
const notice = updateNotice(updateDeps(context, deps));
|
|
3408
|
+
if (!notice) return;
|
|
3409
|
+
const env = deps.env ?? process.env;
|
|
3410
|
+
if (env["PERSISTMEMORY_AUTO_UPDATE"]) {
|
|
3411
|
+
context.print(notice.split("\n")[0] ?? "");
|
|
3412
|
+
await updateCommand(context);
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
context.error(notice);
|
|
3416
|
+
}
|
|
3417
|
+
function updateDeps(context, deps) {
|
|
3418
|
+
return {
|
|
3419
|
+
file: updateCacheFile(context.paths.dir),
|
|
3420
|
+
current: VERSION,
|
|
3421
|
+
...deps.fetch ? { fetch: deps.fetch } : {},
|
|
3422
|
+
...deps.env ? { env: deps.env } : {},
|
|
3423
|
+
isTty: context.isTty,
|
|
3424
|
+
quiet: context.flags.quiet
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
2780
3427
|
async function dispatch(context) {
|
|
2781
3428
|
const [verb, noun] = context.args.words;
|
|
2782
3429
|
switch (verb) {
|
|
2783
3430
|
case "auth":
|
|
2784
3431
|
return authCommand(context);
|
|
3432
|
+
case "setup":
|
|
3433
|
+
case "init":
|
|
3434
|
+
return setupCommand(context);
|
|
3435
|
+
case "update":
|
|
3436
|
+
case "upgrade":
|
|
3437
|
+
return updateCommand(context);
|
|
3438
|
+
case "uninstall":
|
|
3439
|
+
return uninstallCommand(context);
|
|
3440
|
+
case "delete":
|
|
3441
|
+
return deleteCommand(context);
|
|
2785
3442
|
case "agent":
|
|
2786
3443
|
return agentCommand(context);
|
|
2787
3444
|
case "chat":
|
|
@@ -2803,6 +3460,12 @@ async function dispatch(context) {
|
|
|
2803
3460
|
* a consistent grammar is what lets a tool grow past the handful of
|
|
2804
3461
|
* commands anybody can memorise.
|
|
2805
3462
|
*/
|
|
3463
|
+
case "spaces":
|
|
3464
|
+
case "space":
|
|
3465
|
+
if (noun === "create" || noun === "new") return createSpaceCommand(context);
|
|
3466
|
+
if (noun === void 0 || noun === "list") return listSpacesCommand(context);
|
|
3467
|
+
context.error(`Cannot "pm spaces ${noun}". Try list or create.`);
|
|
3468
|
+
return 2;
|
|
2806
3469
|
case "list":
|
|
2807
3470
|
if (noun === "memories" || noun === "memory") return listMemoriesCommand(context);
|
|
2808
3471
|
if (noun === "spaces" || noun === "space") return listSpacesCommand(context);
|