@jbarket/loomai 0.5.0 → 0.7.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/CHANGELOG.md +129 -1
- package/README.md +115 -0
- package/dist/backends/dedup.d.ts +23 -0
- package/dist/backends/dedup.js +100 -0
- package/dist/backends/embedding-runtime.js +38 -9
- package/dist/backends/episodes.js +1 -1
- package/dist/backends/fastembed.js +30 -4
- package/dist/backends/migrations.js +200 -0
- package/dist/backends/mmr.d.ts +8 -0
- package/dist/backends/mmr.js +18 -4
- package/dist/backends/people.d.ts +192 -0
- package/dist/backends/people.js +824 -0
- package/dist/backends/proposals.js +1 -1
- package/dist/backends/recorder-health.d.ts +34 -0
- package/dist/backends/recorder-health.js +117 -0
- package/dist/backends/salience.d.ts +39 -0
- package/dist/backends/salience.js +52 -4
- package/dist/backends/scope.d.ts +56 -0
- package/dist/backends/scope.js +75 -0
- package/dist/backends/sqlite-knowledge.d.ts +24 -4
- package/dist/backends/sqlite-knowledge.js +253 -91
- package/dist/backends/sqlite-vec.d.ts +32 -1
- package/dist/backends/sqlite-vec.js +394 -75
- package/dist/backends/types.d.ts +203 -3
- package/dist/categories.d.ts +2 -0
- package/dist/categories.js +2 -0
- package/dist/cli/args.d.ts +16 -1
- package/dist/cli/args.js +45 -1
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +81 -3
- package/dist/cli/knowledge.js +4 -1
- package/dist/cli/memory.js +2 -2
- package/dist/cli/person.d.ts +2 -0
- package/dist/cli/person.js +116 -0
- package/dist/cli/pieces.d.ts +24 -0
- package/dist/cli/pieces.js +139 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.js +10 -0
- package/dist/cli/remote.d.ts +36 -0
- package/dist/cli/remote.js +173 -0
- package/dist/cli/serve.d.ts +7 -0
- package/dist/cli/serve.js +94 -5
- package/dist/cli/subcommands.d.ts +1 -1
- package/dist/cli/subcommands.js +2 -2
- package/dist/cli/update.js +30 -11
- package/dist/clients.js +7 -4
- package/dist/config.d.ts +14 -0
- package/dist/config.js +25 -2
- package/dist/index.js +28 -8
- package/dist/injection/harnesses.js +9 -3
- package/dist/install/harnesses.js +7 -1
- package/dist/server.d.ts +8 -1
- package/dist/server.js +461 -374
- package/dist/tools/dossier.js +5 -1
- package/dist/tools/eurorack-import.js +3 -0
- package/dist/tools/identity.d.ts +2 -0
- package/dist/tools/identity.js +35 -2
- package/dist/tools/knowledge-history.d.ts +6 -1
- package/dist/tools/knowledge-history.js +26 -8
- package/dist/tools/knowledge-maintain.d.ts +5 -0
- package/dist/tools/knowledge-maintain.js +92 -5
- package/dist/tools/knowledge-recall.d.ts +7 -1
- package/dist/tools/knowledge-recall.js +53 -3
- package/dist/tools/knowledge-verify.d.ts +11 -1
- package/dist/tools/knowledge-verify.js +17 -4
- package/dist/tools/knowledge-write.d.ts +8 -1
- package/dist/tools/knowledge-write.js +27 -9
- package/dist/tools/memory-history.d.ts +5 -0
- package/dist/tools/memory-history.js +52 -0
- package/dist/tools/memory-list.js +2 -1
- package/dist/tools/memory-merge.d.ts +2 -0
- package/dist/tools/memory-merge.js +40 -0
- package/dist/tools/memory-revision-restore.d.ts +5 -0
- package/dist/tools/memory-revision-restore.js +33 -0
- package/dist/tools/person.d.ts +32 -0
- package/dist/tools/person.js +111 -0
- package/dist/tools/prune.js +1 -1
- package/dist/tools/recall.js +7 -1
- package/dist/tools/remember.js +18 -2
- package/dist/tools/update.js +5 -1
- package/dist/transport/cli-endpoint.d.ts +13 -0
- package/dist/transport/cli-endpoint.js +97 -0
- package/dist/transport/guards.d.ts +14 -0
- package/dist/transport/guards.js +26 -0
- package/dist/transport/http-server.d.ts +28 -1
- package/dist/transport/http-server.js +156 -18
- package/dist/transport/knowledge-server.d.ts +11 -0
- package/dist/transport/knowledge-server.js +30 -0
- package/dist/transport/knowledge-tools.d.ts +50 -0
- package/dist/transport/knowledge-tools.js +290 -0
- package/dist/transport/mcp-app-hello.d.ts +1 -1
- package/dist/widgets/display-modes.html +62 -56
- package/dist/widgets/hello.html +62 -56
- package/dist/widgets/proposals.html +62 -56
- package/dist/widgets/top-of-mind.html +60 -54
- package/package.json +7 -5
package/dist/backends/types.d.ts
CHANGED
|
@@ -5,6 +5,19 @@
|
|
|
5
5
|
* storage backend. Any backend that implements MemoryBackend can be
|
|
6
6
|
* swapped in; v0.3.1 ships SqliteVecBackend as the single opinion.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* How a memory was sourced — mirrors the knowledge wing's sourcing field.
|
|
10
|
+
* Displayed at injection time so readers can calibrate confidence.
|
|
11
|
+
*
|
|
12
|
+
* - `observed` — directly witnessed by this body: measured, tested, read from source.
|
|
13
|
+
* - `relayed` — learned from another person/body: conversation, letter, episode.
|
|
14
|
+
* - `inferred` — AI-generated reasoning/synthesis: extrapolation, pattern-matching.
|
|
15
|
+
* Treat with appropriate skepticism — not ground truth.
|
|
16
|
+
* - `system` — auto-written by the harness (episode fallback, lane output, etc.).
|
|
17
|
+
*/
|
|
18
|
+
export type MemorySourcing = 'observed' | 'relayed' | 'inferred' | 'system';
|
|
19
|
+
export type { MemoryScope } from './scope.js';
|
|
20
|
+
import type { MemoryScope } from './scope.js';
|
|
8
21
|
export interface MemoryInput {
|
|
9
22
|
category: string;
|
|
10
23
|
title: string;
|
|
@@ -13,6 +26,29 @@ export interface MemoryInput {
|
|
|
13
26
|
metadata?: Record<string, unknown>;
|
|
14
27
|
/** Optional time-to-live. Parsed durations like "7d", "30d", or "permanent". */
|
|
15
28
|
ttl?: string;
|
|
29
|
+
/**
|
|
30
|
+
* How this memory was sourced. Displayed at injection so the reader can
|
|
31
|
+
* calibrate confidence — 'inferred' flags AI-generated reasoning that should
|
|
32
|
+
* not be treated as ground truth.
|
|
33
|
+
*/
|
|
34
|
+
sourcing?: MemorySourcing;
|
|
35
|
+
/**
|
|
36
|
+
* Free-form origin: surface, body, session, or person. E.g. "wake:w-74fccc
|
|
37
|
+
* 2026-09-07", "Jonathan, Discord #loom", "lane:letters". For episodes,
|
|
38
|
+
* prefer metadata.where (already conventional); this carries additional detail.
|
|
39
|
+
*/
|
|
40
|
+
provenance?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Explicit scope override — 'project' (only recallable while scoped to
|
|
43
|
+
* this memory's project) or 'global' (recallable regardless of project).
|
|
44
|
+
* Omit to take the category default (see backends/scope.ts): 'user' and
|
|
45
|
+
* 'reference' default global, everything else defaults project when a
|
|
46
|
+
* project is attached, global when it isn't. Set this explicitly to flag
|
|
47
|
+
* a genuinely cross-project fact learned in a project context — e.g. a
|
|
48
|
+
* git/security/tool-workflow practice that happened to surface while
|
|
49
|
+
* working on one project but applies everywhere (t-337).
|
|
50
|
+
*/
|
|
51
|
+
scope?: MemoryScope;
|
|
16
52
|
}
|
|
17
53
|
export interface MemoryRef {
|
|
18
54
|
ref: string;
|
|
@@ -66,13 +102,38 @@ export interface MemoryMatch {
|
|
|
66
102
|
project?: string;
|
|
67
103
|
created: string;
|
|
68
104
|
content: string;
|
|
105
|
+
/**
|
|
106
|
+
* Final ranking score for this result's position in the returned list.
|
|
107
|
+
* Equals `similarity` unless MMR re-ranking (recall's `diversity` > 0)
|
|
108
|
+
* traded this pick off against ones already selected — in that case this
|
|
109
|
+
* is the blended score (λ·similarity − (1−λ)·max similarity-to-selected)
|
|
110
|
+
* that actually placed it, not its raw similarity. See `similarity` for
|
|
111
|
+
* the pre-MMR number alongside it (t-337a; modeled on MemoriLabs/Memori's
|
|
112
|
+
* FactSearchResult.{similarity, rank_score} split).
|
|
113
|
+
*/
|
|
69
114
|
relevance: number;
|
|
115
|
+
/** Raw cosine similarity (1 − distance) to the query, before any MMR re-ranking. */
|
|
116
|
+
similarity: number;
|
|
117
|
+
/** Effective scope this memory was recalled under — see backends/scope.ts (t-337b). */
|
|
118
|
+
scope: MemoryScope;
|
|
70
119
|
/** ISO timestamp of last recall hit, if tracked */
|
|
71
120
|
lastAccessed?: string;
|
|
72
121
|
/** TTL value if set (e.g. "7d", "permanent") */
|
|
73
122
|
ttl?: string;
|
|
74
123
|
/** ISO timestamp when this memory expires, if TTL is set */
|
|
75
124
|
expiresAt?: string;
|
|
125
|
+
/** Sourcing tier — how the memory was produced. Null on legacy records. */
|
|
126
|
+
sourcing?: MemorySourcing;
|
|
127
|
+
/** Free-form origin text (surface / person / session). Null on legacy records. */
|
|
128
|
+
provenance?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Evidence-backed confidence (0.3-0.9), feedback memories only (t-338).
|
|
131
|
+
* Decayed toward neutral by category half-life — see salience.ts
|
|
132
|
+
* decayedConfidence(). Undefined for non-feedback memories.
|
|
133
|
+
*/
|
|
134
|
+
confidence?: number;
|
|
135
|
+
/** Number of observations (creation + confirm/contradict) behind `confidence`. */
|
|
136
|
+
evidenceCount?: number;
|
|
76
137
|
}
|
|
77
138
|
export interface ForgetInput {
|
|
78
139
|
/** Direct reference (category/filename) for single deletion */
|
|
@@ -97,6 +158,13 @@ export interface UpdateInput {
|
|
|
97
158
|
content?: string;
|
|
98
159
|
/** Metadata fields to add or update */
|
|
99
160
|
metadata?: Record<string, unknown>;
|
|
161
|
+
/**
|
|
162
|
+
* Record an observation against this memory's confidence (feedback
|
|
163
|
+
* memories only, t-338): 'confirm' raises it, 'contradict' lowers it.
|
|
164
|
+
* Computed in code (see salience.ts applyObservation) — never a prompt.
|
|
165
|
+
* Ignored on non-feedback memories.
|
|
166
|
+
*/
|
|
167
|
+
observation?: 'confirm' | 'contradict';
|
|
100
168
|
}
|
|
101
169
|
export interface ForgetResult {
|
|
102
170
|
/** Refs that were successfully deleted */
|
|
@@ -133,6 +201,10 @@ export interface UpdateResult {
|
|
|
133
201
|
ref?: string;
|
|
134
202
|
/** Id of the body snapshot taken before content was overwritten (if any). */
|
|
135
203
|
snapshotId?: number;
|
|
204
|
+
/** Raw (undecayed) confidence after applying `observation`, when one was given. */
|
|
205
|
+
confidence?: number;
|
|
206
|
+
/** Evidence count after applying `observation`, when one was given. */
|
|
207
|
+
evidenceCount?: number;
|
|
136
208
|
}
|
|
137
209
|
/** Revision listing entry — metadata only, no body payload. */
|
|
138
210
|
export interface MemoryRevisionMeta {
|
|
@@ -173,8 +245,31 @@ export interface MemorySupersededResult {
|
|
|
173
245
|
/** True when old_ref was archived by this call; false if already archived. */
|
|
174
246
|
archived: boolean;
|
|
175
247
|
}
|
|
248
|
+
export interface MemoryMergeInput {
|
|
249
|
+
/** Refs of the losing memories to fold into the target (all must be active). */
|
|
250
|
+
source_refs: string[];
|
|
251
|
+
/** Ref of the canonical memory that survives the merge (must already exist, active). */
|
|
252
|
+
target_ref: string;
|
|
253
|
+
/** Optional note about this merge, stored in supersession tombstones on the losers. */
|
|
254
|
+
note?: string;
|
|
255
|
+
/** Hard-delete losers after archiving/superseding them (default false). */
|
|
256
|
+
hard_delete_losers?: boolean;
|
|
257
|
+
/** Append loser bodies to the target content under section markers (default false). */
|
|
258
|
+
append_loser_bodies?: boolean;
|
|
259
|
+
}
|
|
260
|
+
export interface MemoryMergeResult {
|
|
261
|
+
target_ref: string;
|
|
262
|
+
sources_merged: number;
|
|
263
|
+
/** Target's times_seen after summing in each merged source's count. */
|
|
264
|
+
times_seen: number;
|
|
265
|
+
losers: {
|
|
266
|
+
ref: string;
|
|
267
|
+
title: string;
|
|
268
|
+
content: string;
|
|
269
|
+
}[];
|
|
270
|
+
}
|
|
176
271
|
export interface PruneResult {
|
|
177
|
-
/** Memories that were
|
|
272
|
+
/** Memories that were archived (moved to archive tier) because their TTL expired */
|
|
178
273
|
expired: string[];
|
|
179
274
|
/** Memories that haven't been accessed within the stale threshold */
|
|
180
275
|
stale: string[];
|
|
@@ -190,6 +285,14 @@ export interface MemoryEntry {
|
|
|
190
285
|
category: string;
|
|
191
286
|
project?: string;
|
|
192
287
|
created: string;
|
|
288
|
+
/** Sourcing tier — how the memory was produced. Null on legacy records. */
|
|
289
|
+
sourcing?: MemorySourcing;
|
|
290
|
+
/** Free-form origin text (surface / person / session). Null on legacy records. */
|
|
291
|
+
provenance?: string;
|
|
292
|
+
/** Decayed confidence (feedback memories only, t-338). */
|
|
293
|
+
confidence?: number;
|
|
294
|
+
/** Observation count behind `confidence`. */
|
|
295
|
+
evidenceCount?: number;
|
|
193
296
|
}
|
|
194
297
|
export interface FindSimilarInput {
|
|
195
298
|
/** Anchor on an existing memory's embedding. Exactly one of `ref`/`text`. */
|
|
@@ -239,7 +342,7 @@ export interface AuditReport {
|
|
|
239
342
|
stale: AuditStaleEntry[];
|
|
240
343
|
/** Pairs of memories whose vector similarity ≥ `similarityThreshold`. */
|
|
241
344
|
duplicates: DuplicatePair[];
|
|
242
|
-
/** Memories whose TTL has expired (would be
|
|
345
|
+
/** Memories whose TTL has expired (would be archived by `prune`). */
|
|
243
346
|
expired: string[];
|
|
244
347
|
}
|
|
245
348
|
export interface MemoryBackend {
|
|
@@ -269,6 +372,13 @@ export interface MemoryBackend {
|
|
|
269
372
|
restoreRevision(input: MemoryRevisionRestoreInput): Promise<MemoryRevisionRestoreResult>;
|
|
270
373
|
/** Archive old_ref with a tombstone and record that new_ref supersedes it. */
|
|
271
374
|
supersede(input: MemorySupersededInput): Promise<MemorySupersededResult>;
|
|
375
|
+
/**
|
|
376
|
+
* Consolidate 2+ memories into one canonical row. Acts on an audit()
|
|
377
|
+
* duplicate-finding: sources are archived with a supersession pointer to
|
|
378
|
+
* the target (parity with supersede(), but N:1), times_seen is summed
|
|
379
|
+
* onto the target, and bodies are optionally concatenated.
|
|
380
|
+
*/
|
|
381
|
+
mergeMemories(input: MemoryMergeInput): Promise<MemoryMergeResult>;
|
|
272
382
|
/** Release the underlying store handle. Cached backends evict on close. */
|
|
273
383
|
close(): void;
|
|
274
384
|
}
|
|
@@ -291,6 +401,14 @@ export interface KnowledgePageInput {
|
|
|
291
401
|
verified_at?: string;
|
|
292
402
|
/** The version/date the claims are valid as-of, e.g. "Syntakt OS 1.21" or "as of 2026-05". */
|
|
293
403
|
freshness_anchor?: string;
|
|
404
|
+
/**
|
|
405
|
+
* Machine-readable SLA class. When omitted, derived automatically: a
|
|
406
|
+
* version-anchored page (freshness_anchor matches /v?\d+\.\d+/) → 'software';
|
|
407
|
+
* device-domain page (domain starts with 'music/gear' or 'monitor/hardware') →
|
|
408
|
+
* 'device'; all others → 'default'. SLAs: device=7d, software=14d, default=30d.
|
|
409
|
+
* Pass explicitly to override the derived value.
|
|
410
|
+
*/
|
|
411
|
+
sla_class?: 'device' | 'software' | 'default';
|
|
294
412
|
citations?: KnowledgeCitationInput[];
|
|
295
413
|
/**
|
|
296
414
|
* How `body` combines with an existing page on upsert.
|
|
@@ -308,6 +426,15 @@ export interface KnowledgePageInput {
|
|
|
308
426
|
* (e.g. "v2", "2026-08-19", "t-81"). Preserved across upserts when omitted.
|
|
309
427
|
*/
|
|
310
428
|
version?: string;
|
|
429
|
+
/**
|
|
430
|
+
* Caller's loom identity, resolved server-side (never a free-text tool
|
|
431
|
+
* argument — that would let any caller claim any authorship). Stamped as
|
|
432
|
+
* pages.author only on creation (the identity that wrote v1 — never
|
|
433
|
+
* overwritten by later upserts) and as page_revisions.actor on any
|
|
434
|
+
* snapshot this call produces. Omitted means "unknown" (legacy direct
|
|
435
|
+
* callers, tests) rather than a hard requirement.
|
|
436
|
+
*/
|
|
437
|
+
actor?: string;
|
|
311
438
|
}
|
|
312
439
|
export interface KnowledgeCitationInput {
|
|
313
440
|
claim: string;
|
|
@@ -334,10 +461,22 @@ export interface KnowledgePage {
|
|
|
334
461
|
verified_at?: string | null;
|
|
335
462
|
/** The version/date the claims are valid as-of, e.g. "Syntakt OS 1.21". */
|
|
336
463
|
freshness_anchor?: string | null;
|
|
464
|
+
/**
|
|
465
|
+
* Machine-readable SLA class: 'device' (7d), 'software' (14d), 'default' (30d).
|
|
466
|
+
* Derived from freshness_anchor shape and domain on write; may be overridden
|
|
467
|
+
* explicitly via KnowledgePageInput.sla_class.
|
|
468
|
+
*/
|
|
469
|
+
sla_class: string;
|
|
337
470
|
/** ours/ class: who created or last owned this artifact (e.g. "art", "jonathan"). */
|
|
338
471
|
created_by?: string | null;
|
|
339
472
|
/** ours/ class: artifact version or revision tag (e.g. "v2", "2026-08-19"). */
|
|
340
473
|
version?: string | null;
|
|
474
|
+
/**
|
|
475
|
+
* Identity that wrote v1 of this page — set once at creation, never
|
|
476
|
+
* changed by later upserts. Null for pages created before this column
|
|
477
|
+
* existed, or written by a direct backend caller that omitted actor.
|
|
478
|
+
*/
|
|
479
|
+
author?: string | null;
|
|
341
480
|
}
|
|
342
481
|
export interface KnowledgeCitation {
|
|
343
482
|
id: number;
|
|
@@ -369,6 +508,19 @@ export interface KnowledgeQueryInput {
|
|
|
369
508
|
* never-verified pages first. Overrides the default hit-count ordering.
|
|
370
509
|
*/
|
|
371
510
|
sortByVerified?: boolean;
|
|
511
|
+
/**
|
|
512
|
+
* Who is reading (t-677). When stampAccess fires, this identity's row in
|
|
513
|
+
* knowledge_access is upserted alongside the shared pages.hit_count /
|
|
514
|
+
* last_accessed aggregate. Defaults to 'unknown' when omitted — callers
|
|
515
|
+
* that care about attribution should always pass the acting identity.
|
|
516
|
+
*/
|
|
517
|
+
identity?: string;
|
|
518
|
+
}
|
|
519
|
+
/** One reader's access stats on a page (t-677 per-identity attribution). */
|
|
520
|
+
export interface KnowledgeAccessStat {
|
|
521
|
+
identity: string;
|
|
522
|
+
hit_count: number;
|
|
523
|
+
last_accessed: string | null;
|
|
372
524
|
}
|
|
373
525
|
export interface KnowledgePageRef {
|
|
374
526
|
uuid: string;
|
|
@@ -481,14 +633,41 @@ export interface KnowledgeVerifyInput {
|
|
|
481
633
|
freshness_anchor?: string;
|
|
482
634
|
/** Optional note — appended to the body as a "## Verification — <date>" section (single-page mode only). */
|
|
483
635
|
note?: string;
|
|
636
|
+
/**
|
|
637
|
+
* Caller's loom identity, resolved server-side (never a free-text tool
|
|
638
|
+
* argument). Recorded as verifications.verifier for every page stamped
|
|
639
|
+
* by this call. Omitted means "unknown" (legacy direct callers, tests).
|
|
640
|
+
*/
|
|
641
|
+
verifier?: string;
|
|
642
|
+
/** What the verification found. Defaults to 'confirmed' (the claims still hold). */
|
|
643
|
+
outcome?: 'confirmed' | 'stale' | 'corrected';
|
|
644
|
+
/** Whether the verifier actually checked the page's citations against their sources. Defaults to false. */
|
|
645
|
+
citation_checked?: boolean;
|
|
484
646
|
}
|
|
485
647
|
export interface KnowledgeVerifyResult {
|
|
486
648
|
verified: number;
|
|
487
649
|
slugs: string[];
|
|
650
|
+
/** The verification time recorded in the audit trail. */
|
|
488
651
|
verified_at: string;
|
|
652
|
+
/**
|
|
653
|
+
* True when pages.verified_at was advanced to `verified_at`. False for an
|
|
654
|
+
* outcome of 'stale' — a failed verification is audited but does not
|
|
655
|
+
* renew the page's successful-freshness stamp.
|
|
656
|
+
*/
|
|
657
|
+
stamped: boolean;
|
|
489
658
|
/** True when a note section was appended to the body. */
|
|
490
659
|
noted: boolean;
|
|
491
660
|
}
|
|
661
|
+
/** A verification record — who verified a page, when, and what they found. */
|
|
662
|
+
export interface KnowledgeVerificationRecord {
|
|
663
|
+
id: number;
|
|
664
|
+
page_id: number;
|
|
665
|
+
/** Resolved loom identity of the verifier; null for pre-attribution rows. */
|
|
666
|
+
verifier: string | null;
|
|
667
|
+
verified_at: string;
|
|
668
|
+
outcome: string;
|
|
669
|
+
citation_checked: boolean;
|
|
670
|
+
}
|
|
492
671
|
/** Revision listing entry — metadata only, no body payload. */
|
|
493
672
|
export interface KnowledgeRevisionMeta {
|
|
494
673
|
id: number;
|
|
@@ -499,6 +678,8 @@ export interface KnowledgeRevisionMeta {
|
|
|
499
678
|
op: string;
|
|
500
679
|
replaced_at: string;
|
|
501
680
|
body_length: number;
|
|
681
|
+
/** Resolved loom identity that performed the action, null for pre-attribution rows. */
|
|
682
|
+
actor: string | null;
|
|
502
683
|
}
|
|
503
684
|
export interface KnowledgeRevision extends Omit<KnowledgeRevisionMeta, 'body_length'> {
|
|
504
685
|
body: string;
|
|
@@ -506,6 +687,8 @@ export interface KnowledgeRevision extends Omit<KnowledgeRevisionMeta, 'body_len
|
|
|
506
687
|
export interface KnowledgeRevisionRestoreInput {
|
|
507
688
|
slug: string;
|
|
508
689
|
revision_id: number;
|
|
690
|
+
/** Caller's loom identity, resolved server-side — recorded on the snapshot this restore takes of the displaced body. */
|
|
691
|
+
actor?: string;
|
|
509
692
|
}
|
|
510
693
|
export interface KnowledgeRevisionRestoreResult {
|
|
511
694
|
slug: string;
|
|
@@ -527,9 +710,14 @@ export interface KnowledgePurgeResult {
|
|
|
527
710
|
export interface KnowledgeBackend {
|
|
528
711
|
/** Upsert an entity page by slug; create or append. */
|
|
529
712
|
writePage(input: KnowledgePageInput): Promise<KnowledgeWriteResult>;
|
|
530
|
-
/**
|
|
713
|
+
/**
|
|
714
|
+
* Get a single page by slug. opts.stampAccess marks the fetch as a real
|
|
715
|
+
* read (last_accessed / hit_count); opts.identity attributes that read
|
|
716
|
+
* to a reader (t-677) — defaults to 'unknown' when omitted.
|
|
717
|
+
*/
|
|
531
718
|
getPage(slug: string, opts?: {
|
|
532
719
|
stampAccess?: boolean;
|
|
720
|
+
identity?: string;
|
|
533
721
|
}): Promise<KnowledgePageWithCitations | null>;
|
|
534
722
|
/** List pages with optional filters. */
|
|
535
723
|
listPages(input?: KnowledgeQueryInput): Promise<KnowledgePageWithCitations[]>;
|
|
@@ -551,12 +739,24 @@ export interface KnowledgeBackend {
|
|
|
551
739
|
purgePages(input: KnowledgePurgeInput): Promise<KnowledgePurgeResult>;
|
|
552
740
|
/** Stamp verified_at / freshness_anchor without touching the body (optional appended note in single-page mode). */
|
|
553
741
|
verifyPages(input: KnowledgeVerifyInput): Promise<KnowledgeVerifyResult>;
|
|
742
|
+
/** List verification records for a page, newest first. */
|
|
743
|
+
getVerifications(slug: string): Promise<KnowledgeVerificationRecord[]>;
|
|
554
744
|
/** List body snapshots for a page, newest first. Metadata only — no body payloads. */
|
|
555
745
|
listRevisions(slug: string): Promise<KnowledgeRevisionMeta[]>;
|
|
556
746
|
/** Fetch one revision including its full body. */
|
|
557
747
|
getRevision(revisionId: number): Promise<KnowledgeRevision | null>;
|
|
558
748
|
/** Restore a revision's body onto its page; the displaced body is snapshotted first. */
|
|
559
749
|
restoreRevision(input: KnowledgeRevisionRestoreInput): Promise<KnowledgeRevisionRestoreResult>;
|
|
750
|
+
/**
|
|
751
|
+
* Per-identity read stats for a page (t-677), newest-touched first. This
|
|
752
|
+
* is the fine-grained "who's reading this" breakdown; it is NOT consulted
|
|
753
|
+
* by knowledge_maintain's cold/expansion ranking, which deliberately keeps
|
|
754
|
+
* using the shared pages.hit_count / last_accessed aggregate — that
|
|
755
|
+
* aggregate already answers "has anyone read this recently" correctly
|
|
756
|
+
* across readers, so splitting it per-identity there would only make the
|
|
757
|
+
* cold list miss pages a *different* reader keeps alive.
|
|
758
|
+
*/
|
|
759
|
+
getAccessStats(pageId: number): Promise<KnowledgeAccessStat[]>;
|
|
560
760
|
/** Close the underlying SQLite connection. */
|
|
561
761
|
close(): void;
|
|
562
762
|
}
|
package/dist/categories.d.ts
CHANGED
|
@@ -12,5 +12,7 @@ export declare const MEMORY_CATEGORIES: readonly ['user', 'project', 'self', 'fe
|
|
|
12
12
|
export declare const EPISODE_CATEGORY = "episode";
|
|
13
13
|
/** Episodes expire by default — they are a tape, not the stack. */
|
|
14
14
|
export declare const EPISODE_DEFAULT_TTL = "48h";
|
|
15
|
+
/** The category that carries evidence-backed confidence (t-338). */
|
|
16
|
+
export declare const FEEDBACK_CATEGORY = "feedback";
|
|
15
17
|
export type MemoryCategory = (typeof MEMORY_CATEGORIES)[number];
|
|
16
18
|
export declare function isMemoryCategory(value: string): value is MemoryCategory;
|
package/dist/categories.js
CHANGED
|
@@ -24,6 +24,8 @@ export const MEMORY_CATEGORIES = [
|
|
|
24
24
|
export const EPISODE_CATEGORY = 'episode';
|
|
25
25
|
/** Episodes expire by default — they are a tape, not the stack. */
|
|
26
26
|
export const EPISODE_DEFAULT_TTL = '48h';
|
|
27
|
+
/** The category that carries evidence-backed confidence (t-338). */
|
|
28
|
+
export const FEEDBACK_CATEGORY = 'feedback';
|
|
27
29
|
export function isMemoryCategory(value) {
|
|
28
30
|
return MEMORY_CATEGORIES.includes(value);
|
|
29
31
|
}
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -9,11 +9,26 @@ export interface RawGlobalFlags {
|
|
|
9
9
|
client?: string;
|
|
10
10
|
model?: string;
|
|
11
11
|
json?: boolean;
|
|
12
|
+
/** Remote loom-http to run against instead of a local stack (src/cli/remote.ts). */
|
|
13
|
+
url?: string;
|
|
12
14
|
}
|
|
13
15
|
export declare function resolveEnv(flags: RawGlobalFlags, processEnv: NodeJS.ProcessEnv): ResolvedEnv;
|
|
16
|
+
/**
|
|
17
|
+
* Index of the first token in `argv` that is neither a leading global flag
|
|
18
|
+
* nor a global flag's value — i.e. where the subcommand is, if there is
|
|
19
|
+
* one. Returns `argv.length` when argv holds nothing but globals.
|
|
20
|
+
*
|
|
21
|
+
* The help text calls --context-dir/--client/--model/--json GLOBAL, which
|
|
22
|
+
* has to mean they are accepted before the subcommand as well as after.
|
|
23
|
+
* Both entry points resolve the subcommand through this so that promise
|
|
24
|
+
* holds: src/index.ts to decide CLI-vs-MCP, runCli to dispatch. Scanning
|
|
25
|
+
* only over *leading* globals (rather than searching all of argv for a
|
|
26
|
+
* known verb) is what keeps a flag's value from being mistaken for one.
|
|
27
|
+
*/
|
|
28
|
+
export declare function firstNonGlobalIndex(argv: string[]): number;
|
|
14
29
|
/**
|
|
15
30
|
* Extracts global flags from an argv slice, returning the remaining argv.
|
|
16
|
-
* Recognizes: --context-dir, --client, --model, --json.
|
|
31
|
+
* Recognizes: --context-dir, --client, --model, --url, --json — in any position.
|
|
17
32
|
*/
|
|
18
33
|
export declare function extractGlobalFlags(argv: string[]): {
|
|
19
34
|
flags: RawGlobalFlags;
|
package/dist/cli/args.js
CHANGED
|
@@ -16,9 +16,49 @@ export function resolveEnv(flags, processEnv) {
|
|
|
16
16
|
json: Boolean(flags.json),
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Global flags that consume the NEXT argv token as their value. Shared by
|
|
21
|
+
* `extractGlobalFlags` and `firstNonGlobalIndex` so the two cannot drift:
|
|
22
|
+
* a flag that takes a value must be skipped in pairs when we are looking
|
|
23
|
+
* past the globals for a subcommand, or `--context-dir recall` reads as an
|
|
24
|
+
* invocation of `recall`.
|
|
25
|
+
*/
|
|
26
|
+
const VALUED_GLOBAL_FLAGS = new Set([
|
|
27
|
+
'--context-dir', '--client', '--model', '--url',
|
|
28
|
+
]);
|
|
29
|
+
/** Global flags that stand alone. */
|
|
30
|
+
const BARE_GLOBAL_FLAGS = new Set(['--json']);
|
|
31
|
+
/**
|
|
32
|
+
* Index of the first token in `argv` that is neither a leading global flag
|
|
33
|
+
* nor a global flag's value — i.e. where the subcommand is, if there is
|
|
34
|
+
* one. Returns `argv.length` when argv holds nothing but globals.
|
|
35
|
+
*
|
|
36
|
+
* The help text calls --context-dir/--client/--model/--json GLOBAL, which
|
|
37
|
+
* has to mean they are accepted before the subcommand as well as after.
|
|
38
|
+
* Both entry points resolve the subcommand through this so that promise
|
|
39
|
+
* holds: src/index.ts to decide CLI-vs-MCP, runCli to dispatch. Scanning
|
|
40
|
+
* only over *leading* globals (rather than searching all of argv for a
|
|
41
|
+
* known verb) is what keeps a flag's value from being mistaken for one.
|
|
42
|
+
*/
|
|
43
|
+
export function firstNonGlobalIndex(argv) {
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < argv.length) {
|
|
46
|
+
const a = argv[i];
|
|
47
|
+
if (VALUED_GLOBAL_FLAGS.has(a)) {
|
|
48
|
+
i += 2;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (BARE_GLOBAL_FLAGS.has(a)) {
|
|
52
|
+
i += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
return i;
|
|
56
|
+
}
|
|
57
|
+
return argv.length;
|
|
58
|
+
}
|
|
19
59
|
/**
|
|
20
60
|
* Extracts global flags from an argv slice, returning the remaining argv.
|
|
21
|
-
* Recognizes: --context-dir, --client, --model, --json.
|
|
61
|
+
* Recognizes: --context-dir, --client, --model, --url, --json — in any position.
|
|
22
62
|
*/
|
|
23
63
|
export function extractGlobalFlags(argv) {
|
|
24
64
|
const flags = {};
|
|
@@ -37,6 +77,10 @@ export function extractGlobalFlags(argv) {
|
|
|
37
77
|
flags.model = argv[++i];
|
|
38
78
|
continue;
|
|
39
79
|
}
|
|
80
|
+
if (a === '--url') {
|
|
81
|
+
flags.url = argv[++i];
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
40
84
|
if (a === '--json') {
|
|
41
85
|
flags.json = true;
|
|
42
86
|
continue;
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { IOStreams } from './io.js';
|
|
2
2
|
export declare function runCli(argv: string[], io?: IOStreams): Promise<number>;
|
|
3
|
+
/**
|
|
4
|
+
* The CLI with no remote mode on its path at all: what POST /cli runs
|
|
5
|
+
* (transport/cli-endpoint.ts). A request arriving over HTTP must never be
|
|
6
|
+
* able to send the server on to another URL, and that holds by structure
|
|
7
|
+
* here, not only because the endpoint refuses --url.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runCliLocal(argv: string[], io: IOStreams): Promise<number>;
|
package/dist/cli/index.js
CHANGED
|
@@ -7,6 +7,9 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { resolveRepoRoot } from '../config.js';
|
|
8
8
|
import { realStreams } from './io.js';
|
|
9
9
|
import { SUBCOMMANDS } from './subcommands.js';
|
|
10
|
+
import { extractGlobalFlags, firstNonGlobalIndex } from './args.js';
|
|
11
|
+
import { REMOTE_VERBS, runRemote } from './remote.js';
|
|
12
|
+
import { findPiece, isPieceName, runPiece } from './pieces.js';
|
|
10
13
|
const TOP_HELP = `Usage: loom <command> [options]
|
|
11
14
|
|
|
12
15
|
Commands:
|
|
@@ -19,6 +22,7 @@ Commands:
|
|
|
19
22
|
Browse / clean / find duplicates / health report
|
|
20
23
|
knowledge write|recall|maintain
|
|
21
24
|
Write / search / health-report the knowledge store
|
|
25
|
+
person show|list Read person cards (condensed current state per person)
|
|
22
26
|
update-identity Edit preferences.md / self-model.md sections
|
|
23
27
|
bootstrap Initialize a fresh agent
|
|
24
28
|
serve Explicit MCP stdio startup (same as no args)
|
|
@@ -27,11 +31,19 @@ Commands:
|
|
|
27
31
|
install Write loom-setup skill into a harness's skills dir
|
|
28
32
|
doctor Probe the loom environment (read-only)
|
|
29
33
|
migrate Apply pending schema migrations to memories.db
|
|
34
|
+
pieces List installed pieces (loom-<name> on PATH)
|
|
35
|
+
|
|
36
|
+
Pieces: \`loom <name> ...\` runs an installed extension, loom-<name> on PATH
|
|
37
|
+
(e.g. loom tasks list). Core commands always win. See docs/extensions.md.
|
|
30
38
|
|
|
31
39
|
Global flags:
|
|
32
40
|
--context-dir <path> Agent context dir (default: $LOOM_CONTEXT_DIR or ~/.config/loom/default)
|
|
33
41
|
--client <name> Harness adapter hint (default: $LOOM_CLIENT)
|
|
34
42
|
--model <name> Model manifest hint (default: $LOOM_MODEL)
|
|
43
|
+
--url <url> Run against a loom-http server (default: $LOOM_URL), e.g.
|
|
44
|
+
http://host:8787/mcp. Bearer token from $LOOM_TOKEN_FILE.
|
|
45
|
+
Applies to wake, recall, remember, update, forget,
|
|
46
|
+
memory, knowledge, person; other commands stay local.
|
|
35
47
|
--json Machine-readable output
|
|
36
48
|
--help, -h Show help
|
|
37
49
|
--version, -V Print loom version
|
|
@@ -43,7 +55,60 @@ async function readVersion() {
|
|
|
43
55
|
return pkg.version;
|
|
44
56
|
}
|
|
45
57
|
export async function runCli(argv, io = realStreams()) {
|
|
46
|
-
|
|
58
|
+
// A piece (docs/extensions.md): not a core command, and loom-<name> is on PATH.
|
|
59
|
+
const pieceIdx = firstNonGlobalIndex(argv);
|
|
60
|
+
const pieceName = argv[pieceIdx];
|
|
61
|
+
if (isPieceName(pieceName)) {
|
|
62
|
+
const path = findPiece(pieceName, io.env);
|
|
63
|
+
if (path) {
|
|
64
|
+
if (pieceIdx > 0) {
|
|
65
|
+
io.stderr(`loom: global flags (${argv.slice(0, pieceIdx).join(' ')}) don't apply to pieces; put the piece's own flags after its name.\n`);
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
return runPiece(path, argv.slice(pieceIdx + 1), io);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const resolved = await resolveSubcommand(argv, io);
|
|
72
|
+
if (typeof resolved === 'number')
|
|
73
|
+
return resolved;
|
|
74
|
+
const { sub, rest } = resolved;
|
|
75
|
+
// Remote mode: the store verbs go to loom-http when a URL is set.
|
|
76
|
+
const url = extractGlobalFlags(rest).flags.url;
|
|
77
|
+
if (rest.includes('--url') && !url) {
|
|
78
|
+
io.stderr(`--url needs a value (a loom-http URL, e.g. http://host:8787/mcp).\n`);
|
|
79
|
+
return 2;
|
|
80
|
+
}
|
|
81
|
+
if (REMOTE_VERBS.has(sub)) {
|
|
82
|
+
const target = url ?? io.env.LOOM_URL;
|
|
83
|
+
if (target)
|
|
84
|
+
return runRemote(sub, rest, target, io);
|
|
85
|
+
}
|
|
86
|
+
else if (url !== undefined) {
|
|
87
|
+
io.stderr(`--url only applies to ${[...REMOTE_VERBS].join(', ')}; ${sub} runs locally.\n`);
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
return dispatch(sub, rest, io);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The CLI with no remote mode on its path at all: what POST /cli runs
|
|
94
|
+
* (transport/cli-endpoint.ts). A request arriving over HTTP must never be
|
|
95
|
+
* able to send the server on to another URL, and that holds by structure
|
|
96
|
+
* here, not only because the endpoint refuses --url.
|
|
97
|
+
*/
|
|
98
|
+
export async function runCliLocal(argv, io) {
|
|
99
|
+
const resolved = await resolveSubcommand(argv, io);
|
|
100
|
+
if (typeof resolved === 'number')
|
|
101
|
+
return resolved;
|
|
102
|
+
return dispatch(resolved.sub, resolved.rest, io);
|
|
103
|
+
}
|
|
104
|
+
/** The subcommand and the argv it gets, or the exit code when there's nothing to dispatch. */
|
|
105
|
+
async function resolveSubcommand(argv, io) {
|
|
106
|
+
// The subcommand is the first token that is not a leading global flag,
|
|
107
|
+
// so the flags the help text calls global work in front of it too. When
|
|
108
|
+
// it is already at argv[0] — the overwhelmingly common form — subIdx is
|
|
109
|
+
// 0 and every line below behaves exactly as it did before.
|
|
110
|
+
const subIdx = firstNonGlobalIndex(argv);
|
|
111
|
+
const first = argv[subIdx];
|
|
47
112
|
if (first === '--help' || first === '-h' || first === undefined) {
|
|
48
113
|
io.stdout(TOP_HELP);
|
|
49
114
|
return 0;
|
|
@@ -54,11 +119,16 @@ export async function runCli(argv, io = realStreams()) {
|
|
|
54
119
|
}
|
|
55
120
|
if (!SUBCOMMANDS.includes(first)) {
|
|
56
121
|
io.stderr(`Unknown subcommand: ${first}\n`);
|
|
122
|
+
if (isPieceName(first))
|
|
123
|
+
io.stderr(`(no piece installed either: nothing named loom-${first} on PATH; \`loom pieces\` lists what is)\n`);
|
|
57
124
|
io.stderr(TOP_HELP);
|
|
58
125
|
return 2;
|
|
59
126
|
}
|
|
60
|
-
|
|
61
|
-
|
|
127
|
+
// Drop only the subcommand token; the globals that preceded it stay in
|
|
128
|
+
// argv so each command's own extractGlobalFlags still sees them.
|
|
129
|
+
return { sub: first, rest: [...argv.slice(0, subIdx), ...argv.slice(subIdx + 1)] };
|
|
130
|
+
}
|
|
131
|
+
async function dispatch(sub, rest, io) {
|
|
62
132
|
switch (sub) {
|
|
63
133
|
case 'wake': {
|
|
64
134
|
const { run } = await import('./wake.js');
|
|
@@ -76,6 +146,10 @@ export async function runCli(argv, io = realStreams()) {
|
|
|
76
146
|
const { run } = await import('./knowledge.js');
|
|
77
147
|
return run(rest, io);
|
|
78
148
|
}
|
|
149
|
+
case 'person': {
|
|
150
|
+
const { run } = await import('./person.js');
|
|
151
|
+
return run(rest, io);
|
|
152
|
+
}
|
|
79
153
|
case 'forget': {
|
|
80
154
|
const { run } = await import('./forget.js');
|
|
81
155
|
return run(rest, io);
|
|
@@ -120,6 +194,10 @@ export async function runCli(argv, io = realStreams()) {
|
|
|
120
194
|
const { run } = await import('./migrate.js');
|
|
121
195
|
return run(rest, io);
|
|
122
196
|
}
|
|
197
|
+
case 'pieces': {
|
|
198
|
+
const { runPiecesCommand } = await import('./pieces.js');
|
|
199
|
+
return runPiecesCommand(rest, io);
|
|
200
|
+
}
|
|
123
201
|
default:
|
|
124
202
|
io.stderr(`Subcommand not implemented yet: ${sub}\n`);
|
|
125
203
|
return 2;
|
package/dist/cli/knowledge.js
CHANGED
|
@@ -6,6 +6,7 @@ import { knowledgeWrite } from '../tools/knowledge-write.js';
|
|
|
6
6
|
import { knowledgeRecall } from '../tools/knowledge-recall.js';
|
|
7
7
|
import { knowledgeMaintain } from '../tools/knowledge-maintain.js';
|
|
8
8
|
import { createKnowledgeBackend } from '../backends/index.js';
|
|
9
|
+
import { resolveIdentityName } from '../config.js';
|
|
9
10
|
import { extractGlobalFlags, resolveEnv } from './args.js';
|
|
10
11
|
import { renderJson } from './io.js';
|
|
11
12
|
const USAGE = `Usage: loom knowledge <subcommand> [options]
|
|
@@ -13,7 +14,7 @@ const USAGE = `Usage: loom knowledge <subcommand> [options]
|
|
|
13
14
|
Subcommands:
|
|
14
15
|
write Upsert an entity page (create or append/revise body in place)
|
|
15
16
|
recall Search knowledge pages (LIKE over title+body+domain)
|
|
16
|
-
maintain Read-only health report (expansion candidates, cold pages, misfile audit)
|
|
17
|
+
maintain Read-only health report (expansion candidates, cold pages, misfile audit, person-page check)
|
|
17
18
|
|
|
18
19
|
Options (write):
|
|
19
20
|
--slug <slug> Entity key for upsert (default: derived from title)
|
|
@@ -112,6 +113,7 @@ export async function run(argv, io) {
|
|
|
112
113
|
body,
|
|
113
114
|
sourcing,
|
|
114
115
|
citations,
|
|
116
|
+
actor: resolveIdentityName(env.contextDir),
|
|
115
117
|
});
|
|
116
118
|
renderJson(io, result);
|
|
117
119
|
return 0;
|
|
@@ -210,6 +212,7 @@ export async function run(argv, io) {
|
|
|
210
212
|
expansionHitThreshold,
|
|
211
213
|
thinBodyThreshold,
|
|
212
214
|
coldDays,
|
|
215
|
+
personCards: true,
|
|
213
216
|
});
|
|
214
217
|
io.stdout(text.endsWith('\n') ? text : text + '\n');
|
|
215
218
|
return 0;
|
package/dist/cli/memory.js
CHANGED
|
@@ -20,7 +20,7 @@ const USAGE = `Usage: loom memory <subcommand> [options]
|
|
|
20
20
|
|
|
21
21
|
Subcommands:
|
|
22
22
|
list Browse memories (table or --json)
|
|
23
|
-
prune Report /
|
|
23
|
+
prune Report / archive expired memories; report stale ones
|
|
24
24
|
similar Surface memories semantically near a ref or text
|
|
25
25
|
audit One-shot health report (counts, stale, duplicates, expired)
|
|
26
26
|
archive Soft-retire a memory with a tombstone (recoverable)
|
|
@@ -42,7 +42,7 @@ Options (list):
|
|
|
42
42
|
|
|
43
43
|
Options (prune):
|
|
44
44
|
--stale-days <n> Stale threshold in days
|
|
45
|
-
--dry-run Report what would be
|
|
45
|
+
--dry-run Report what would be archived, don't archive
|
|
46
46
|
--json Emit PruneResult
|
|
47
47
|
|
|
48
48
|
Options (similar):
|