@persistmemory/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2853 @@
1
+ // ../sdk-js/dist/index.js
2
+ function encodeQuery(params) {
3
+ if (!params) return "";
4
+ const search = new URLSearchParams();
5
+ for (const [key, value] of Object.entries(params)) {
6
+ if (value === void 0) continue;
7
+ if (Array.isArray(value)) {
8
+ for (const one of value) search.append(key, String(one));
9
+ continue;
10
+ }
11
+ search.append(key, String(value));
12
+ }
13
+ const encoded = search.toString();
14
+ return encoded ? `?${encoded}` : "";
15
+ }
16
+ function pageQuery(params, cursor, extra) {
17
+ return {
18
+ ...params.limit !== void 0 ? { limit: params.limit } : {},
19
+ ...cursor !== void 0 ? { cursor } : params.cursor !== void 0 ? { cursor: params.cursor } : {},
20
+ ...extra
21
+ };
22
+ }
23
+ var DEFAULT_BACKOFF = { baseMs: 250, maxMs: 8e3, factor: 2, jitter: 1 };
24
+ function backoffMs(attempt, options = {}) {
25
+ const base = options.baseMs ?? DEFAULT_BACKOFF.baseMs;
26
+ const max = options.maxMs ?? DEFAULT_BACKOFF.maxMs;
27
+ const factor = options.factor ?? DEFAULT_BACKOFF.factor;
28
+ const jitter = clamp01(options.jitter ?? DEFAULT_BACKOFF.jitter);
29
+ const random = options.random ?? Math.random;
30
+ const exponent = Math.max(0, Math.floor(attempt) - 1);
31
+ const ceiling = Math.min(max, base * Math.pow(factor, exponent));
32
+ if (jitter <= 0) return Math.round(ceiling);
33
+ const fixed = ceiling * (1 - jitter);
34
+ return Math.round(fixed + random() * (ceiling - fixed));
35
+ }
36
+ function delayFor(args) {
37
+ const computed = backoffMs(args.attempt, args.options ?? {});
38
+ if (args.retryAfterSeconds === void 0 || !Number.isFinite(args.retryAfterSeconds)) {
39
+ return computed;
40
+ }
41
+ return Math.max(computed, Math.max(0, args.retryAfterSeconds) * 1e3);
42
+ }
43
+ function clamp01(value) {
44
+ if (!Number.isFinite(value)) return 0;
45
+ return Math.min(1, Math.max(0, value));
46
+ }
47
+ var PersistMemoryError = class extends Error {
48
+ status;
49
+ code;
50
+ fields;
51
+ requestId;
52
+ retryAfterSeconds;
53
+ /** Retrying this exact request could plausibly succeed. */
54
+ retryable = false;
55
+ constructor(init) {
56
+ super(redact(init.message));
57
+ this.name = new.target.name;
58
+ this.status = init.status;
59
+ this.code = init.code;
60
+ if (init.fields) this.fields = init.fields;
61
+ if (init.requestId) this.requestId = init.requestId;
62
+ if (init.retryAfterSeconds !== void 0) this.retryAfterSeconds = init.retryAfterSeconds;
63
+ }
64
+ /**
65
+ * A one-line summary safe to log.
66
+ *
67
+ * Provided so callers reach for this instead of `JSON.stringify(error)`,
68
+ * which walks own properties and would pick up anything a future field
69
+ * holds. Everything here is already server-supplied and key-free.
70
+ */
71
+ toString() {
72
+ const id = this.requestId ? ` requestId=${this.requestId}` : "";
73
+ return `${this.name}: [${this.status} ${this.code}] ${this.message}${id}`;
74
+ }
75
+ };
76
+ var AuthenticationError = class extends PersistMemoryError {
77
+ };
78
+ var PermissionDeniedError = class extends PersistMemoryError {
79
+ };
80
+ var NotFoundError = class extends PersistMemoryError {
81
+ };
82
+ var ValidationError = class extends PersistMemoryError {
83
+ };
84
+ var ConflictError = class extends PersistMemoryError {
85
+ };
86
+ var RateLimitError = class extends PersistMemoryError {
87
+ retryable = true;
88
+ };
89
+ var ServerError = class extends PersistMemoryError {
90
+ retryable = true;
91
+ };
92
+ var ConnectionError = class extends PersistMemoryError {
93
+ retryable = true;
94
+ constructor(message2) {
95
+ super({ status: 0, code: "CONNECTION_ERROR", message: message2 });
96
+ }
97
+ };
98
+ var TimeoutError = class extends PersistMemoryError {
99
+ retryable = true;
100
+ constructor(message2) {
101
+ super({ status: 0, code: "TIMEOUT", message: message2 });
102
+ }
103
+ };
104
+ var AbortError = class extends PersistMemoryError {
105
+ constructor(message2 = "The request was aborted by the caller.") {
106
+ super({ status: 0, code: "ABORTED", message: message2 });
107
+ }
108
+ };
109
+ function errorFromResponse(status2, body, headers) {
110
+ const envelope = body ?? {};
111
+ const code = typeof envelope.error?.code === "string" ? envelope.error.code : codeForStatus(status2);
112
+ const message2 = typeof envelope.error?.message === "string" && envelope.error.message.length > 0 ? envelope.error.message : defaultMessage(status2);
113
+ const retryAfterSeconds = status2 === 429 || status2 === 503 ? retryAfterOf(headers) : void 0;
114
+ const init = {
115
+ status: status2,
116
+ code,
117
+ message: message2,
118
+ ...isFieldMap(envelope.error?.fields) ? { fields: envelope.error.fields } : {},
119
+ ...typeof envelope.error?.requestId === "string" ? { requestId: envelope.error.requestId } : {},
120
+ ...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
121
+ };
122
+ if (status2 === 429 || code === "RATE_LIMITED") return new RateLimitError(init);
123
+ if (status2 === 401 || code === "UNAUTHORIZED") return new AuthenticationError(init);
124
+ if (status2 === 403 || code === "FORBIDDEN") return new PermissionDeniedError(init);
125
+ if (status2 === 404 || code === "NOT_FOUND") return new NotFoundError(init);
126
+ if (status2 === 409 || code === "CONFLICT") return new ConflictError(init);
127
+ if (status2 === 400 || status2 === 413 || status2 === 422) return new ValidationError(init);
128
+ if (status2 >= 500) return new ServerError(init);
129
+ return new PersistMemoryError(init);
130
+ }
131
+ function retryAfterOf(headers) {
132
+ const raw = headers.get("retry-after");
133
+ if (!raw) return void 0;
134
+ const seconds = Number(raw.trim());
135
+ if (!Number.isFinite(seconds) || seconds < 0) return void 0;
136
+ return Math.min(seconds, 300);
137
+ }
138
+ function isFieldMap(value) {
139
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
140
+ return Object.values(value).every((one) => typeof one === "string");
141
+ }
142
+ function codeForStatus(status2) {
143
+ if (status2 === 401) return "UNAUTHORIZED";
144
+ if (status2 === 403) return "FORBIDDEN";
145
+ if (status2 === 404) return "NOT_FOUND";
146
+ if (status2 === 409) return "CONFLICT";
147
+ if (status2 === 429) return "RATE_LIMITED";
148
+ if (status2 === 413) return "PAYLOAD_TOO_LARGE";
149
+ if (status2 >= 500) return "INTERNAL_ERROR";
150
+ return "VALIDATION_ERROR";
151
+ }
152
+ function defaultMessage(status2) {
153
+ return `The API returned ${status2} with no readable error body.`;
154
+ }
155
+ function redact(text) {
156
+ return text.replace(/pm_(live|test)_[A-Za-z0-9_-]+/g, "pm_$1_[redacted]");
157
+ }
158
+ var DEFAULT_BASE_URL = "https://api.persistmemory.com";
159
+ var DEFAULT_TIMEOUT_MS = 3e4;
160
+ var DEFAULT_MAX_ATTEMPTS = 3;
161
+ var RETRY_WITHOUT_ASKING = /* @__PURE__ */ new Set(["GET", "HEAD", "PATCH", "DELETE"]);
162
+ var HttpClient = class {
163
+ /**
164
+ * The credential, in a private field, and never anywhere else.
165
+ *
166
+ * Private (`#`) rather than `readonly`: a public field is enumerable, so
167
+ * `JSON.stringify(client)` and every structured logger that walks own
168
+ * properties would write the key into a log line. `toJSON` and the inspect
169
+ * hook below close the two remaining paths - a bug report pasted from
170
+ * `console.log(client)` is exactly how a key gets shared with strangers.
171
+ */
172
+ #apiKey;
173
+ #baseUrl;
174
+ #fetch;
175
+ #timeoutMs;
176
+ #maxAttempts;
177
+ #backoff;
178
+ #sleep;
179
+ #userAgent;
180
+ constructor(options) {
181
+ if (typeof options.apiKey !== "string" || options.apiKey.trim().length === 0) {
182
+ throw new PersistMemoryError({
183
+ status: 0,
184
+ code: "VALIDATION_ERROR",
185
+ message: "An API key is required. Pass `apiKey`, or set PERSISTMEMORY_API_KEY."
186
+ });
187
+ }
188
+ const key = options.apiKey.trim();
189
+ if (/[\r\n\0]/.test(key)) {
190
+ throw new PersistMemoryError({
191
+ status: 0,
192
+ code: "VALIDATION_ERROR",
193
+ message: "The API key contains characters that cannot go in a header."
194
+ });
195
+ }
196
+ this.#apiKey = key;
197
+ this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
198
+ this.#fetch = options.fetch ?? globalThis.fetch;
199
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
200
+ this.#maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
201
+ this.#backoff = options.backoff ?? {};
202
+ this.#sleep = options.sleep ?? defaultSleep;
203
+ this.#userAgent = options.userAgent ?? "persistmemory-sdk-js/0.1.0";
204
+ }
205
+ /**
206
+ * What this object looks like when something serialises it.
207
+ *
208
+ * Both hooks return the same key-free shape. `toJSON` covers
209
+ * `JSON.stringify`, the inspect symbol covers `console.log` under Node, and
210
+ * between them they cover how a credential actually escapes: not through a
211
+ * deliberate log line, but through an object dumped into a bug report.
212
+ */
213
+ toJSON() {
214
+ return { baseUrl: this.#baseUrl, apiKey: "[redacted]" };
215
+ }
216
+ [Symbol.for("nodejs.util.inspect.custom")]() {
217
+ return this.toJSON();
218
+ }
219
+ async get(path, query, options) {
220
+ return this.#request({
221
+ method: "GET",
222
+ path,
223
+ ...query ? { query } : {},
224
+ ...options ? { options } : {}
225
+ });
226
+ }
227
+ async post(path, body, options) {
228
+ return this.#request({
229
+ method: "POST",
230
+ path,
231
+ ...body !== void 0 ? { body } : {},
232
+ ...options ? { options } : {}
233
+ });
234
+ }
235
+ async patch(path, body, options) {
236
+ return this.#request({
237
+ method: "PATCH",
238
+ path,
239
+ ...body !== void 0 ? { body } : {},
240
+ ...options ? { options } : {}
241
+ });
242
+ }
243
+ async delete(path, body, options) {
244
+ return this.#request({
245
+ method: "DELETE",
246
+ path,
247
+ ...body !== void 0 ? { body } : {},
248
+ ...options ? { options } : {}
249
+ });
250
+ }
251
+ async #request(request) {
252
+ const maxAttempts = Math.max(1, request.options?.maxAttempts ?? this.#maxAttempts);
253
+ const url = this.#baseUrl + request.path + encodeQuery(request.query);
254
+ let attempt = 0;
255
+ for (; ; ) {
256
+ attempt += 1;
257
+ let error;
258
+ try {
259
+ return await this.#attempt(request, url);
260
+ } catch (thrown) {
261
+ if (!(thrown instanceof PersistMemoryError)) throw thrown;
262
+ error = thrown;
263
+ }
264
+ if (attempt >= maxAttempts) throw error;
265
+ if (!mayRetry(error, request)) throw error;
266
+ const delay = delayFor({
267
+ attempt,
268
+ ...error.retryAfterSeconds !== void 0 ? { retryAfterSeconds: error.retryAfterSeconds } : {},
269
+ options: this.#backoff
270
+ });
271
+ await this.#sleep(delay, request.options?.signal);
272
+ }
273
+ }
274
+ async #attempt(request, url) {
275
+ const timeoutMs = request.options?.timeoutMs ?? this.#timeoutMs;
276
+ const deadline = new AbortController();
277
+ const timer = setTimeout(() => deadline.abort(), timeoutMs);
278
+ const onCallerAbort = () => deadline.abort();
279
+ const caller = request.options?.signal;
280
+ caller?.addEventListener("abort", onCallerAbort, { once: true });
281
+ try {
282
+ if (caller?.aborted) throw new AbortError();
283
+ const response = await untilAborted(
284
+ this.#fetch(url, {
285
+ method: request.method,
286
+ headers: this.#headers(request),
287
+ ...request.body !== void 0 ? { body: JSON.stringify(request.body) } : {},
288
+ signal: deadline.signal
289
+ }),
290
+ deadline.signal
291
+ );
292
+ const payload = await readBody(response);
293
+ if (!response.ok) throw errorFromResponse(response.status, payload, response.headers);
294
+ return payload;
295
+ } catch (thrown) {
296
+ if (thrown instanceof PersistMemoryError) throw thrown;
297
+ if (caller?.aborted) throw new AbortError();
298
+ if (deadline.signal.aborted) {
299
+ throw new TimeoutError(`The request did not complete within ${timeoutMs}ms.`);
300
+ }
301
+ const detail = thrown instanceof Error ? redact(thrown.message) : "transport failure";
302
+ throw new ConnectionError(`Could not reach the API: ${detail}`);
303
+ } finally {
304
+ clearTimeout(timer);
305
+ caller?.removeEventListener("abort", onCallerAbort);
306
+ }
307
+ }
308
+ #headers(request) {
309
+ return {
310
+ // The only place the key is ever read.
311
+ authorization: `Bearer ${this.#apiKey}`,
312
+ accept: "application/json",
313
+ "user-agent": this.#userAgent,
314
+ ...request.body !== void 0 ? { "content-type": "application/json" } : {},
315
+ ...request.options?.idempotencyKey ? { "idempotency-key": request.options.idempotencyKey } : {}
316
+ };
317
+ }
318
+ };
319
+ function mayRetry(error, request) {
320
+ if (!error.retryable) return false;
321
+ if (RETRY_WITHOUT_ASKING.has(request.method)) return true;
322
+ if (request.options?.idempotencyKey) return true;
323
+ return error.status === 429;
324
+ }
325
+ async function readBody(response) {
326
+ if (response.status === 204) return void 0;
327
+ const text = await response.text().catch(() => "");
328
+ if (text.length === 0) return void 0;
329
+ const type = response.headers.get("content-type") ?? "";
330
+ if (!type.includes("json")) return { raw: text.slice(0, 500) };
331
+ try {
332
+ return JSON.parse(text);
333
+ } catch {
334
+ return { raw: text.slice(0, 500) };
335
+ }
336
+ }
337
+ var SignalFired = class extends Error {
338
+ constructor() {
339
+ super("signal fired");
340
+ this.name = "SignalFired";
341
+ }
342
+ };
343
+ function untilAborted(work, signal) {
344
+ work.catch(() => void 0);
345
+ return new Promise((resolve5, reject) => {
346
+ if (signal.aborted) {
347
+ reject(new SignalFired());
348
+ return;
349
+ }
350
+ const onAbort = () => reject(new SignalFired());
351
+ signal.addEventListener("abort", onAbort, { once: true });
352
+ work.then(
353
+ (value) => {
354
+ signal.removeEventListener("abort", onAbort);
355
+ resolve5(value);
356
+ },
357
+ (error) => {
358
+ signal.removeEventListener("abort", onAbort);
359
+ reject(error instanceof Error ? error : new Error(String(error)));
360
+ }
361
+ );
362
+ });
363
+ }
364
+ function defaultSleep(ms, signal) {
365
+ return new Promise((resolve5, reject) => {
366
+ if (signal?.aborted) {
367
+ reject(new AbortError());
368
+ return;
369
+ }
370
+ const timer = setTimeout(() => {
371
+ signal?.removeEventListener("abort", onAbort);
372
+ resolve5();
373
+ }, ms);
374
+ function onAbort() {
375
+ clearTimeout(timer);
376
+ reject(new AbortError());
377
+ }
378
+ signal?.addEventListener("abort", onAbort, { once: true });
379
+ });
380
+ }
381
+ var Paginated = class {
382
+ #fetchPage;
383
+ constructor(fetchPage) {
384
+ this.#fetchPage = fetchPage;
385
+ }
386
+ /** The first page, and nothing more. For a UI that renders one page at a time. */
387
+ async first() {
388
+ return this.#fetchPage(void 0);
389
+ }
390
+ /**
391
+ * Page by page, for a caller that wants the cursors or wants to stop early.
392
+ *
393
+ * A generator rather than an array of pages: fetching them all up front
394
+ * would make "show me the first ten" cost every page in the account.
395
+ */
396
+ async *pages() {
397
+ let cursor;
398
+ const seen = /* @__PURE__ */ new Set();
399
+ for (; ; ) {
400
+ const page = await this.#fetchPage(cursor);
401
+ yield page;
402
+ const next = page.pagination.nextCursor;
403
+ if (!next) return;
404
+ if (seen.has(next)) return;
405
+ seen.add(next);
406
+ cursor = next;
407
+ }
408
+ }
409
+ /** Every item across every page. `for await (const memory of ...)`. */
410
+ async *[Symbol.asyncIterator]() {
411
+ for await (const page of this.pages()) {
412
+ for (const item of page.data) yield item;
413
+ }
414
+ }
415
+ /**
416
+ * Everything, in one array.
417
+ *
418
+ * `maxItems` is not optional, and that is the point. An unbounded `all()` on
419
+ * an account with two hundred thousand memories is a request loop that runs
420
+ * for minutes and an array that exhausts the heap, and the call site that
421
+ * does it reads as innocently as any other. Ask for a number you can hold.
422
+ */
423
+ async all(maxItems) {
424
+ const collected = [];
425
+ if (maxItems <= 0) return collected;
426
+ for await (const item of this) {
427
+ collected.push(item);
428
+ if (collected.length >= maxItems) break;
429
+ }
430
+ return collected;
431
+ }
432
+ };
433
+ var Memories = class {
434
+ #http;
435
+ constructor(http) {
436
+ this.#http = http;
437
+ }
438
+ /**
439
+ * One page, plus the cursor loop.
440
+ *
441
+ * Returns a `Paginated`, so `await memories.list().first()` gets a page and
442
+ * `for await (const memory of memories.list())` walks the lot. The filters
443
+ * are carried into every page automatically - a caller re-passing them per
444
+ * page is a caller who will eventually forget one, and the pages after that
445
+ * come from a differently filtered list.
446
+ */
447
+ list(params = {}, options) {
448
+ return new Paginated(
449
+ (cursor) => this.#http.get(
450
+ "/api/v1/memories",
451
+ pageQuery(params, cursor, toQuery(params)),
452
+ options
453
+ )
454
+ );
455
+ }
456
+ async get(id, options) {
457
+ return this.#http.get(`/api/v1/memories/${encodeURIComponent(id)}`, void 0, options);
458
+ }
459
+ /**
460
+ * Hands material to the ingestion pipeline. Needs a key with `write` scope.
461
+ *
462
+ * This does NOT create a memory, and the return type says so: it answers 202
463
+ * with a job id. Extraction, entity resolution, deduplication and conflict
464
+ * detection all run afterwards and may produce one memory, several, or none.
465
+ * Poll `client.jobs.get(result.jobId)` to find out which.
466
+ *
467
+ * Pass an `idempotencyKey` if this can be retried by anything - a queue, a
468
+ * user pressing a button twice, or this client's own retry loop, which
469
+ * refuses to repeat a POST without one. `remember:note-42`, not a fresh
470
+ * random value per call.
471
+ */
472
+ async remember(params, options) {
473
+ return this.#http.post("/api/v1/remember", params, options);
474
+ }
475
+ };
476
+ function toQuery(params) {
477
+ return {
478
+ ...params.type !== void 0 ? { type: params.type } : {},
479
+ ...params.state !== void 0 ? { state: params.state } : {},
480
+ ...params.scope !== void 0 ? { scope: params.scope } : {},
481
+ ...params.spaceIds !== void 0 ? { spaceIds: params.spaceIds } : {},
482
+ ...params.createdAfter !== void 0 ? { createdAfter: params.createdAfter } : {},
483
+ ...params.createdBefore !== void 0 ? { createdBefore: params.createdBefore } : {},
484
+ ...params.minConfidence !== void 0 ? { minConfidence: params.minConfidence } : {},
485
+ ...params.includeHistorical !== void 0 ? { includeHistorical: params.includeHistorical } : {}
486
+ };
487
+ }
488
+ var Search = class {
489
+ #http;
490
+ constructor(http) {
491
+ this.#http = http;
492
+ }
493
+ /**
494
+ * Ranked results, with an account of how they were found.
495
+ *
496
+ * Read `diagnostics.degraded` before showing the results. Search degrades
497
+ * rather than fails - with embeddings unavailable it falls back to
498
+ * deterministic retrieval and still answers - and a UI that cannot tell
499
+ * degraded from healthy tells the user the system knows nothing when it is
500
+ * merely looking with one eye.
501
+ */
502
+ async query(params, options) {
503
+ const query = {
504
+ query: params.query,
505
+ ...params.limit !== void 0 ? { limit: params.limit } : {},
506
+ ...params.scope !== void 0 ? { scope: params.scope } : {},
507
+ ...params.spaceIds !== void 0 ? { spaceIds: params.spaceIds } : {},
508
+ ...params.types !== void 0 ? { types: params.types } : {},
509
+ ...params.minScore !== void 0 ? { minScore: params.minScore } : {},
510
+ ...params.asOf !== void 0 ? { asOf: params.asOf } : {},
511
+ ...params.includeHistorical !== void 0 ? { includeHistorical: params.includeHistorical } : {},
512
+ ...params.includeEvidence !== void 0 ? { includeEvidence: params.includeEvidence } : {},
513
+ ...params.explain !== void 0 ? { explain: params.explain } : {}
514
+ };
515
+ return this.#http.get("/api/v1/search", query, options);
516
+ }
517
+ /**
518
+ * A context window, assembled and ready to paste into a prompt.
519
+ *
520
+ * A POST that is safe to repeat - it reads and returns, it writes nothing -
521
+ * so this is one of the few places where retrying without an idempotency key
522
+ * would be harmless. It still is not retried by default: the request loop
523
+ * decides by METHOD, and a per-endpoint exception is a rule that holds until
524
+ * someone adds a POST next to it that does write.
525
+ */
526
+ async context(params, options) {
527
+ return this.#http.post("/api/v1/context", params, options);
528
+ }
529
+ };
530
+ var Spaces = class {
531
+ #http;
532
+ constructor(http) {
533
+ this.#http = http;
534
+ }
535
+ list(params = {}, options) {
536
+ return new Paginated(
537
+ (cursor) => this.#http.get(
538
+ "/api/v1/spaces",
539
+ pageQuery(params, cursor, {
540
+ ...params.includeArchived !== void 0 ? { includeArchived: params.includeArchived } : {}
541
+ }),
542
+ options
543
+ )
544
+ );
545
+ }
546
+ async get(id, options) {
547
+ return this.#http.get(`/api/v1/spaces/${encodeURIComponent(id)}`, void 0, options);
548
+ }
549
+ /**
550
+ * Creates a Space. Answers 201.
551
+ *
552
+ * Worth an idempotency key when a person is behind it: a double-clicked
553
+ * "create Space" button makes two Spaces called Work, and nothing later can
554
+ * tell which of them memories should have gone into.
555
+ */
556
+ async create(params, options) {
557
+ return this.#http.post("/api/v1/spaces", params, options);
558
+ }
559
+ /** Renaming, retention, and archiving - `archived` is a field, not a verb. */
560
+ async update(id, params, options) {
561
+ return this.#http.patch(`/api/v1/spaces/${encodeURIComponent(id)}`, params, options);
562
+ }
563
+ /**
564
+ * The memories filed in a Space.
565
+ *
566
+ * This endpoint answers `{ data, pagination: { limit } }` with no cursor: it
567
+ * returns the first `limit` members and stops. Wrapped in a `Paginated`
568
+ * anyway so it reads like every other list, and it simply yields one page -
569
+ * a caller who needs more should filter `memories.list` by `spaceIds`, which
570
+ * is the endpoint that actually pages.
571
+ */
572
+ memories(id, params = {}, options) {
573
+ return new Paginated(
574
+ () => this.#http.get(
575
+ `/api/v1/spaces/${encodeURIComponent(id)}/memories`,
576
+ { ...params.limit !== void 0 ? { limit: params.limit } : {} },
577
+ options
578
+ )
579
+ );
580
+ }
581
+ async addMemories(id, memoryIds, options) {
582
+ return this.#http.post(
583
+ `/api/v1/spaces/${encodeURIComponent(id)}/memories`,
584
+ { memoryIds },
585
+ options
586
+ );
587
+ }
588
+ /**
589
+ * Removes memberships. The memories themselves are untouched.
590
+ *
591
+ * A body on a DELETE, which is unusual and is what the API takes: the
592
+ * alternative is five hundred ids in a query string, and every proxy in
593
+ * between has its own limit on how long a URL may be.
594
+ */
595
+ async removeMemories(id, memoryIds, options) {
596
+ return this.#http.delete(
597
+ `/api/v1/spaces/${encodeURIComponent(id)}/memories`,
598
+ { memoryIds },
599
+ options
600
+ );
601
+ }
602
+ };
603
+ var Sources = class {
604
+ #http;
605
+ constructor(http) {
606
+ this.#http = http;
607
+ }
608
+ list(params = {}, options) {
609
+ return new Paginated(
610
+ (cursor) => this.#http.get(
611
+ "/api/v1/sources",
612
+ pageQuery(params, cursor, {
613
+ ...params.provider !== void 0 ? { provider: params.provider } : {}
614
+ }),
615
+ options
616
+ )
617
+ );
618
+ }
619
+ async get(id, options) {
620
+ return this.#http.get(`/api/v1/sources/${encodeURIComponent(id)}`, void 0, options);
621
+ }
622
+ };
623
+ var Documents = class {
624
+ #http;
625
+ constructor(http) {
626
+ this.#http = http;
627
+ }
628
+ list(params = {}, options) {
629
+ return new Paginated(
630
+ (cursor) => this.#http.get(
631
+ "/api/v1/documents",
632
+ pageQuery(params, cursor, {
633
+ ...params.sourceId !== void 0 ? { sourceId: params.sourceId } : {},
634
+ ...params.status !== void 0 ? { status: params.status } : {}
635
+ }),
636
+ options
637
+ )
638
+ );
639
+ }
640
+ async get(id, options) {
641
+ return this.#http.get(
642
+ `/api/v1/documents/${encodeURIComponent(id)}`,
643
+ void 0,
644
+ options
645
+ );
646
+ }
647
+ };
648
+ var Jobs = class {
649
+ #http;
650
+ constructor(http) {
651
+ this.#http = http;
652
+ }
653
+ list(params = {}, options) {
654
+ return new Paginated(
655
+ (cursor) => this.#http.get(
656
+ "/api/v1/jobs",
657
+ pageQuery(params, cursor, {
658
+ ...params.status !== void 0 ? { status: params.status } : {},
659
+ ...params.type !== void 0 ? { type: params.type } : {}
660
+ }),
661
+ options
662
+ )
663
+ );
664
+ }
665
+ /**
666
+ * One job, by id. This is what `remember` hands back a reference to.
667
+ *
668
+ * `completed` is the terminal success state - the store's own word, not
669
+ * `succeeded`. A caller polling for a state the API never writes waits
670
+ * forever with nothing to show why.
671
+ */
672
+ async get(id, options) {
673
+ return this.#http.get(`/api/v1/jobs/${encodeURIComponent(id)}`, void 0, options);
674
+ }
675
+ };
676
+ var Entities = class {
677
+ #http;
678
+ constructor(http) {
679
+ this.#http = http;
680
+ }
681
+ list(params = {}, options) {
682
+ return new Paginated(
683
+ (cursor) => this.#http.get(
684
+ "/api/v1/entities",
685
+ pageQuery(params, cursor, {
686
+ ...params.type !== void 0 ? { type: params.type } : {},
687
+ ...params.q !== void 0 ? { q: params.q } : {}
688
+ }),
689
+ options
690
+ )
691
+ );
692
+ }
693
+ async get(id, options) {
694
+ return this.#http.get(`/api/v1/entities/${encodeURIComponent(id)}`, void 0, options);
695
+ }
696
+ /**
697
+ * Which memories mention this entity, and how.
698
+ *
699
+ * Returns MENTIONS - a memory id, a role and a confidence - not the memories
700
+ * themselves. "Memories about Sam" and "memories Sam appears in" are
701
+ * different questions, and `role` is what separates them.
702
+ *
703
+ * Like the Space membership list, this endpoint answers with a `pagination`
704
+ * block that carries no cursor, so it yields one page and stops.
705
+ */
706
+ memories(id, params = {}, options) {
707
+ return new Paginated(
708
+ (cursor) => this.#http.get(
709
+ `/api/v1/entities/${encodeURIComponent(id)}/memories`,
710
+ pageQuery(params, cursor, {
711
+ ...params.role !== void 0 ? { role: params.role } : {},
712
+ ...params.minConfidence !== void 0 ? { minConfidence: params.minConfidence } : {}
713
+ }),
714
+ options
715
+ )
716
+ );
717
+ }
718
+ };
719
+ var Graph = class {
720
+ #http;
721
+ constructor(http) {
722
+ this.#http = http;
723
+ }
724
+ async traverse(params, options) {
725
+ return this.#http.get(
726
+ "/api/v1/graph",
727
+ {
728
+ from: params.from,
729
+ ...params.depth !== void 0 ? { depth: params.depth } : {},
730
+ ...params.maxNodes !== void 0 ? { maxNodes: params.maxNodes } : {},
731
+ ...params.memoryLimit !== void 0 ? { memoryLimit: params.memoryLimit } : {}
732
+ },
733
+ options
734
+ );
735
+ }
736
+ };
737
+ var Conflicts = class {
738
+ #http;
739
+ constructor(http) {
740
+ this.#http = http;
741
+ }
742
+ list(params = {}, options) {
743
+ return new Paginated(
744
+ (cursor) => this.#http.get(
745
+ "/api/v1/conflicts",
746
+ pageQuery(params, cursor, {
747
+ ...params.includeResolved !== void 0 ? { includeResolved: params.includeResolved } : {},
748
+ ...params.type !== void 0 ? { type: params.type } : {}
749
+ }),
750
+ options
751
+ )
752
+ );
753
+ }
754
+ async get(id, options) {
755
+ return this.#http.get(
756
+ `/api/v1/conflicts/${encodeURIComponent(id)}`,
757
+ void 0,
758
+ options
759
+ );
760
+ }
761
+ /**
762
+ * Settles one.
763
+ *
764
+ * `keep` names the winner and supersedes the loser; it does not delete it.
765
+ * `dismiss` records that the detector was wrong, which is worth knowing when
766
+ * the same pair trips it again.
767
+ *
768
+ * The parameter type is a union, so `keep` without a `keepId` does not
769
+ * compile. The server rejects it too - this just moves the failure from a
770
+ * 400 in production to a red squiggle.
771
+ */
772
+ async resolve(id, params, options) {
773
+ return this.#http.post(
774
+ `/api/v1/conflicts/${encodeURIComponent(id)}/resolve`,
775
+ params,
776
+ options
777
+ );
778
+ }
779
+ };
780
+ var Conversations = class {
781
+ #http;
782
+ constructor(http) {
783
+ this.#http = http;
784
+ }
785
+ list(params = {}, options) {
786
+ return new Paginated(
787
+ (cursor) => this.#http.get(
788
+ "/api/v1/conversations",
789
+ pageQuery(params, cursor, {
790
+ ...params.channel !== void 0 ? { channel: params.channel } : {}
791
+ }),
792
+ options
793
+ )
794
+ );
795
+ }
796
+ async get(id, options) {
797
+ return this.#http.get(
798
+ `/api/v1/conversations/${encodeURIComponent(id)}`,
799
+ void 0,
800
+ options
801
+ );
802
+ }
803
+ async create(params = {}, options) {
804
+ return this.#http.post("/api/v1/conversations", params, options);
805
+ }
806
+ messages(id, params = {}, options) {
807
+ return new Paginated(
808
+ (cursor) => this.#http.get(
809
+ `/api/v1/conversations/${encodeURIComponent(id)}/messages`,
810
+ pageQuery(params, cursor, {}),
811
+ options
812
+ )
813
+ );
814
+ }
815
+ /**
816
+ * Appends turns, and by default extracts memories from them.
817
+ *
818
+ * Check `extracting` on the result. With no queue configured the turns are
819
+ * stored and never become memory, and the API says so in `note` rather than
820
+ * reporting a success - a caller that ignores it believes a memory is on its
821
+ * way that never arrives.
822
+ *
823
+ * `system` and `tool` turns are stored but never extracted: a system prompt
824
+ * is configuration, and remembering it would file our own instructions as
825
+ * the user's facts.
826
+ *
827
+ * Give this an `idempotencyKey`. Appending the same turn twice is the most
828
+ * likely duplicate in the whole API - a client reconnecting after a dropped
829
+ * response has no other way to tell whether its last write landed.
830
+ */
831
+ async append(id, params, options) {
832
+ return this.#http.post(
833
+ `/api/v1/conversations/${encodeURIComponent(id)}/messages`,
834
+ params,
835
+ options
836
+ );
837
+ }
838
+ };
839
+ var Integrations = class {
840
+ #http;
841
+ constructor(http) {
842
+ this.#http = http;
843
+ }
844
+ list(params = {}, options) {
845
+ return new Paginated(
846
+ (cursor) => this.#http.get(
847
+ "/api/v1/integrations",
848
+ pageQuery(params, cursor, {
849
+ ...params.provider !== void 0 ? { provider: params.provider } : {},
850
+ ...params.status !== void 0 ? { status: params.status } : {}
851
+ }),
852
+ options
853
+ )
854
+ );
855
+ }
856
+ /** Providers that can be connected at all. Not the user's own connections. */
857
+ async available(options) {
858
+ return this.#http.get("/api/v1/integrations/available", void 0, options);
859
+ }
860
+ async get(id, options) {
861
+ return this.#http.get(
862
+ `/api/v1/integrations/${encodeURIComponent(id)}`,
863
+ void 0,
864
+ options
865
+ );
866
+ }
867
+ async connect(params, options) {
868
+ return this.#http.post(
869
+ "/api/v1/integrations/connect",
870
+ params,
871
+ options
872
+ );
873
+ }
874
+ async update(id, params, options) {
875
+ return this.#http.patch(
876
+ `/api/v1/integrations/${encodeURIComponent(id)}`,
877
+ params,
878
+ options
879
+ );
880
+ }
881
+ /**
882
+ * Asks for a sync now rather than waiting for the schedule. Answers 202.
883
+ *
884
+ * `full` re-reads everything and is deliberately opt-in: on a large Drive
885
+ * that is thousands of documents and a real bill. The incremental default is
886
+ * what should run almost always.
887
+ */
888
+ async sync(id, params = {}, options) {
889
+ return this.#http.post(
890
+ `/api/v1/integrations/${encodeURIComponent(id)}/sync`,
891
+ params,
892
+ options
893
+ );
894
+ }
895
+ /**
896
+ * Destroys the credentials. The row stays.
897
+ *
898
+ * History still has to attribute the memories this connection produced, and
899
+ * deleting the row would leave them pointing at nothing.
900
+ */
901
+ async disconnect(id, options) {
902
+ return this.#http.delete(
903
+ `/api/v1/integrations/${encodeURIComponent(id)}`,
904
+ void 0,
905
+ options
906
+ );
907
+ }
908
+ };
909
+ var Health = class {
910
+ #http;
911
+ constructor(http) {
912
+ this.#http = http;
913
+ }
914
+ async live(options) {
915
+ return this.#http.get("/health/live", void 0, options);
916
+ }
917
+ async ready(options) {
918
+ return this.#http.get("/health/ready", void 0, options);
919
+ }
920
+ };
921
+ var PersistMemory = class {
922
+ memories;
923
+ search;
924
+ spaces;
925
+ sources;
926
+ documents;
927
+ jobs;
928
+ entities;
929
+ graph;
930
+ conflicts;
931
+ conversations;
932
+ integrations;
933
+ health;
934
+ #http;
935
+ constructor(options) {
936
+ this.#http = new HttpClient(options);
937
+ this.memories = new Memories(this.#http);
938
+ this.search = new Search(this.#http);
939
+ this.spaces = new Spaces(this.#http);
940
+ this.sources = new Sources(this.#http);
941
+ this.documents = new Documents(this.#http);
942
+ this.jobs = new Jobs(this.#http);
943
+ this.entities = new Entities(this.#http);
944
+ this.graph = new Graph(this.#http);
945
+ this.conflicts = new Conflicts(this.#http);
946
+ this.conversations = new Conversations(this.#http);
947
+ this.integrations = new Integrations(this.#http);
948
+ this.health = new Health(this.#http);
949
+ }
950
+ /**
951
+ * An escape hatch for an endpoint this package has not caught up with.
952
+ *
953
+ * Typed as `unknown` on purpose: a caller reaching past the typed surface is
954
+ * taking responsibility for the shape, and handing them `any` would let that
955
+ * responsibility spread silently through their codebase.
956
+ */
957
+ async request(method, path, body, options) {
958
+ if (method === "GET") return this.#http.get(path, void 0, options);
959
+ if (method === "POST") return this.#http.post(path, body, options);
960
+ if (method === "PATCH") return this.#http.patch(path, body, options);
961
+ return this.#http.delete(path, body, options);
962
+ }
963
+ /** Never the key. See `HttpClient.toJSON`, which this delegates to. */
964
+ toJSON() {
965
+ return this.#http.toJSON();
966
+ }
967
+ [Symbol.for("nodejs.util.inspect.custom")]() {
968
+ return this.#http.toJSON();
969
+ }
970
+ };
971
+
972
+ // src/args.ts
973
+ function parseArgs(argv) {
974
+ const words = [];
975
+ const flags = {};
976
+ const rest = [];
977
+ let index = 0;
978
+ while (index < argv.length) {
979
+ const token = argv[index];
980
+ if (token === "--") {
981
+ rest.push(...argv.slice(index + 1));
982
+ break;
983
+ }
984
+ if (token.startsWith("--")) {
985
+ const body = token.slice(2);
986
+ const equals = body.indexOf("=");
987
+ if (equals !== -1) {
988
+ flags[body.slice(0, equals)] = body.slice(equals + 1);
989
+ index += 1;
990
+ continue;
991
+ }
992
+ if (body.startsWith("no-")) {
993
+ flags[body.slice(3)] = false;
994
+ index += 1;
995
+ continue;
996
+ }
997
+ const next = argv[index + 1];
998
+ if (next !== void 0 && !next.startsWith("-")) {
999
+ flags[body] = next;
1000
+ index += 2;
1001
+ continue;
1002
+ }
1003
+ flags[body] = true;
1004
+ index += 1;
1005
+ continue;
1006
+ }
1007
+ if (token.startsWith("-") && token.length > 1) {
1008
+ const body = token.slice(1);
1009
+ const next = argv[index + 1];
1010
+ if (next !== void 0 && !next.startsWith("-")) {
1011
+ flags[body] = next;
1012
+ index += 2;
1013
+ continue;
1014
+ }
1015
+ flags[body] = true;
1016
+ index += 1;
1017
+ continue;
1018
+ }
1019
+ words.push(token);
1020
+ index += 1;
1021
+ }
1022
+ return { words, flags, rest };
1023
+ }
1024
+ function stringFlag(args, ...names) {
1025
+ for (const name of names) {
1026
+ const value = args.flags[name];
1027
+ if (typeof value === "string") return value;
1028
+ }
1029
+ return void 0;
1030
+ }
1031
+ function boolFlag(args, ...names) {
1032
+ for (const name of names) {
1033
+ const value = args.flags[name];
1034
+ if (typeof value === "boolean") return value;
1035
+ if (value === "true") return true;
1036
+ if (value === "false") return false;
1037
+ }
1038
+ return false;
1039
+ }
1040
+ function numberFlag(args, ...names) {
1041
+ for (const name of names) {
1042
+ const value = args.flags[name];
1043
+ if (value === void 0 || typeof value === "boolean") continue;
1044
+ const parsed = Number(value);
1045
+ if (!Number.isFinite(parsed)) return "invalid";
1046
+ return parsed;
1047
+ }
1048
+ return void 0;
1049
+ }
1050
+ function listFlag(args, ...names) {
1051
+ const raw = stringFlag(args, ...names);
1052
+ if (raw === void 0) return void 0;
1053
+ const items = raw.split(",").map((one) => one.trim()).filter((one) => one.length > 0);
1054
+ return items.length > 0 ? items : void 0;
1055
+ }
1056
+
1057
+ // src/config.ts
1058
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1059
+ import { homedir } from "node:os";
1060
+ import { join } from "node:path";
1061
+ var DEFAULT_API_URL = "https://api.persistmemory.com";
1062
+ var DEFAULT_PROFILE = "default";
1063
+ function pathsFor(home = homedir()) {
1064
+ const dir = process.env["PERSISTMEMORY_HOME"] ?? join(home, ".persistmemory");
1065
+ return {
1066
+ dir,
1067
+ config: join(dir, "config.json"),
1068
+ credentials: join(dir, "credentials.json")
1069
+ };
1070
+ }
1071
+ function readConfig(paths) {
1072
+ const empty = { current: DEFAULT_PROFILE, profiles: {} };
1073
+ if (!existsSync(paths.config)) return empty;
1074
+ try {
1075
+ const parsed = JSON.parse(readFileSync(paths.config, "utf8"));
1076
+ return {
1077
+ current: typeof parsed.current === "string" ? parsed.current : DEFAULT_PROFILE,
1078
+ profiles: typeof parsed.profiles === "object" && parsed.profiles ? parsed.profiles : {}
1079
+ };
1080
+ } catch {
1081
+ return empty;
1082
+ }
1083
+ }
1084
+ function readCredentials(paths) {
1085
+ if (!existsSync(paths.credentials)) return {};
1086
+ try {
1087
+ return JSON.parse(readFileSync(paths.credentials, "utf8"));
1088
+ } catch {
1089
+ return {};
1090
+ }
1091
+ }
1092
+ function writeSecurely(path, contents, mode) {
1093
+ const temporary = `${path}.tmp`;
1094
+ writeFileSync(temporary, contents, { mode });
1095
+ chmodSync(temporary, mode);
1096
+ renameSync(temporary, path);
1097
+ }
1098
+ function writeConfig(paths, config) {
1099
+ mkdirSync(paths.dir, { recursive: true, mode: 448 });
1100
+ writeSecurely(paths.config, `${JSON.stringify(config, null, 2)}
1101
+ `, 384);
1102
+ }
1103
+ function writeCredentials(paths, credentials) {
1104
+ mkdirSync(paths.dir, { recursive: true, mode: 448 });
1105
+ writeSecurely(paths.credentials, `${JSON.stringify(credentials, null, 2)}
1106
+ `, 384);
1107
+ }
1108
+ function resolve(args) {
1109
+ const env = args.env ?? process.env;
1110
+ const config = readConfig(args.paths);
1111
+ const profile = args.profileFlag ?? env["PERSISTMEMORY_PROFILE"] ?? config.current ?? DEFAULT_PROFILE;
1112
+ const stored = config.profiles[profile];
1113
+ const apiUrl = args.apiUrlFlag ?? env["PERSISTMEMORY_API_URL"] ?? stored?.apiUrl ?? DEFAULT_API_URL;
1114
+ const fromFlag = args.apiKeyFlag;
1115
+ const fromEnv = env["PERSISTMEMORY_API_KEY"];
1116
+ const clientId = stored?.clientId;
1117
+ if (fromFlag) {
1118
+ return {
1119
+ profile,
1120
+ apiUrl,
1121
+ credential: { kind: "api-key", token: fromFlag },
1122
+ ...clientId ? { clientId } : {},
1123
+ fromEnvironment: true
1124
+ };
1125
+ }
1126
+ if (fromEnv) {
1127
+ return {
1128
+ profile,
1129
+ apiUrl,
1130
+ credential: { kind: "api-key", token: fromEnv },
1131
+ ...clientId ? { clientId } : {},
1132
+ fromEnvironment: true
1133
+ };
1134
+ }
1135
+ const credential = readCredentials(args.paths)[profile];
1136
+ return {
1137
+ profile,
1138
+ apiUrl,
1139
+ ...credential ? { credential } : {},
1140
+ ...clientId ? { clientId } : {},
1141
+ fromEnvironment: false
1142
+ };
1143
+ }
1144
+ function saveLogin(args) {
1145
+ const config = readConfig(args.paths);
1146
+ writeConfig(args.paths, {
1147
+ // Logging in makes that profile the current one. Anything else means a
1148
+ // person logs in, runs a command, and is told they are not logged in.
1149
+ current: args.profile,
1150
+ profiles: {
1151
+ ...config.profiles,
1152
+ [args.profile]: {
1153
+ apiUrl: args.apiUrl,
1154
+ ...args.account ? { account: args.account } : {},
1155
+ // Kept from the existing profile when this login did not register a
1156
+ // new client, so an --api-key login does not erase the browser
1157
+ // client id and force a re-registration on the next `pm auth login`.
1158
+ ...args.clientId ?? config.profiles[args.profile]?.clientId ? { clientId: args.clientId ?? config.profiles[args.profile]?.clientId } : {}
1159
+ }
1160
+ }
1161
+ });
1162
+ writeCredentials(args.paths, {
1163
+ ...readCredentials(args.paths),
1164
+ [args.profile]: args.credential
1165
+ });
1166
+ }
1167
+ function clearLogin(paths, profile) {
1168
+ const credentials = readCredentials(paths);
1169
+ if (!(profile in credentials)) return false;
1170
+ delete credentials[profile];
1171
+ writeCredentials(paths, credentials);
1172
+ return true;
1173
+ }
1174
+ function maskToken(token) {
1175
+ if (token.length <= 8) return "*".repeat(token.length);
1176
+ return `${token.slice(0, 4)}\u2026${token.slice(-4)}`;
1177
+ }
1178
+
1179
+ // src/output.ts
1180
+ var OUTPUT_FORMATS = ["table", "json", "yaml", "csv", "tsv"];
1181
+ function isOutputFormat(value) {
1182
+ return OUTPUT_FORMATS.includes(value);
1183
+ }
1184
+ function render(rows, columns, options) {
1185
+ switch (options.format) {
1186
+ case "json":
1187
+ return JSON.stringify(rows, null, 2);
1188
+ case "yaml":
1189
+ return toYaml(rows);
1190
+ case "csv":
1191
+ return delimited(rows, columns, ",");
1192
+ case "tsv":
1193
+ return delimited(rows, columns, " ");
1194
+ default:
1195
+ return table(rows, columns, options.width);
1196
+ }
1197
+ }
1198
+ function renderOne(row, fields, options) {
1199
+ if (options.format === "json") return JSON.stringify(row, null, 2);
1200
+ if (options.format === "yaml") return toYaml(row);
1201
+ if (options.format === "csv" || options.format === "tsv") {
1202
+ return delimited([row], fields, options.format === "csv" ? "," : " ");
1203
+ }
1204
+ const width = Math.max(...fields.map((field) => field.header.length));
1205
+ return fields.map((field) => `${field.header.padEnd(width)} ${field.value(row)}`).join("\n");
1206
+ }
1207
+ function table(rows, columns, width = process.stdout.columns || 120) {
1208
+ if (rows.length === 0) return "";
1209
+ const cells = rows.map((row) => columns.map((column) => oneLine(column.value(row))));
1210
+ const widths = columns.map(
1211
+ (column, index) => Math.max(column.header.length, ...cells.map((row) => (row[index] ?? "").length))
1212
+ );
1213
+ const separator = 2;
1214
+ let total = widths.reduce((sum, one) => sum + one + separator, -separator);
1215
+ while (total > width && Math.max(...widths) > 8) {
1216
+ const widest = widths.indexOf(Math.max(...widths));
1217
+ widths[widest] = widths[widest] - 1;
1218
+ total -= 1;
1219
+ }
1220
+ const line = (values) => values.map((value, index) => clip(value, widths[index]).padEnd(widths[index])).join(" ").trimEnd();
1221
+ return [
1222
+ line(columns.map((column) => column.header.toUpperCase())),
1223
+ ...cells.map((row) => line(row))
1224
+ ].join("\n");
1225
+ }
1226
+ function delimited(rows, columns, separator) {
1227
+ const escape = (value) => {
1228
+ const flat = oneLine(value);
1229
+ if (!flat.includes(separator) && !flat.includes('"') && !flat.includes("\n")) return flat;
1230
+ return `"${flat.replace(/"/g, '""')}"`;
1231
+ };
1232
+ return [
1233
+ columns.map((column) => escape(column.header)).join(separator),
1234
+ ...rows.map((row) => columns.map((column) => escape(column.value(row))).join(separator))
1235
+ ].join("\n");
1236
+ }
1237
+ function toYaml(value, indent = 0) {
1238
+ const pad = " ".repeat(indent);
1239
+ if (value === null || value === void 0) return "null";
1240
+ if (typeof value === "boolean" || typeof value === "number") return String(value);
1241
+ if (typeof value === "string") return yamlString(value);
1242
+ if (Array.isArray(value)) {
1243
+ if (value.length === 0) return "[]";
1244
+ return value.map((item) => {
1245
+ const rendered = toYaml(item, indent + 2);
1246
+ return isBlock(item) ? `${pad}-
1247
+ ${rendered}` : `${pad}- ${rendered}`;
1248
+ }).join("\n");
1249
+ }
1250
+ if (typeof value === "object") {
1251
+ const entries = Object.entries(value);
1252
+ if (entries.length === 0) return "{}";
1253
+ return entries.map(([key, item]) => {
1254
+ const rendered = toYaml(item, indent + 2);
1255
+ return isBlock(item) ? `${pad}${key}:
1256
+ ${rendered}` : `${pad}${key}: ${rendered}`;
1257
+ }).join("\n");
1258
+ }
1259
+ return String(value);
1260
+ }
1261
+ function isBlock(value) {
1262
+ if (Array.isArray(value)) return value.length > 0;
1263
+ return typeof value === "object" && value !== null && Object.keys(value).length > 0;
1264
+ }
1265
+ function yamlString(value) {
1266
+ if (value === "") return '""';
1267
+ if (value.includes("\n")) {
1268
+ return `|-
1269
+ ${value.split("\n").map((line) => ` ${line}`).join("\n")}`;
1270
+ }
1271
+ const ambiguous = /^(y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF|null|Null|NULL|~)$/.test(
1272
+ value
1273
+ ) || /^[-+]?[0-9]/.test(value) || /^[\s#&*!|>'"%@`{}[\],]/.test(value) || value.includes(": ") || value.endsWith(":");
1274
+ return ambiguous ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
1275
+ }
1276
+ function oneLine(value) {
1277
+ return value.replace(/\s*\n\s*/g, " ").trim();
1278
+ }
1279
+ function clip(value, width) {
1280
+ if (value.length <= width) return value;
1281
+ return width <= 1 ? value.slice(0, width) : `${value.slice(0, width - 1)}\u2026`;
1282
+ }
1283
+ function shortDate(iso) {
1284
+ if (!iso) return "";
1285
+ const at = new Date(iso);
1286
+ return Number.isNaN(at.getTime()) ? "" : iso.slice(0, 16).replace("T", " ");
1287
+ }
1288
+
1289
+ // src/help.ts
1290
+ var VERSION = "0.1.0";
1291
+ var HELP = `
1292
+ pm \u2014 PersistMemory from your terminal
1293
+
1294
+ Memory that persists across every model, tool and session you use.
1295
+
1296
+ GETTING STARTED
1297
+
1298
+ pm auth login sign in with your browser
1299
+ pm start a session and just ask
1300
+ pm remember "we chose Postgres" capture something
1301
+ pm search "what did we choose" ask for it back
1302
+
1303
+ COMMANDS
1304
+
1305
+ auth login sign in through the browser
1306
+ auth login --api-key sign in with a key, for CI and headless machines
1307
+
1308
+ agent --root <dir> answer file requests from your assistants,
1309
+ reading only inside the folders you name
1310
+ auth status who am I, and does the server still accept it
1311
+ auth logout forget the stored credential
1312
+
1313
+ chat start an interactive session
1314
+ chat --resume <id> pick up an earlier session
1315
+
1316
+ remember <text> capture text
1317
+ remember - capture whatever is piped in
1318
+ remember --file <path> capture a file's contents
1319
+
1320
+ search <query> search your memory
1321
+ list memories the most recent memories
1322
+ list spaces your Spaces
1323
+ get memory <id> one memory, in full
1324
+
1325
+ status is the service healthy
1326
+ requests file requests waiting for you to approve
1327
+
1328
+ FLAGS
1329
+
1330
+ --output table|json|yaml|csv|tsv how to print (default: table on a
1331
+ terminal, json when piped)
1332
+ --profile <name> use a named account
1333
+ --api-url <url> talk to a different server
1334
+ --limit <n> how many results
1335
+ --space <a,b> restrict to Spaces
1336
+ --quiet suppress notices
1337
+ --version print the version
1338
+ --help print this
1339
+
1340
+ ENVIRONMENT
1341
+
1342
+ PERSISTMEMORY_API_KEY a key, taking precedence over any stored
1343
+ login. This is what CI should set.
1344
+ PERSISTMEMORY_API_URL the server to talk to
1345
+ PERSISTMEMORY_PROFILE which stored profile to use
1346
+ PERSISTMEMORY_HOME where config, credentials and session
1347
+ transcripts live (default: ~/.persistmemory)
1348
+ PERSISTMEMORY_CLIENT_ID override the OAuth client id, for a
1349
+ self-hosted deployment
1350
+
1351
+ Docs: https://persistmemory.com/docs/cli
1352
+ `;
1353
+
1354
+ // src/auth/oauth.ts
1355
+ import { spawn } from "node:child_process";
1356
+ import { timingSafeEqual } from "node:crypto";
1357
+
1358
+ // src/auth/pkce.ts
1359
+ import { createHash, randomBytes } from "node:crypto";
1360
+ function createPkce() {
1361
+ const verifier = base64Url(randomBytes(32));
1362
+ return {
1363
+ verifier,
1364
+ challenge: base64Url(createHash("sha256").update(verifier).digest()),
1365
+ // Never "plain". OAuth 2.1 removes it, and a plain challenge is the
1366
+ // verifier, which defends against nothing.
1367
+ method: "S256"
1368
+ };
1369
+ }
1370
+ function randomState() {
1371
+ return base64Url(randomBytes(24));
1372
+ }
1373
+ function base64Url(bytes) {
1374
+ return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1375
+ }
1376
+
1377
+ // src/auth/loopback.ts
1378
+ import { createServer } from "node:http";
1379
+ var CALLBACK_PATH = "/callback";
1380
+ async function startLoopback(options = {}) {
1381
+ const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
1382
+ let resolveCallback;
1383
+ let rejectCallback;
1384
+ const received = new Promise((resolve5, reject) => {
1385
+ resolveCallback = resolve5;
1386
+ rejectCallback = reject;
1387
+ });
1388
+ const server = createServer((request, response) => {
1389
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
1390
+ if (url.pathname !== CALLBACK_PATH) {
1391
+ response.writeHead(404, { "content-type": "text/plain" });
1392
+ response.end("Not found");
1393
+ return;
1394
+ }
1395
+ const callback = {
1396
+ ...url.searchParams.get("code") ? { code: url.searchParams.get("code") } : {},
1397
+ ...url.searchParams.get("state") ? { state: url.searchParams.get("state") } : {},
1398
+ ...url.searchParams.get("error") ? { error: url.searchParams.get("error") } : {},
1399
+ ...url.searchParams.get("error_description") ? { errorDescription: url.searchParams.get("error_description") } : {}
1400
+ };
1401
+ response.writeHead(200, {
1402
+ "content-type": "text/html; charset=utf-8",
1403
+ // This page is one-use and holds a result; nothing should keep it.
1404
+ "cache-control": "no-store",
1405
+ // It never loads anything, so it is not allowed to.
1406
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'"
1407
+ });
1408
+ response.end(donePage(callback));
1409
+ resolveCallback?.(callback);
1410
+ });
1411
+ await new Promise((resolve5, reject) => {
1412
+ server.once("error", reject);
1413
+ server.listen(0, "127.0.0.1", resolve5);
1414
+ });
1415
+ const address = server.address();
1416
+ if (address === null || typeof address === "string") {
1417
+ server.close();
1418
+ throw new Error("could not determine the port the callback listener bound to");
1419
+ }
1420
+ const timer = setTimeout(() => {
1421
+ rejectCallback?.(
1422
+ new Error("timed out waiting for the browser. Run `pm auth login` again, or use --api-key.")
1423
+ );
1424
+ }, timeoutMs);
1425
+ timer.unref?.();
1426
+ return {
1427
+ port: address.port,
1428
+ redirectUri: `http://127.0.0.1:${address.port}${CALLBACK_PATH}`,
1429
+ async waitForCallback() {
1430
+ try {
1431
+ return await received;
1432
+ } finally {
1433
+ clearTimeout(timer);
1434
+ }
1435
+ },
1436
+ close() {
1437
+ clearTimeout(timer);
1438
+ server.close();
1439
+ }
1440
+ };
1441
+ }
1442
+ function donePage(callback) {
1443
+ const failed = Boolean(callback.error) || !callback.code;
1444
+ const title = failed ? "Sign-in failed" : "You are signed in";
1445
+ const detail = failed ? escapeHtml(callback.errorDescription ?? callback.error ?? "No authorization code was returned.") : "You can close this tab and go back to your terminal.";
1446
+ return `<!doctype html>
1447
+ <html lang="en"><head><meta charset="utf-8"><title>${title}</title>
1448
+ <style>
1449
+ :root { color-scheme: light dark; }
1450
+ body { font: 16px/1.6 ui-sans-serif, system-ui, -apple-system, sans-serif;
1451
+ display: grid; place-items: center; min-height: 100vh; margin: 0; }
1452
+ main { max-width: 30rem; padding: 2rem; text-align: center; }
1453
+ h1 { font-size: 1.25rem; margin: 0 0 .5rem; }
1454
+ p { margin: 0; opacity: .8; }
1455
+ </style></head>
1456
+ <body><main><h1>${title}</h1><p>${detail}</p></main></body></html>`;
1457
+ }
1458
+ function escapeHtml(value) {
1459
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1460
+ }
1461
+
1462
+ // src/auth/oauth.ts
1463
+ async function protectedResource(apiUrl, deps) {
1464
+ try {
1465
+ const url = new URL("/.well-known/oauth-protected-resource", apiUrl).toString();
1466
+ const response = await deps.fetch(url, { headers: { accept: "application/json" } });
1467
+ if (!response.ok) return void 0;
1468
+ const body = await response.json();
1469
+ const servers = body["authorization_servers"];
1470
+ const resource = body["resource"];
1471
+ if (!Array.isArray(servers) || typeof servers[0] !== "string") return void 0;
1472
+ if (typeof resource !== "string") return void 0;
1473
+ return { issuer: servers[0], resource };
1474
+ } catch {
1475
+ return void 0;
1476
+ }
1477
+ }
1478
+ async function discover(apiUrl, deps) {
1479
+ const guarded = await protectedResource(apiUrl, deps);
1480
+ const issuerUrl = guarded?.issuer ?? apiUrl;
1481
+ const url = new URL("/.well-known/oauth-authorization-server", issuerUrl).toString();
1482
+ const response = await deps.fetch(url, { headers: { accept: "application/json" } });
1483
+ if (!response.ok) {
1484
+ throw new Error(
1485
+ `${issuerUrl} does not look like a PersistMemory API: discovery answered ${response.status}.`
1486
+ );
1487
+ }
1488
+ const body = await response.json();
1489
+ const required = (key) => {
1490
+ const value = body[key];
1491
+ if (typeof value !== "string") {
1492
+ throw new Error(`the server's metadata is missing "${key}"`);
1493
+ }
1494
+ return value;
1495
+ };
1496
+ return {
1497
+ issuer: required("issuer"),
1498
+ authorizationEndpoint: required("authorization_endpoint"),
1499
+ tokenEndpoint: required("token_endpoint"),
1500
+ // Carried through so the authorize request can name it. Absent when the
1501
+ // API is its own resource, in which case the server uses its default.
1502
+ ...guarded?.resource ? { resource: guarded.resource } : {},
1503
+ ...typeof body["registration_endpoint"] === "string" ? { registrationEndpoint: body["registration_endpoint"] } : {},
1504
+ ...Array.isArray(body["scopes_supported"]) ? { scopesSupported: body["scopes_supported"].map(String) } : {}
1505
+ };
1506
+ }
1507
+ async function registerClient(server, redirectUri, scope, deps) {
1508
+ if (!server.registrationEndpoint) {
1509
+ throw new Error(
1510
+ "this server does not offer dynamic client registration. Use `pm auth login --api-key` instead."
1511
+ );
1512
+ }
1513
+ const response = await deps.fetch(server.registrationEndpoint, {
1514
+ method: "POST",
1515
+ headers: { "content-type": "application/json", accept: "application/json" },
1516
+ body: JSON.stringify({
1517
+ client_name: "PersistMemory CLI",
1518
+ client_uri: "https://persistmemory.com/docs/cli",
1519
+ // Registered WITHOUT a port. The server compares loopback redirects
1520
+ // ignoring the port (RFC 8252 §7.3), so registering the one we happened
1521
+ // to bind this time would be noise — and would break the next login,
1522
+ // which binds a different one.
1523
+ redirect_uris: ["http://127.0.0.1/callback"],
1524
+ grant_types: ["authorization_code", "refresh_token"],
1525
+ response_types: ["code"],
1526
+ token_endpoint_auth_method: "none",
1527
+ scope
1528
+ })
1529
+ });
1530
+ if (!response.ok) {
1531
+ throw new Error(`could not register with ${server.issuer}: ${await describe(response)}`);
1532
+ }
1533
+ const body = await response.json();
1534
+ if (typeof body.client_id !== "string") {
1535
+ throw new Error("the server registered this client but returned no client_id");
1536
+ }
1537
+ return body.client_id;
1538
+ }
1539
+ var CLI_CLIENT_ID = "persistmemory-cli";
1540
+ async function knownClient(server, clientId, deps) {
1541
+ try {
1542
+ const probe = new URL(server.authorizationEndpoint);
1543
+ probe.searchParams.set("client_id", clientId);
1544
+ probe.searchParams.set("response_type", "code");
1545
+ const response = await deps.fetch(probe.toString(), {
1546
+ method: "GET",
1547
+ redirect: "manual"
1548
+ });
1549
+ if (response.status >= 500) return false;
1550
+ const body = await response.text().catch(() => "");
1551
+ return !body.includes("invalid_client");
1552
+ } catch {
1553
+ return false;
1554
+ }
1555
+ }
1556
+ async function loginWithBrowser(args) {
1557
+ const { deps } = args;
1558
+ const print = deps.print ?? (() => void 0);
1559
+ const server = await discover(args.apiUrl, deps);
1560
+ const listener = await startLoopback(
1561
+ args.timeoutMs !== void 0 ? { timeoutMs: args.timeoutMs } : {}
1562
+ );
1563
+ try {
1564
+ const clientId = args.clientId ?? process.env["PERSISTMEMORY_CLIENT_ID"] ?? (await knownClient(server, CLI_CLIENT_ID, deps) ? CLI_CLIENT_ID : await registerClient(server, listener.redirectUri, args.scope, deps));
1565
+ const pkce = createPkce();
1566
+ const state = randomState();
1567
+ const authorize = new URL(server.authorizationEndpoint);
1568
+ authorize.searchParams.set("response_type", "code");
1569
+ authorize.searchParams.set("client_id", clientId);
1570
+ authorize.searchParams.set("redirect_uri", listener.redirectUri);
1571
+ authorize.searchParams.set("scope", args.scope);
1572
+ authorize.searchParams.set("state", state);
1573
+ authorize.searchParams.set("code_challenge", pkce.challenge);
1574
+ authorize.searchParams.set("code_challenge_method", pkce.method);
1575
+ if (server.resource) authorize.searchParams.set("resource", server.resource);
1576
+ print("Opening your browser to sign in.");
1577
+ print(`If it does not open, visit:
1578
+
1579
+ ${authorize.toString()}
1580
+ `);
1581
+ await (deps.openBrowser ?? openBrowser)(authorize.toString()).catch(() => void 0);
1582
+ const callback = await listener.waitForCallback();
1583
+ if (callback.error) {
1584
+ throw new Error(
1585
+ `sign-in was refused: ${callback.errorDescription ?? callback.error}`
1586
+ );
1587
+ }
1588
+ if (!callback.code) {
1589
+ throw new Error("the browser came back without an authorization code");
1590
+ }
1591
+ if (!callback.state || !safeEqual(callback.state, state)) {
1592
+ throw new Error("the browser came back with the wrong state \u2014 sign-in was not completed");
1593
+ }
1594
+ const credential = await redeem({
1595
+ server,
1596
+ clientId,
1597
+ code: callback.code,
1598
+ verifier: pkce.verifier,
1599
+ redirectUri: listener.redirectUri,
1600
+ deps
1601
+ });
1602
+ return { credential, clientId };
1603
+ } finally {
1604
+ listener.close();
1605
+ }
1606
+ }
1607
+ async function redeem(args) {
1608
+ const form = new URLSearchParams({
1609
+ grant_type: "authorization_code",
1610
+ code: args.code,
1611
+ redirect_uri: args.redirectUri,
1612
+ client_id: args.clientId,
1613
+ code_verifier: args.verifier,
1614
+ // Restated at redemption. The server compares it against the resource the
1615
+ // code was authorized for and refuses a mismatch, which is what stops a
1616
+ // code issued for one service being redeemed for a token against another.
1617
+ ...args.server.resource ? { resource: args.server.resource } : {}
1618
+ });
1619
+ const response = await args.deps.fetch(args.server.tokenEndpoint, {
1620
+ method: "POST",
1621
+ headers: {
1622
+ "content-type": "application/x-www-form-urlencoded",
1623
+ accept: "application/json"
1624
+ },
1625
+ body: form.toString()
1626
+ });
1627
+ if (!response.ok) {
1628
+ throw new Error(`the server refused to issue a token: ${await describe(response)}`);
1629
+ }
1630
+ return toCredential(await response.json());
1631
+ }
1632
+ async function refresh(args) {
1633
+ const server = await discover(args.apiUrl, args.deps);
1634
+ const response = await args.deps.fetch(server.tokenEndpoint, {
1635
+ method: "POST",
1636
+ headers: {
1637
+ "content-type": "application/x-www-form-urlencoded",
1638
+ accept: "application/json"
1639
+ },
1640
+ body: new URLSearchParams({
1641
+ grant_type: "refresh_token",
1642
+ refresh_token: args.refreshToken,
1643
+ client_id: args.clientId
1644
+ }).toString()
1645
+ });
1646
+ if (!response.ok) {
1647
+ throw new Error(`could not renew the session: ${await describe(response)}`);
1648
+ }
1649
+ const credential = toCredential(await response.json());
1650
+ return credential.refreshToken ? credential : { ...credential, refreshToken: args.refreshToken };
1651
+ }
1652
+ function toCredential(body) {
1653
+ const token = body["access_token"];
1654
+ if (typeof token !== "string") {
1655
+ throw new Error("the server's token response contained no access_token");
1656
+ }
1657
+ const expiresIn = body["expires_in"];
1658
+ const expiresAt = typeof expiresIn === "number" && Number.isFinite(expiresIn) ? new Date(Date.now() + expiresIn * 1e3).toISOString() : void 0;
1659
+ return {
1660
+ kind: "oauth",
1661
+ token,
1662
+ ...typeof body["refresh_token"] === "string" ? { refreshToken: body["refresh_token"] } : {},
1663
+ ...expiresAt ? { expiresAt } : {},
1664
+ ...typeof body["scope"] === "string" ? { scope: body["scope"] } : {}
1665
+ };
1666
+ }
1667
+ function safeEqual(a, b) {
1668
+ const left = Buffer.from(a);
1669
+ const right = Buffer.from(b);
1670
+ if (left.length !== right.length) return false;
1671
+ return timingSafeEqual(left, right);
1672
+ }
1673
+ async function openBrowser(url) {
1674
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
1675
+ await new Promise((resolve5, reject) => {
1676
+ const child = spawn(command, args, {
1677
+ stdio: "ignore",
1678
+ // Detached so closing the terminal does not close the browser, and so
1679
+ // this process can exit without waiting for it.
1680
+ detached: true
1681
+ });
1682
+ child.once("error", reject);
1683
+ child.unref();
1684
+ resolve5();
1685
+ });
1686
+ }
1687
+ async function describe(response) {
1688
+ try {
1689
+ const body = await response.json();
1690
+ const error = body["error"];
1691
+ const description = body["error_description"];
1692
+ if (typeof description === "string") return `${String(error ?? response.status)} \u2014 ${description}`;
1693
+ if (typeof error === "string") return error;
1694
+ } catch {
1695
+ }
1696
+ return `HTTP ${response.status}`;
1697
+ }
1698
+
1699
+ // src/session.ts
1700
+ var NotSignedIn = class extends Error {
1701
+ constructor() {
1702
+ super(
1703
+ "not signed in. Run `pm auth login`, or set PERSISTMEMORY_API_KEY for a non-interactive session."
1704
+ );
1705
+ this.name = "NotSignedIn";
1706
+ }
1707
+ };
1708
+ var RENEW_BEFORE_MS = 6e4;
1709
+ function isExpired(credential, now = Date.now()) {
1710
+ if (credential.kind !== "oauth" || !credential.expiresAt) return false;
1711
+ const at = Date.parse(credential.expiresAt);
1712
+ return Number.isFinite(at) && at - RENEW_BEFORE_MS <= now;
1713
+ }
1714
+ async function clientFor(resolved, deps) {
1715
+ const credential = await currentCredential(resolved, deps);
1716
+ return new PersistMemory({
1717
+ apiKey: credential.token,
1718
+ baseUrl: resolved.apiUrl,
1719
+ ...deps.userAgent ? { userAgent: deps.userAgent } : {},
1720
+ fetch: deps.fetch
1721
+ });
1722
+ }
1723
+ async function currentCredential(resolved, deps) {
1724
+ const credential = resolved.credential;
1725
+ if (!credential) throw new NotSignedIn();
1726
+ if (!isExpired(credential, deps.now?.() ?? Date.now())) return credential;
1727
+ if (!credential.refreshToken || !resolved.clientId) {
1728
+ throw new Error("your session has expired. Run `pm auth login` to sign in again.");
1729
+ }
1730
+ const renewed = await refresh({
1731
+ apiUrl: resolved.apiUrl,
1732
+ clientId: resolved.clientId,
1733
+ refreshToken: credential.refreshToken,
1734
+ deps
1735
+ });
1736
+ writeCredentials(deps.paths, {
1737
+ ...readCredentials(deps.paths),
1738
+ [resolved.profile]: renewed
1739
+ });
1740
+ return renewed;
1741
+ }
1742
+
1743
+ // src/context.ts
1744
+ import { createInterface } from "node:readline";
1745
+ var ETX = "";
1746
+ var DELETE = "\x7F";
1747
+ var BACKSPACE = "\b";
1748
+ async function readSecretFromTty(prompt) {
1749
+ const input = process.stdin;
1750
+ if (!input.isTTY) {
1751
+ return new Promise((resolve5) => {
1752
+ const readline = createInterface({ input });
1753
+ readline.once("line", (line) => {
1754
+ readline.close();
1755
+ resolve5(line.trim());
1756
+ });
1757
+ readline.once("close", () => resolve5(""));
1758
+ });
1759
+ }
1760
+ process.stdout.write(prompt);
1761
+ const previouslyRaw = input.isRaw ?? false;
1762
+ input.setRawMode?.(true);
1763
+ input.resume();
1764
+ input.setEncoding("utf8");
1765
+ return new Promise((resolve5) => {
1766
+ let value = "";
1767
+ const finish = () => {
1768
+ input.removeListener("data", onData);
1769
+ input.setRawMode?.(previouslyRaw);
1770
+ input.pause();
1771
+ process.stdout.write("\n");
1772
+ resolve5(value.trim());
1773
+ };
1774
+ const onData = (chunk) => {
1775
+ for (const character of chunk) {
1776
+ switch (character) {
1777
+ case "\r":
1778
+ case "\n":
1779
+ finish();
1780
+ return;
1781
+ case ETX:
1782
+ input.setRawMode?.(previouslyRaw);
1783
+ process.stdout.write("\n");
1784
+ process.exit(130);
1785
+ return;
1786
+ case DELETE:
1787
+ case BACKSPACE:
1788
+ value = value.slice(0, -1);
1789
+ break;
1790
+ default:
1791
+ if (character >= " ") value += character;
1792
+ }
1793
+ }
1794
+ };
1795
+ input.on("data", onData);
1796
+ });
1797
+ }
1798
+ async function readStdin() {
1799
+ const chunks = [];
1800
+ for await (const chunk of process.stdin) {
1801
+ chunks.push(Buffer.from(chunk));
1802
+ }
1803
+ return Buffer.concat(chunks).toString("utf8");
1804
+ }
1805
+
1806
+ // src/commands/agent.ts
1807
+ import { hostname } from "node:os";
1808
+ import { homedir as homedir2 } from "node:os";
1809
+ import { resolve as resolve3 } from "node:path";
1810
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
1811
+
1812
+ // src/files.ts
1813
+ import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
1814
+ import { dirname, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
1815
+ var OutsideWorkspace = class extends Error {
1816
+ constructor(path) {
1817
+ super(`${path} is outside the directory this session was started in.`);
1818
+ this.name = "OutsideWorkspace";
1819
+ }
1820
+ };
1821
+ var TooLarge = class extends Error {
1822
+ constructor(path, bytes, limit) {
1823
+ super(`${path} is ${Math.round(bytes / 1024)} KB, over the ${Math.round(limit / 1024)} KB limit.`);
1824
+ this.name = "TooLarge";
1825
+ }
1826
+ };
1827
+ var MAX_READ_BYTES = 512 * 1024;
1828
+ function realLocation(absolute) {
1829
+ let existing = absolute;
1830
+ const trailing = [];
1831
+ while (!existsSync2(existing)) {
1832
+ const parent = dirname(existing);
1833
+ if (parent === existing) return absolute;
1834
+ trailing.unshift(existing.slice(parent.length + 1));
1835
+ existing = parent;
1836
+ }
1837
+ try {
1838
+ return join2(realpathSync(existing), ...trailing);
1839
+ } catch {
1840
+ return absolute;
1841
+ }
1842
+ }
1843
+ function within(root, path) {
1844
+ const absolute = isAbsolute(path) ? path : resolve2(root, path);
1845
+ const real = realLocation(absolute);
1846
+ const realRoot = realLocation(resolve2(root));
1847
+ const rel = relative(realRoot, real);
1848
+ if (rel !== "" && (rel.startsWith("..") || isAbsolute(rel))) {
1849
+ throw new OutsideWorkspace(path);
1850
+ }
1851
+ return real;
1852
+ }
1853
+ function readWithin(root, path) {
1854
+ const absolute = within(root, path);
1855
+ const stats = statSync(absolute);
1856
+ if (!stats.isFile()) throw new Error(`${path} is not a file.`);
1857
+ if (stats.size > MAX_READ_BYTES) throw new TooLarge(path, stats.size, MAX_READ_BYTES);
1858
+ return {
1859
+ path: absolute,
1860
+ text: readFileSync2(absolute, "utf8"),
1861
+ bytes: stats.size
1862
+ };
1863
+ }
1864
+ function proposeWrite(root, path, contents) {
1865
+ const absolute = within(root, path);
1866
+ let existing;
1867
+ try {
1868
+ const stats = statSync(absolute);
1869
+ if (stats.isFile() && stats.size <= MAX_READ_BYTES) {
1870
+ existing = readFileSync2(absolute, "utf8");
1871
+ }
1872
+ } catch {
1873
+ }
1874
+ return {
1875
+ path: absolute,
1876
+ contents,
1877
+ ...existing !== void 0 ? { existing } : {}
1878
+ };
1879
+ }
1880
+ function commitWrite(write2, confirmed) {
1881
+ if (!confirmed) throw new Error("refusing to write without confirmation");
1882
+ writeFileSync2(write2.path, write2.contents, "utf8");
1883
+ }
1884
+ function summarise(write2, maxLines = 40) {
1885
+ if (write2.existing === void 0) {
1886
+ const lines = write2.contents.split("\n");
1887
+ const head = lines.slice(0, maxLines).map((line) => `+ ${line}`);
1888
+ if (lines.length > maxLines) head.push(` \u2026 ${lines.length - maxLines} more lines`);
1889
+ return `create ${write2.path} (${lines.length} lines)
1890
+ ${head.join("\n")}`;
1891
+ }
1892
+ if (write2.existing === write2.contents) return `${write2.path} is already exactly this.`;
1893
+ const before = write2.existing.split("\n");
1894
+ const after = write2.contents.split("\n");
1895
+ const changes = [];
1896
+ let start = 0;
1897
+ while (start < before.length && start < after.length && before[start] === after[start]) {
1898
+ start += 1;
1899
+ }
1900
+ let end = 0;
1901
+ while (end < before.length - start && end < after.length - start && before[before.length - 1 - end] === after[after.length - 1 - end]) {
1902
+ end += 1;
1903
+ }
1904
+ const removed = before.slice(start, before.length - end);
1905
+ const added = after.slice(start, after.length - end);
1906
+ for (const line of removed.slice(0, maxLines)) changes.push(`- ${line}`);
1907
+ if (removed.length > maxLines) changes.push(` \u2026 ${removed.length - maxLines} more removed`);
1908
+ for (const line of added.slice(0, maxLines)) changes.push(`+ ${line}`);
1909
+ if (added.length > maxLines) changes.push(` \u2026 ${added.length - maxLines} more added`);
1910
+ return [
1911
+ `edit ${write2.path} (line ${start + 1}: -${removed.length} +${added.length})`,
1912
+ ...changes
1913
+ ].join("\n");
1914
+ }
1915
+
1916
+ // src/commands/agent.ts
1917
+ async function said(response, fallback) {
1918
+ const body = await response.json().catch(() => void 0);
1919
+ const message2 = typeof body === "object" && body !== null && "error" in body ? body.error?.message : void 0;
1920
+ return message2 ?? `${fallback} (${response.status})`;
1921
+ }
1922
+ function locate(roots, requested) {
1923
+ const expanded = requested.startsWith("~") ? resolve3(homedir2(), requested.slice(1).replace(/^[/\\]/, "")) : requested;
1924
+ for (const root of roots) {
1925
+ try {
1926
+ return within(root, expanded);
1927
+ } catch (error) {
1928
+ if (!(error instanceof OutsideWorkspace)) throw error;
1929
+ }
1930
+ }
1931
+ throw new Error(`${requested} is not inside any allowed folder (${roots.join(", ")})`);
1932
+ }
1933
+ async function answer(context, apiUrl, token, roots, request) {
1934
+ let located;
1935
+ try {
1936
+ located = locate(roots, request.path);
1937
+ } catch (error) {
1938
+ return { ok: false, error: error instanceof Error ? error.message : "refused" };
1939
+ }
1940
+ let bytes;
1941
+ try {
1942
+ const stats = statSync2(located);
1943
+ if (!stats.isFile()) return { ok: false, error: `${request.path} is not a file.` };
1944
+ if (stats.size > MAX_READ_BYTES) {
1945
+ return {
1946
+ ok: false,
1947
+ error: new TooLarge(request.path, stats.size, MAX_READ_BYTES).message
1948
+ };
1949
+ }
1950
+ bytes = readFileSync3(located);
1951
+ } catch (error) {
1952
+ return { ok: false, error: error instanceof Error ? error.message : "could not read it" };
1953
+ }
1954
+ const grant = await fetch(`${apiUrl}/api/v1/agent/upload-url`, {
1955
+ method: "POST",
1956
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1957
+ body: JSON.stringify({
1958
+ // The name the person asked for, not the resolved path. The resolved one
1959
+ // says where this machine keeps things, which the server has no business
1960
+ // recording.
1961
+ //
1962
+ // No content type is sent: this machine has a path, not a declaration.
1963
+ // The server resolves it from the name against the one table that knows
1964
+ // which types it can read, and tells us below what it decided.
1965
+ filename: request.path.split("/").pop() ?? "file"
1966
+ })
1967
+ });
1968
+ if (!grant.ok) {
1969
+ return { ok: false, error: await said(grant, "could not get an upload url") };
1970
+ }
1971
+ const { uploadUrl, contentType } = await grant.json();
1972
+ const put = await fetch(uploadUrl, {
1973
+ method: "PUT",
1974
+ // The type the grant was signed for. Anything else is refused.
1975
+ headers: { "content-type": contentType },
1976
+ body: new Uint8Array(bytes)
1977
+ });
1978
+ if (!put.ok) return { ok: false, error: `upload refused (${put.status})` };
1979
+ const stored = await put.json().catch(() => ({}));
1980
+ if (!stored.attachToken) return { ok: false, error: "the upload returned no reference" };
1981
+ return { ok: true, attachToken: stored.attachToken, bytes: bytes.length };
1982
+ }
1983
+ async function agentCommand(context) {
1984
+ const credential = context.resolved.credential;
1985
+ if (!credential) {
1986
+ context.error("Sign in first: pm auth login");
1987
+ return 1;
1988
+ }
1989
+ const roots = (listFlag(context.args, "root") ?? []).map(
1990
+ (one) => resolve3(one.startsWith("~") ? resolve3(homedir2(), one.slice(1).replace(/^[/\\]/, "")) : one)
1991
+ );
1992
+ if (roots.length === 0) {
1993
+ context.error(
1994
+ "Say which folders this machine may read from, and nothing outside them will be:"
1995
+ );
1996
+ context.error(" pm agent --root ~/notes --root ~/projects");
1997
+ return 1;
1998
+ }
1999
+ for (const root of roots) {
2000
+ try {
2001
+ if (!statSync2(root).isDirectory()) {
2002
+ context.error(`${root} is not a folder.`);
2003
+ return 1;
2004
+ }
2005
+ } catch {
2006
+ context.error(`${root} does not exist.`);
2007
+ return 1;
2008
+ }
2009
+ }
2010
+ const apiUrl = context.resolved.apiUrl.replace(/\/+$/, "");
2011
+ const name = hostname();
2012
+ const asked = numberFlag(context.args, "interval");
2013
+ if (asked === "invalid") {
2014
+ context.error("--interval takes a number of seconds.");
2015
+ return 1;
2016
+ }
2017
+ const every = Math.max(2, asked ?? 5) * 1e3;
2018
+ context.print(`Answering as ${name}, from: ${roots.join(", ")}`);
2019
+ context.print("Nothing outside those folders can be read. Ctrl-C to stop.");
2020
+ let running = true;
2021
+ const stop = () => {
2022
+ running = false;
2023
+ context.print("\nStopping. The current request will finish first.");
2024
+ };
2025
+ process.on("SIGINT", stop);
2026
+ process.on("SIGTERM", stop);
2027
+ const call = async (path, body) => fetch(`${apiUrl}/api/v1/agent/${path}`, {
2028
+ method: "POST",
2029
+ headers: {
2030
+ authorization: `Bearer ${credential.token}`,
2031
+ "content-type": "application/json"
2032
+ },
2033
+ body: JSON.stringify(body)
2034
+ });
2035
+ let complaint;
2036
+ const complain = (message2) => {
2037
+ if (complaint === message2) return;
2038
+ complaint = message2;
2039
+ context.error(` ${message2}`);
2040
+ };
2041
+ const working = () => {
2042
+ if (complaint !== void 0) {
2043
+ complaint = void 0;
2044
+ context.print(" connected again");
2045
+ }
2046
+ };
2047
+ while (running) {
2048
+ try {
2049
+ const beat = await call("heartbeat", { hostname: name, platform: process.platform });
2050
+ if (!beat.ok) {
2051
+ complain(await said(beat, "the service refused this machine"));
2052
+ } else {
2053
+ working();
2054
+ const state = await beat.json();
2055
+ if (state.status === "disabled") {
2056
+ context.print("This machine is switched off after going quiet. Ask an operator to re-enable it.");
2057
+ await new Promise((r) => setTimeout(r, 6e4));
2058
+ continue;
2059
+ }
2060
+ if (state.released > 0) {
2061
+ context.print(`Reconnected. ${state.released} request(s) were waiting.`);
2062
+ }
2063
+ }
2064
+ const claimed = await call("claim", { hostname: name, limit: 5 });
2065
+ if (!claimed.ok) {
2066
+ complain(await said(claimed, "could not pick up work"));
2067
+ } else {
2068
+ const { items } = await claimed.json();
2069
+ for (const request of items) {
2070
+ context.print(`Reading ${request.path}`);
2071
+ const outcome = await answer(context, apiUrl, credential.token, roots, request);
2072
+ const done = await call(
2073
+ `complete/${encodeURIComponent(request.id)}`,
2074
+ outcome.ok ? { result: { attachToken: outcome.attachToken } } : { error: outcome.error }
2075
+ );
2076
+ if (!done.ok) {
2077
+ context.error(` stored, but the service did not record it: ${await said(done, "refused")}`);
2078
+ continue;
2079
+ }
2080
+ context.print(outcome.ok ? ` sent ${outcome.bytes} bytes` : ` refused: ${outcome.error}`);
2081
+ }
2082
+ }
2083
+ } catch (error) {
2084
+ context.error(` ${error instanceof Error ? error.message : "connection failed"}`);
2085
+ }
2086
+ if (running) await new Promise((r) => setTimeout(r, every));
2087
+ }
2088
+ return 0;
2089
+ }
2090
+
2091
+ // src/commands/requests.ts
2092
+ async function requestsCommand(context) {
2093
+ const credential = context.resolved.credential;
2094
+ if (!credential) {
2095
+ context.error("Sign in first: pm auth login");
2096
+ return 1;
2097
+ }
2098
+ const apiUrl = context.resolved.apiUrl.replace(/\/+$/, "");
2099
+ const response = await fetch(`${apiUrl}/api/v1/agent/awaiting`, {
2100
+ headers: { authorization: `Bearer ${credential.token}` }
2101
+ });
2102
+ if (response.status === 403) {
2103
+ context.error(
2104
+ "This credential cannot see the approval queue. It is visible only to you, signed in, at /dashboard/requests."
2105
+ );
2106
+ return 3;
2107
+ }
2108
+ if (!response.ok) {
2109
+ context.error(`Could not read the queue (${response.status}).`);
2110
+ return 1;
2111
+ }
2112
+ const { items } = await response.json();
2113
+ if (items.length === 0) {
2114
+ context.print("Nothing is waiting. No app or chat has asked for a file from your computers.");
2115
+ return 0;
2116
+ }
2117
+ context.print(
2118
+ `${items.length} request${items.length === 1 ? "" : "s"} waiting. Nothing has been read.`
2119
+ );
2120
+ context.print("");
2121
+ for (const one of items) {
2122
+ context.print(` ${one.path}`);
2123
+ context.print(` asked by ${one.askedBy ?? "something"} \xB7 ${one.id}`);
2124
+ context.print("");
2125
+ }
2126
+ context.print("Approve or refuse them while signed in, at /dashboard/requests.");
2127
+ context.print("They cannot be approved from here \u2014 see `pm help requests`.");
2128
+ return 0;
2129
+ }
2130
+
2131
+ // src/commands/auth.ts
2132
+ async function authCommand(context) {
2133
+ const action = context.args.words[1] ?? "status";
2134
+ switch (action) {
2135
+ case "login":
2136
+ return login(context);
2137
+ case "logout":
2138
+ return logout(context);
2139
+ case "status":
2140
+ return status(context);
2141
+ default:
2142
+ context.error(`Unknown command "pm auth ${action}". Try login, logout or status.`);
2143
+ return 2;
2144
+ }
2145
+ }
2146
+ var SCOPE = "memory:read memory:capture memory:write usage:read";
2147
+ async function login(context) {
2148
+ const { flags } = context;
2149
+ const profile = flags.profile ?? DEFAULT_PROFILE;
2150
+ const apiUrl = context.resolved.apiUrl;
2151
+ if (flags.apiKey !== void 0) {
2152
+ const key = flags.apiKey === true ? await context.readSecret("API key: ") : flags.apiKey;
2153
+ if (!key) {
2154
+ context.error("No API key was given.");
2155
+ return 2;
2156
+ }
2157
+ saveLogin({
2158
+ paths: context.paths,
2159
+ profile,
2160
+ apiUrl,
2161
+ credential: { kind: "api-key", token: key }
2162
+ });
2163
+ context.print(`Signed in to ${apiUrl} as profile "${profile}" with an API key.`);
2164
+ return 0;
2165
+ }
2166
+ try {
2167
+ const { credential, clientId } = await loginWithBrowser({
2168
+ apiUrl,
2169
+ scope: SCOPE,
2170
+ clientId: context.resolved.clientId,
2171
+ deps: context.oauth
2172
+ });
2173
+ saveLogin({
2174
+ paths: context.paths,
2175
+ profile,
2176
+ apiUrl,
2177
+ credential,
2178
+ clientId
2179
+ });
2180
+ context.print(`
2181
+ Signed in to ${apiUrl} as profile "${profile}".`);
2182
+ return 0;
2183
+ } catch (error) {
2184
+ context.error(message(error));
2185
+ return 1;
2186
+ }
2187
+ }
2188
+ function logout(context) {
2189
+ const profile = context.flags.profile ?? context.resolved.profile;
2190
+ const forgotten = clearLogin(context.paths, profile);
2191
+ context.print(
2192
+ forgotten ? `Signed out of profile "${profile}".` : `Profile "${profile}" was not signed in.`
2193
+ );
2194
+ if (process.env["PERSISTMEMORY_API_KEY"]) {
2195
+ context.print(
2196
+ "PERSISTMEMORY_API_KEY is still set in this shell, and takes precedence. Unset it to finish signing out."
2197
+ );
2198
+ }
2199
+ return 0;
2200
+ }
2201
+ async function status(context) {
2202
+ const config = readConfig(context.paths);
2203
+ const resolved = context.resolved;
2204
+ if (!resolved.credential) {
2205
+ context.print(`Not signed in. Run \`pm auth login\`.
2206
+
2207
+ api url ${resolved.apiUrl}`);
2208
+ return 1;
2209
+ }
2210
+ const lines = [
2211
+ ` profile ${resolved.profile}${config.current === resolved.profile ? " (current)" : ""}`,
2212
+ ` api url ${resolved.apiUrl}`,
2213
+ ` method ${resolved.credential.kind === "api-key" ? "API key" : "browser sign-in"}`,
2214
+ ` token ${maskToken(resolved.credential.token)}`
2215
+ ];
2216
+ if (resolved.fromEnvironment) {
2217
+ lines.push(" source PERSISTMEMORY_API_KEY (overrides the stored profile)");
2218
+ }
2219
+ if (resolved.credential.expiresAt) {
2220
+ lines.push(` expires ${resolved.credential.expiresAt}`);
2221
+ }
2222
+ if (resolved.credential.scope) {
2223
+ lines.push(` scope ${resolved.credential.scope}`);
2224
+ }
2225
+ context.print(lines.join("\n"));
2226
+ try {
2227
+ await currentCredential(resolved, context.session);
2228
+ const client = await context.client();
2229
+ await client.health.ready();
2230
+ context.print("\n The server accepted this credential.");
2231
+ return 0;
2232
+ } catch (error) {
2233
+ context.print(`
2234
+ The server did NOT accept this credential: ${message(error)}`);
2235
+ return 1;
2236
+ }
2237
+ }
2238
+ function message(error) {
2239
+ return error instanceof Error ? error.message : String(error);
2240
+ }
2241
+
2242
+ // src/commands/session.ts
2243
+ import { createInterface as createInterface2 } from "node:readline";
2244
+ import { randomUUID } from "node:crypto";
2245
+ import { relative as relative2 } from "node:path";
2246
+
2247
+ // src/events.ts
2248
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4 } from "node:fs";
2249
+ import { join as join3 } from "node:path";
2250
+ function openSessionLog(paths, id) {
2251
+ const directory = join3(paths.dir, "sessions");
2252
+ mkdirSync2(directory, { recursive: true, mode: 448 });
2253
+ const path = join3(directory, `${id}.jsonl`);
2254
+ return {
2255
+ id,
2256
+ path,
2257
+ append(event) {
2258
+ try {
2259
+ appendFileSync(path, `${JSON.stringify(event)}
2260
+ `, { mode: 384 });
2261
+ } catch {
2262
+ }
2263
+ },
2264
+ read() {
2265
+ if (!existsSync3(path)) return [];
2266
+ return readFileSync4(path, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
2267
+ try {
2268
+ return [JSON.parse(line)];
2269
+ } catch {
2270
+ return [];
2271
+ }
2272
+ });
2273
+ }
2274
+ };
2275
+ }
2276
+ function turnsFrom(events) {
2277
+ const turns = [];
2278
+ for (const event of events) {
2279
+ if (event.kind === "prompt") turns.push({ role: "user", content: event.text });
2280
+ if (event.kind === "reply") turns.push({ role: "assistant", content: event.text });
2281
+ }
2282
+ return turns;
2283
+ }
2284
+ function totalUsage(events) {
2285
+ let turns = 0;
2286
+ let inputTokens = 0;
2287
+ let outputTokens = 0;
2288
+ for (const event of events) {
2289
+ if (event.kind !== "reply") continue;
2290
+ turns += 1;
2291
+ inputTokens += event.usage?.inputTokens ?? 0;
2292
+ outputTokens += event.usage?.outputTokens ?? 0;
2293
+ }
2294
+ return { turns, inputTokens, outputTokens };
2295
+ }
2296
+
2297
+ // src/commands/session.ts
2298
+ var HELP2 = `
2299
+ Commands
2300
+
2301
+ /read <path> read a file into the conversation
2302
+ /capture <path> read a file AND remember it
2303
+ /write <path> write the last reply to a file (asks first)
2304
+ /remember <text> remember something directly
2305
+ /usage what this session has cost
2306
+ /new start a fresh conversation
2307
+ /exit leave (Ctrl-D also works)
2308
+
2309
+ Anything else is a question, answered from your memory.
2310
+ `;
2311
+ async function sessionCommand(context) {
2312
+ const resumeId = stringFlag(context.args, "resume");
2313
+ const id = resumeId ?? randomUUID();
2314
+ const log = openSessionLog(context.paths, id);
2315
+ const state = { turns: [], attached: [] };
2316
+ if (resumeId) {
2317
+ const previous = log.read();
2318
+ state.turns = turnsFrom(previous);
2319
+ if (state.turns.length === 0) {
2320
+ context.error(`No session "${resumeId}" to resume.`);
2321
+ return 1;
2322
+ }
2323
+ context.print(`Resumed session ${resumeId} \u2014 ${state.turns.length} turns.`);
2324
+ }
2325
+ try {
2326
+ await context.client();
2327
+ } catch (error) {
2328
+ context.error(error instanceof Error ? error.message : String(error));
2329
+ return 3;
2330
+ }
2331
+ log.append({
2332
+ kind: "session.started",
2333
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2334
+ cwd: process.cwd(),
2335
+ apiUrl: context.resolved.apiUrl,
2336
+ profile: context.resolved.profile
2337
+ });
2338
+ context.print(`
2339
+ PersistMemory \u2014 session ${id.slice(0, 8)}`);
2340
+ context.print(` Ask anything. /help for commands, /exit to leave.
2341
+ `);
2342
+ const readline = createInterface2({ input: process.stdin, output: process.stdout });
2343
+ const ask = (prompt) => new Promise((resolve5) => {
2344
+ readline.question(prompt, resolve5);
2345
+ readline.once("close", () => resolve5(void 0));
2346
+ });
2347
+ const root = process.cwd();
2348
+ let running = true;
2349
+ while (running) {
2350
+ const line = await ask("> ");
2351
+ if (line === void 0) break;
2352
+ const input = line.trim();
2353
+ if (input === "") continue;
2354
+ try {
2355
+ running = await handleInput({ input, context, state, log, root, ask });
2356
+ } catch (error) {
2357
+ const message2 = error instanceof Error ? error.message : String(error);
2358
+ log.append({ kind: "error", at: (/* @__PURE__ */ new Date()).toISOString(), message: message2 });
2359
+ context.error(` ${message2}`);
2360
+ }
2361
+ }
2362
+ readline.close();
2363
+ const totals = totalUsage(log.read());
2364
+ log.append({ kind: "session.ended", at: (/* @__PURE__ */ new Date()).toISOString(), turns: totals.turns });
2365
+ context.print(
2366
+ `
2367
+ ${totals.turns} turn${totals.turns === 1 ? "" : "s"}` + (totals.inputTokens + totals.outputTokens > 0 ? `, ${totals.inputTokens + totals.outputTokens} tokens` : "") + `
2368
+ Transcript: ${log.path}
2369
+ Resume with: pm chat --resume ${id}
2370
+ `
2371
+ );
2372
+ return 0;
2373
+ }
2374
+ async function handleInput(args) {
2375
+ const { input, context, state, log, root } = args;
2376
+ if (!input.startsWith("/")) {
2377
+ await answer2(args);
2378
+ return true;
2379
+ }
2380
+ const [command, ...rest] = input.slice(1).split(/\s+/);
2381
+ const argument = rest.join(" ").trim();
2382
+ switch (command) {
2383
+ case "exit":
2384
+ case "quit":
2385
+ return false;
2386
+ case "help":
2387
+ context.print(HELP2);
2388
+ return true;
2389
+ case "usage": {
2390
+ const totals = totalUsage(log.read());
2391
+ context.print(
2392
+ ` ${totals.turns} turns, ${totals.inputTokens} in / ${totals.outputTokens} out tokens`
2393
+ );
2394
+ return true;
2395
+ }
2396
+ case "new":
2397
+ delete state.conversationId;
2398
+ state.turns = [];
2399
+ state.attached = [];
2400
+ delete state.lastReply;
2401
+ context.print(" Starting a fresh conversation.");
2402
+ return true;
2403
+ case "read":
2404
+ case "capture": {
2405
+ if (!argument) {
2406
+ context.error(` /${command} needs a path.`);
2407
+ return true;
2408
+ }
2409
+ const file = readWithin(root, argument);
2410
+ state.attached.push({ path: file.path, text: file.text });
2411
+ log.append({
2412
+ kind: "file.read",
2413
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2414
+ path: file.path,
2415
+ bytes: file.bytes
2416
+ });
2417
+ context.print(` read ${relative2(root, file.path)} (${Math.round(file.bytes / 1024)} KB)`);
2418
+ if (command === "capture") {
2419
+ const client = await context.client();
2420
+ const result = await client.memories.remember(
2421
+ { text: file.text, title: relative2(root, file.path) },
2422
+ { idempotencyKey: `cli:capture:${file.path}:${file.bytes}` }
2423
+ );
2424
+ log.append({
2425
+ kind: "file.captured",
2426
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2427
+ path: file.path,
2428
+ jobId: result.jobId
2429
+ });
2430
+ context.print(` queued for extraction (job ${result.jobId})`);
2431
+ }
2432
+ return true;
2433
+ }
2434
+ case "remember": {
2435
+ if (!argument) {
2436
+ context.error(" /remember needs something to remember.");
2437
+ return true;
2438
+ }
2439
+ const client = await context.client();
2440
+ const result = await client.memories.remember({ text: argument });
2441
+ context.print(` queued for extraction (job ${result.jobId})`);
2442
+ return true;
2443
+ }
2444
+ case "write":
2445
+ await write({ ...args, path: argument });
2446
+ return true;
2447
+ default:
2448
+ context.error(` Unknown command /${command ?? ""}. Try /help.`);
2449
+ return true;
2450
+ }
2451
+ }
2452
+ async function answer2(args) {
2453
+ const { input, context, state, log } = args;
2454
+ const attached = state.attached.map((file) => `--- ${file.path} ---
2455
+ ${file.text}`).join("\n\n");
2456
+ const content = attached ? `${attached}
2457
+
2458
+ ---
2459
+
2460
+ ${input}` : input;
2461
+ log.append({ kind: "prompt", at: (/* @__PURE__ */ new Date()).toISOString(), text: input });
2462
+ state.turns.push({ role: "user", content });
2463
+ const client = await context.client();
2464
+ const response = await client.request("POST", "/api/v1/chat", {
2465
+ messages: state.turns.slice(-20),
2466
+ ...state.conversationId ? { conversationId: state.conversationId } : {}
2467
+ });
2468
+ const reply = response.message.content;
2469
+ state.turns.push({ role: "assistant", content: reply });
2470
+ state.lastReply = reply;
2471
+ if (response.conversationId) state.conversationId = response.conversationId;
2472
+ state.attached = [];
2473
+ log.append({
2474
+ kind: "reply",
2475
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2476
+ text: reply,
2477
+ citations: response.citations?.length ?? 0,
2478
+ ...response.diagnostics?.usage ? { usage: response.diagnostics.usage } : {},
2479
+ ...response.diagnostics?.model ? { model: response.diagnostics.model } : {}
2480
+ });
2481
+ context.print(`
2482
+ ${reply}
2483
+ `);
2484
+ if (response.citations?.length) {
2485
+ const historical = response.citations.filter((one) => one.historical).length;
2486
+ context.print(
2487
+ ` from ${response.citations.length} memor${response.citations.length === 1 ? "y" : "ies"}` + // Never omitted. A superseded memory presented as current is the most
2488
+ // damaging thing this system can do.
2489
+ (historical > 0 ? `, ${historical} no longer current` : "")
2490
+ );
2491
+ }
2492
+ if (response.diagnostics?.degraded && !context.flags.quiet) {
2493
+ context.error(" Note: answered without the semantic index, so this may be narrower than usual.");
2494
+ }
2495
+ }
2496
+ async function write(args) {
2497
+ const { context, state, log, root } = args;
2498
+ if (!args.path) {
2499
+ context.error(" /write needs a path.");
2500
+ return;
2501
+ }
2502
+ if (!state.lastReply) {
2503
+ context.error(" Nothing to write yet \u2014 ask something first.");
2504
+ return;
2505
+ }
2506
+ const proposed = proposeWrite(root, args.path, `${state.lastReply}
2507
+ `);
2508
+ context.print("");
2509
+ context.print(summarise(proposed));
2510
+ context.print("");
2511
+ const reply = (await args.ask(" Write it? [y/N] "))?.trim().toLowerCase();
2512
+ const approved = reply === "y" || reply === "yes";
2513
+ log.append({
2514
+ kind: "file.write",
2515
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2516
+ path: proposed.path,
2517
+ approved,
2518
+ ...approved ? { bytes: Buffer.byteLength(proposed.contents) } : {}
2519
+ });
2520
+ if (!approved) {
2521
+ context.print(" Not written.");
2522
+ return;
2523
+ }
2524
+ commitWrite(proposed, true);
2525
+ context.print(` Wrote ${relative2(root, proposed.path)}.`);
2526
+ }
2527
+
2528
+ // src/commands/memory.ts
2529
+ import { readFileSync as readFileSync5 } from "node:fs";
2530
+ var memoryColumns = [
2531
+ { header: "id", value: (m) => m.id },
2532
+ { header: "type", value: (m) => m.type },
2533
+ { header: "title", value: (m) => m.title },
2534
+ { header: "confidence", value: (m) => m.confidence.toFixed(2) },
2535
+ { header: "updated", value: (m) => shortDate(m.updatedAt) }
2536
+ ];
2537
+ var memoryFields = [
2538
+ { header: "id", value: (m) => m.id },
2539
+ { header: "type", value: (m) => m.type },
2540
+ { header: "state", value: (m) => m.state },
2541
+ { header: "title", value: (m) => m.title },
2542
+ { header: "content", value: (m) => m.content },
2543
+ { header: "confidence", value: (m) => m.confidence.toFixed(2) },
2544
+ { header: "importance", value: (m) => m.importance.toFixed(2) },
2545
+ { header: "entities", value: (m) => m.entities.map((e) => e.name).join(", ") },
2546
+ { header: "spaces", value: (m) => (m.spaceIds ?? []).join(", ") },
2547
+ { header: "version", value: (m) => String(m.version) },
2548
+ { header: "created", value: (m) => shortDate(m.createdAt) },
2549
+ { header: "updated", value: (m) => shortDate(m.updatedAt) }
2550
+ ];
2551
+ async function rememberCommand(context) {
2552
+ const positional = context.args.words.slice(1);
2553
+ const file = stringFlag(context.args, "file", "f");
2554
+ let text;
2555
+ if (file) {
2556
+ try {
2557
+ text = readFileSync5(file, "utf8");
2558
+ } catch {
2559
+ context.error(`Could not read ${file}.`);
2560
+ return 1;
2561
+ }
2562
+ } else if (positional[0] === "-" || positional.length === 0 && !process.stdin.isTTY) {
2563
+ text = await readStdin();
2564
+ } else {
2565
+ text = positional.join(" ");
2566
+ }
2567
+ if (text.trim() === "") {
2568
+ context.error("Nothing to remember. Pass text, a --file, or pipe something in.");
2569
+ return 2;
2570
+ }
2571
+ const client = await context.client();
2572
+ const spaceIds = listFlag(context.args, "space", "spaces");
2573
+ const title = stringFlag(context.args, "title");
2574
+ const result = await client.memories.remember(
2575
+ {
2576
+ text,
2577
+ ...title ? { title } : {},
2578
+ ...spaceIds ? { spaceIds } : {}
2579
+ },
2580
+ {
2581
+ /**
2582
+ * A key derived from the CONTENT, not a random one.
2583
+ *
2584
+ * The SDK will not retry a POST without one, and a random value per
2585
+ * attempt would defeat the point: a request that timed out after the
2586
+ * server accepted it would be captured twice. Same text, same key, one
2587
+ * memory — which is what a person re-running a failed command expects.
2588
+ */
2589
+ idempotencyKey: `cli:remember:${hash(text)}`
2590
+ }
2591
+ );
2592
+ if (context.flags.output !== "table") {
2593
+ context.print(renderOne(result, [
2594
+ { header: "status", value: (r) => r.status },
2595
+ { header: "jobId", value: (r) => r.jobId },
2596
+ { header: "note", value: (r) => r.note }
2597
+ ], { format: context.flags.output }));
2598
+ return 0;
2599
+ }
2600
+ context.print(`Accepted for processing. Job ${result.jobId}.`);
2601
+ context.print(result.note);
2602
+ return 0;
2603
+ }
2604
+ async function searchCommand(context) {
2605
+ const query = context.args.words.slice(1).join(" ").trim();
2606
+ if (query === "") {
2607
+ context.error('Nothing to search for. Try `pm search "what did we decide about Postgres"`.');
2608
+ return 2;
2609
+ }
2610
+ const limit = numberFlag(context.args, "limit", "n");
2611
+ if (limit === "invalid") {
2612
+ context.error("--limit must be a number.");
2613
+ return 2;
2614
+ }
2615
+ const client = await context.client();
2616
+ const spaceIds = listFlag(context.args, "space", "spaces");
2617
+ const response = await client.search.query({
2618
+ query,
2619
+ ...limit !== void 0 ? { limit } : {},
2620
+ ...spaceIds ? { spaceIds } : {},
2621
+ ...stringFlag(context.args, "as-of") ? { asOf: stringFlag(context.args, "as-of") } : {}
2622
+ });
2623
+ if (context.flags.output === "json" || context.flags.output === "yaml") {
2624
+ context.print(renderOne(response, [], { format: context.flags.output }));
2625
+ return 0;
2626
+ }
2627
+ const columns = [
2628
+ { header: "score", value: (r) => r.score.toFixed(3) },
2629
+ { header: "type", value: (r) => r.memory.type },
2630
+ { header: "title", value: (r) => r.memory.title },
2631
+ { header: "content", value: (r) => r.memory.content },
2632
+ { header: "id", value: (r) => r.memory.id }
2633
+ ];
2634
+ if (response.results.length === 0) {
2635
+ context.print(`Nothing found for "${response.query}".`);
2636
+ } else {
2637
+ context.print(render(response.results, columns, { format: context.flags.output }));
2638
+ }
2639
+ if (response.diagnostics.degraded && !context.flags.quiet) {
2640
+ const notice = response.diagnostics.notice ?? `search ran without ${(response.diagnostics.unavailable ?? ["some capabilities"]).join(", ")}`;
2641
+ context.error(`
2642
+ Note: these results are narrower than usual \u2014 ${notice}`);
2643
+ }
2644
+ return 0;
2645
+ }
2646
+ async function listMemoriesCommand(context) {
2647
+ const limit = numberFlag(context.args, "limit", "n");
2648
+ if (limit === "invalid") {
2649
+ context.error("--limit must be a number.");
2650
+ return 2;
2651
+ }
2652
+ const client = await context.client();
2653
+ const page = client.memories.list({
2654
+ ...limit !== void 0 ? { limit } : {},
2655
+ ...listFlag(context.args, "type") ? { type: listFlag(context.args, "type") } : {},
2656
+ ...listFlag(context.args, "space", "spaces") ? { spaceIds: listFlag(context.args, "space", "spaces") } : {}
2657
+ });
2658
+ const rows = context.args.flags["all"] ? await page.all(limit ?? 1e3) : (await page.first()).data;
2659
+ if (rows.length === 0) {
2660
+ context.print("No memories yet.");
2661
+ return 0;
2662
+ }
2663
+ context.print(render(rows, memoryColumns, { format: context.flags.output }));
2664
+ return 0;
2665
+ }
2666
+ async function getMemoryCommand(context) {
2667
+ const id = context.args.words[2];
2668
+ if (!id) {
2669
+ context.error("Which memory? Try `pm get memory <id>`.");
2670
+ return 2;
2671
+ }
2672
+ const client = await context.client();
2673
+ const memory = await client.memories.get(id);
2674
+ context.print(renderOne(memory, memoryFields, { format: context.flags.output }));
2675
+ return 0;
2676
+ }
2677
+ async function listSpacesCommand(context) {
2678
+ const client = await context.client();
2679
+ const { data } = await client.spaces.list().first();
2680
+ if (data.length === 0) {
2681
+ context.print("No Spaces yet.");
2682
+ return 0;
2683
+ }
2684
+ const columns = [
2685
+ { header: "id", value: (s) => s.id },
2686
+ { header: "name", value: (s) => s.name },
2687
+ { header: "kind", value: (s) => s.kind },
2688
+ { header: "memories", value: (s) => s.memoryCount === void 0 ? "" : String(s.memoryCount) },
2689
+ { header: "created", value: (s) => shortDate(s.createdAt) }
2690
+ ];
2691
+ context.print(render(data, columns, { format: context.flags.output }));
2692
+ return 0;
2693
+ }
2694
+ async function statusCommand(context) {
2695
+ const client = await context.client();
2696
+ const health = await client.health.ready();
2697
+ context.print(
2698
+ renderOne(health, [{ header: "status", value: (h) => JSON.stringify(h) }], {
2699
+ format: context.flags.output === "table" ? "yaml" : context.flags.output
2700
+ })
2701
+ );
2702
+ return 0;
2703
+ }
2704
+ function hash(text) {
2705
+ let value = 2166136261;
2706
+ for (let index = 0; index < text.length; index += 1) {
2707
+ value ^= text.charCodeAt(index);
2708
+ value = Math.imul(value, 16777619) >>> 0;
2709
+ }
2710
+ return value.toString(16).padStart(8, "0");
2711
+ }
2712
+
2713
+ // src/index.ts
2714
+ async function run(deps) {
2715
+ const args = parseArgs(deps.argv);
2716
+ const print = deps.stdout ?? ((line) => process.stdout.write(`${line}
2717
+ `));
2718
+ const error = deps.stderr ?? ((line) => process.stderr.write(`${line}
2719
+ `));
2720
+ if (boolFlag(args, "version", "v")) {
2721
+ print(VERSION);
2722
+ return 0;
2723
+ }
2724
+ if (boolFlag(args, "help", "h")) {
2725
+ print(HELP);
2726
+ return 0;
2727
+ }
2728
+ const flags = globalFlags(args, deps);
2729
+ if (flags === "invalid-output") {
2730
+ error(`--output must be one of table, json, yaml, csv, tsv.`);
2731
+ return 2;
2732
+ }
2733
+ if (flags === "invalid-limit") {
2734
+ error("--limit must be a number.");
2735
+ return 2;
2736
+ }
2737
+ const paths = deps.paths ?? pathsFor();
2738
+ const resolved = resolve({
2739
+ paths,
2740
+ profileFlag: flags.profile,
2741
+ apiUrlFlag: flags.apiUrl,
2742
+ // A prompted key is not known yet; only a literal one participates here.
2743
+ apiKeyFlag: typeof flags.apiKey === "string" ? flags.apiKey : void 0,
2744
+ ...deps.env ? { env: deps.env } : {}
2745
+ });
2746
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
2747
+ const oauth = {
2748
+ fetch: fetchImpl,
2749
+ ...deps.openBrowser ? { openBrowser: deps.openBrowser } : {},
2750
+ print
2751
+ };
2752
+ const session = { ...oauth, paths, userAgent: `persistmemory-cli/${VERSION}` };
2753
+ const context = {
2754
+ args,
2755
+ flags,
2756
+ paths,
2757
+ resolved,
2758
+ oauth,
2759
+ session,
2760
+ client: () => clientFor(resolved, session),
2761
+ print,
2762
+ error,
2763
+ readSecret: deps.readSecret ?? readSecretFromTty
2764
+ };
2765
+ if (args.words.length === 0) {
2766
+ if (!(deps.isTty ?? process.stdin.isTTY ?? false)) {
2767
+ print(HELP);
2768
+ return 0;
2769
+ }
2770
+ return sessionCommand(context);
2771
+ }
2772
+ try {
2773
+ return await dispatch(context);
2774
+ } catch (caught) {
2775
+ return report(caught, error);
2776
+ }
2777
+ }
2778
+ async function dispatch(context) {
2779
+ const [verb, noun] = context.args.words;
2780
+ switch (verb) {
2781
+ case "auth":
2782
+ return authCommand(context);
2783
+ case "agent":
2784
+ return agentCommand(context);
2785
+ case "chat":
2786
+ case "session":
2787
+ return sessionCommand(context);
2788
+ case "remember":
2789
+ return rememberCommand(context);
2790
+ case "search":
2791
+ return searchCommand(context);
2792
+ case "status":
2793
+ return statusCommand(context);
2794
+ case "requests":
2795
+ return requestsCommand(context);
2796
+ /**
2797
+ * `pm <verb> <noun>`, the grammar the Harness CLI uses.
2798
+ *
2799
+ * Worth copying rather than inventing: a person who has typed
2800
+ * `list pipelines` can guess `list memories` without opening the help, and
2801
+ * a consistent grammar is what lets a tool grow past the handful of
2802
+ * commands anybody can memorise.
2803
+ */
2804
+ case "list":
2805
+ if (noun === "memories" || noun === "memory") return listMemoriesCommand(context);
2806
+ if (noun === "spaces" || noun === "space") return listSpacesCommand(context);
2807
+ context.error(`Cannot list "${noun ?? ""}". Try memories or spaces.`);
2808
+ return 2;
2809
+ case "get":
2810
+ if (noun === "memory") return getMemoryCommand(context);
2811
+ context.error(`Cannot get "${noun ?? ""}". Try memory.`);
2812
+ return 2;
2813
+ default:
2814
+ context.error(`Unknown command "${verb ?? ""}". Run \`pm --help\`.`);
2815
+ return 2;
2816
+ }
2817
+ }
2818
+ function globalFlags(args, deps) {
2819
+ const requested = stringFlag(args, "output", "o");
2820
+ if (requested !== void 0 && !isOutputFormat(requested)) return "invalid-output";
2821
+ const limit = numberFlag(args, "limit", "n");
2822
+ if (limit === "invalid") return "invalid-limit";
2823
+ const isTty = deps.isTty ?? process.stdout.isTTY ?? false;
2824
+ const output = requested ?? (isTty ? "table" : "json");
2825
+ const apiKey = args.flags["api-key"];
2826
+ const profile = stringFlag(args, "profile", "p");
2827
+ const apiUrl = stringFlag(args, "api-url");
2828
+ return {
2829
+ ...profile !== void 0 ? { profile } : {},
2830
+ ...apiUrl !== void 0 ? { apiUrl } : {},
2831
+ ...apiKey === true || typeof apiKey === "string" ? { apiKey } : {},
2832
+ output,
2833
+ quiet: boolFlag(args, "quiet", "q"),
2834
+ ...limit !== void 0 ? { limit } : {}
2835
+ };
2836
+ }
2837
+ function report(caught, error) {
2838
+ if (caught instanceof NotSignedIn) {
2839
+ error(caught.message);
2840
+ return 3;
2841
+ }
2842
+ if (caught instanceof PersistMemoryError) {
2843
+ error(caught.message);
2844
+ return caught.status === 401 || caught.status === 403 ? 3 : 1;
2845
+ }
2846
+ error(caught instanceof Error ? caught.message : String(caught));
2847
+ return 1;
2848
+ }
2849
+ export {
2850
+ PersistMemory,
2851
+ run
2852
+ };
2853
+ //# sourceMappingURL=index.js.map