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