@plur-ai/core 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -102
- package/dist/index.d.ts +44 -1
- package/dist/index.js +216 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,141 +1,131 @@
|
|
|
1
1
|
# @plur-ai/core
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The engine behind [PLUR](https://plur.ai) — persistent memory for AI agents.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
npm install @plur-ai/core
|
|
7
|
-
```
|
|
5
|
+
You correct your agent on Monday. On Tuesday, it makes the same mistake. PLUR fixes this. Corrections, preferences, and conventions persist across sessions. Your data stays on your disk as plain YAML. Search runs locally with zero API calls.
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
import { Plur } from '@plur-ai/core'
|
|
7
|
+
The result: **Haiku with PLUR memory outperforms Opus without it** — 2.6x better on tool routing, at 10x less cost. The bottleneck isn't model intelligence. It's context.
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
plur.learn('API uses snake_case', { scope: 'project:myapp', type: 'architectural' })
|
|
14
|
-
const injection = plur.inject('fix the endpoint handler', { budget: 2000 })
|
|
15
|
-
console.log(injection.directives) // injected context, ready to prepend
|
|
16
|
-
```
|
|
9
|
+
## Why @plur-ai/core
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
This is the engine that powers everything. Use it directly when you're building your own agent framework or want programmatic control over memory. If you just want to add memory to Claude Code or Cursor, use [`@plur-ai/mcp`](https://www.npmjs.com/package/@plur-ai/mcp) instead — it wraps this package as MCP tools.
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
## Install
|
|
21
14
|
|
|
22
|
-
```
|
|
23
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npm install @plur-ai/core
|
|
24
17
|
```
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
### `learn(statement, context?)`
|
|
29
|
-
|
|
30
|
-
Create an engram. Returns the created `Engram`.
|
|
19
|
+
## Quick start
|
|
31
20
|
|
|
32
21
|
```typescript
|
|
33
|
-
|
|
34
|
-
type: 'behavioral', // behavioral | architectural | procedural | terminological
|
|
35
|
-
scope: 'project:myapp', // namespace for filtering
|
|
36
|
-
domain: 'software.git', // dot-separated domain tag
|
|
37
|
-
source: 'user-correction',
|
|
38
|
-
})
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### `recall(query, options?)`
|
|
22
|
+
import { Plur } from '@plur-ai/core'
|
|
42
23
|
|
|
43
|
-
|
|
24
|
+
const plur = new Plur()
|
|
44
25
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
min_strength: 0.5,
|
|
26
|
+
// Your agent gets corrected — save it
|
|
27
|
+
plur.learn('toEqual() in Vitest is strict — use toMatchObject() for partial matching', {
|
|
28
|
+
type: 'behavioral',
|
|
29
|
+
scope: 'project:my-app',
|
|
30
|
+
domain: 'dev/testing'
|
|
51
31
|
})
|
|
52
|
-
```
|
|
53
32
|
|
|
54
|
-
|
|
33
|
+
// Next session: recall what was learned (hybrid search, zero cost)
|
|
34
|
+
const results = await plur.recallHybrid('vitest assertion matching')
|
|
55
35
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
budget: 2000,
|
|
61
|
-
scope: 'project:myapp',
|
|
36
|
+
// Or inject the best engrams into a system prompt, within a token budget
|
|
37
|
+
const { directives, consider, tokens_used } = plur.inject('Write tests for the user service', {
|
|
38
|
+
scope: 'project:my-app',
|
|
39
|
+
budget: 2000
|
|
62
40
|
})
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### `feedback(id, signal)`
|
|
66
41
|
|
|
67
|
-
Rate
|
|
42
|
+
// Rate what was useful — the system improves over time
|
|
43
|
+
plur.feedback(results[0].id, 'positive')
|
|
68
44
|
|
|
69
|
-
|
|
70
|
-
plur.
|
|
71
|
-
plur.feedback('ENG-002', 'negative') // -0.10 retrieval strength
|
|
72
|
-
plur.feedback('ENG-003', 'neutral') // signal recorded, no strength change
|
|
45
|
+
// Sync across machines via git
|
|
46
|
+
plur.sync('git@github.com:you/plur-memory.git')
|
|
73
47
|
```
|
|
74
48
|
|
|
75
|
-
|
|
49
|
+
## How it works
|
|
76
50
|
|
|
77
|
-
|
|
51
|
+
Knowledge is stored as **engrams** — small assertions that strengthen with use and decay when irrelevant, modeled on how human memory works (ACT-R activation). The system gets better over time, not just bigger.
|
|
78
52
|
|
|
79
|
-
```typescript
|
|
80
|
-
plur.forget('ENG-001', 'API changed')
|
|
81
53
|
```
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Append an episode to the episodic timeline.
|
|
86
|
-
|
|
87
|
-
```typescript
|
|
88
|
-
plur.capture('Deployed v2.0 to production', {
|
|
89
|
-
agent: 'claude-code',
|
|
90
|
-
session_id: 'abc123',
|
|
91
|
-
channel: 'cli',
|
|
92
|
-
tags: ['deploy', 'production'],
|
|
93
|
-
})
|
|
54
|
+
You correct your agent → engram created → YAML on your disk
|
|
55
|
+
Next session starts → relevant ones injected → agent remembers
|
|
56
|
+
You rate the result → engram strengthens → quality improves
|
|
94
57
|
```
|
|
95
58
|
|
|
96
|
-
|
|
59
|
+
Search is fully local: BM25 over enriched text + BGE-small-en-v1.5 embeddings + Reciprocal Rank Fusion. **86.7% on LongMemEval** — on par with cloud solutions that charge per query.
|
|
60
|
+
|
|
61
|
+
## Search modes
|
|
62
|
+
|
|
63
|
+
Five modes, from fastest to most accurate:
|
|
64
|
+
|
|
65
|
+
| Method | Speed | API calls | Best for |
|
|
66
|
+
|--------|-------|-----------|----------|
|
|
67
|
+
| `recall(query)` | Instant | None | Quick keyword lookup |
|
|
68
|
+
| `recallSemantic(query)` | ~200ms | None | Meaning-based search (local embeddings) |
|
|
69
|
+
| `recallHybrid(query)` | ~200ms | None | **Best default** — BM25 + embeddings via RRF |
|
|
70
|
+
| `recallAsync(query, { llm })` | ~1s | 1 LLM call | LLM-assisted semantic filtering |
|
|
71
|
+
| `recallExpanded(query, { llm })` | ~3s | 3-5 LLM calls | Query expansion for exhaustive retrieval |
|
|
72
|
+
|
|
73
|
+
## Full API
|
|
74
|
+
|
|
75
|
+
| Method | What it does |
|
|
76
|
+
|--------|-------------|
|
|
77
|
+
| `learn(statement, context?)` | Store an engram (correction, preference, convention, decision) |
|
|
78
|
+
| `recall(query, options?)` | BM25 keyword search — instant, zero cost |
|
|
79
|
+
| `recallHybrid(query, options?)` | BM25 + embeddings merged via RRF — best default |
|
|
80
|
+
| `recallSemantic(query, options?)` | Embedding-only search — meaning over keywords |
|
|
81
|
+
| `recallAsync(query, { llm })` | LLM-assisted semantic filtering |
|
|
82
|
+
| `recallExpanded(query, { llm })` | Query expansion + hybrid + RRF merge |
|
|
83
|
+
| `inject(task, options?)` | Select engrams for a task within a token budget |
|
|
84
|
+
| `feedback(id, signal)` | Rate an engram — trains injection relevance over time |
|
|
85
|
+
| `forget(id, reason?)` | Retire an engram (history preserved) |
|
|
86
|
+
| `sync(remote?)` | Git-based sync across machines |
|
|
87
|
+
| `syncStatus()` | Check sync state without making changes |
|
|
88
|
+
| `capture(summary, context?)` | Record a session event to the episodic timeline |
|
|
89
|
+
| `timeline(query?)` | Query past episodes by time, agent, or search |
|
|
90
|
+
| `ingest(content, options?)` | Extract engram candidates from text via pattern matching |
|
|
91
|
+
| `installPack(source)` | Install a shareable engram pack |
|
|
92
|
+
| `exportPack(engrams, dir, manifest)` | Export engrams as a shareable pack |
|
|
93
|
+
| `listPacks()` | List installed packs |
|
|
94
|
+
| `status()` | System health — counts, storage root, config |
|
|
95
|
+
|
|
96
|
+
## Storage
|
|
97
|
+
|
|
98
|
+
Everything is plain YAML. Open it, read it, edit it, version it.
|
|
97
99
|
|
|
98
|
-
Query the episodic timeline. Returns `Episode[]`.
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
const episodes = plur.timeline({
|
|
102
|
-
since: new Date('2025-01-01'),
|
|
103
|
-
agent: 'claude-code',
|
|
104
|
-
search: 'deploy',
|
|
105
|
-
})
|
|
106
100
|
```
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const candidates = plur.ingest(markdownContent, {
|
|
114
|
-
extract_only: true, // preview without saving
|
|
115
|
-
scope: 'project:myapp',
|
|
116
|
-
source: 'docs/architecture.md',
|
|
117
|
-
})
|
|
118
|
-
// set extract_only: false (default) to auto-save candidates as engrams
|
|
101
|
+
~/.plur/
|
|
102
|
+
├── engrams.yaml # learned knowledge
|
|
103
|
+
├── episodes.yaml # session timeline
|
|
104
|
+
├── candidates.yaml # pending engrams
|
|
105
|
+
├── config.yaml # settings
|
|
106
|
+
└── packs/ # installed engram packs
|
|
119
107
|
```
|
|
120
108
|
|
|
121
|
-
|
|
109
|
+
Override the location with `PLUR_PATH` env var or `new Plur({ path: '...' })`.
|
|
122
110
|
|
|
123
|
-
|
|
111
|
+
## Benchmark
|
|
124
112
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
113
|
+
| Metric | Score |
|
|
114
|
+
|--------|-------|
|
|
115
|
+
| LongMemEval overall | **86.7%** |
|
|
116
|
+
| Hit@10 (retrieval) | 93.3% |
|
|
117
|
+
| A/B win rate vs no memory | 89% |
|
|
118
|
+
| House rules accuracy | 100% |
|
|
130
119
|
|
|
131
|
-
|
|
120
|
+
[Full methodology →](https://plur.ai/benchmark.html)
|
|
132
121
|
|
|
133
|
-
|
|
122
|
+
## Related packages
|
|
134
123
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
124
|
+
| Package | For |
|
|
125
|
+
|---------|-----|
|
|
126
|
+
| [`@plur-ai/mcp`](https://www.npmjs.com/package/@plur-ai/mcp) | Claude Code, Cursor, Windsurf (MCP server) |
|
|
127
|
+
| [`@plur-ai/claw`](https://www.npmjs.com/package/@plur-ai/claw) | OpenClaw (automatic memory plugin) |
|
|
138
128
|
|
|
139
129
|
## License
|
|
140
130
|
|
|
141
|
-
Apache-2.0
|
|
131
|
+
Apache-2.0 · [GitHub](https://github.com/plur-ai/plur) · [plur.ai](https://plur.ai)
|
package/dist/index.d.ts
CHANGED
|
@@ -546,6 +546,21 @@ interface PackInfo {
|
|
|
546
546
|
}
|
|
547
547
|
declare function listPacks(packsDir: string): PackInfo[];
|
|
548
548
|
|
|
549
|
+
interface SyncStatus {
|
|
550
|
+
initialized: boolean;
|
|
551
|
+
remote: string | null;
|
|
552
|
+
dirty: boolean;
|
|
553
|
+
branch: string | null;
|
|
554
|
+
ahead: number;
|
|
555
|
+
behind: number;
|
|
556
|
+
}
|
|
557
|
+
interface SyncResult {
|
|
558
|
+
action: 'initialized' | 'committed' | 'synced' | 'up-to-date';
|
|
559
|
+
message: string;
|
|
560
|
+
remote: string | null;
|
|
561
|
+
files_changed: number;
|
|
562
|
+
}
|
|
563
|
+
|
|
549
564
|
declare const EpisodeSchema: z.ZodObject<{
|
|
550
565
|
id: z.ZodString;
|
|
551
566
|
summary: z.ZodString;
|
|
@@ -669,6 +684,30 @@ interface TimelineQuery {
|
|
|
669
684
|
/** Build searchable text from all engram fields */
|
|
670
685
|
declare function engramSearchText(engram: Engram): string;
|
|
671
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Non-blocking version check against npm registry.
|
|
689
|
+
* Caches result in memory — one fetch per process lifetime.
|
|
690
|
+
* Never throws or blocks startup.
|
|
691
|
+
*/
|
|
692
|
+
interface VersionCheckResult {
|
|
693
|
+
current: string;
|
|
694
|
+
latest: string | null;
|
|
695
|
+
updateAvailable: boolean;
|
|
696
|
+
checkedAt: number | null;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Check npm for a newer version. Fetches once, caches forever (process lifetime).
|
|
700
|
+
* Fire-and-forget: call at startup, read later via getCachedUpdateCheck().
|
|
701
|
+
*/
|
|
702
|
+
declare function checkForUpdate(packageName: string, currentVersion: string, onResult?: (result: VersionCheckResult) => void): Promise<VersionCheckResult>;
|
|
703
|
+
/**
|
|
704
|
+
* Read the cached version check result. Returns null if checkForUpdate() hasn't
|
|
705
|
+
* completed yet. This is the zero-cost read path for assemblers.
|
|
706
|
+
*/
|
|
707
|
+
declare function getCachedUpdateCheck(packageName: string): VersionCheckResult | null;
|
|
708
|
+
/** Clear cache (for testing). */
|
|
709
|
+
declare function clearVersionCache(): void;
|
|
710
|
+
|
|
672
711
|
interface IngestOptions {
|
|
673
712
|
source?: string;
|
|
674
713
|
extract_only?: boolean;
|
|
@@ -748,8 +787,12 @@ declare class Plur {
|
|
|
748
787
|
};
|
|
749
788
|
/** List all installed packs. */
|
|
750
789
|
listPacks(): ReturnType<typeof listPacks>;
|
|
790
|
+
/** Sync engrams to git. Initializes repo on first call, commits + push/pull on subsequent calls. */
|
|
791
|
+
sync(remote?: string): SyncResult;
|
|
792
|
+
/** Get git sync status without making changes. */
|
|
793
|
+
syncStatus(): SyncStatus;
|
|
751
794
|
/** Return system health info. */
|
|
752
795
|
status(): StatusResult;
|
|
753
796
|
}
|
|
754
797
|
|
|
755
|
-
export { type Association, type CaptureContext, type Engram, type Episode, type IngestCandidate, type IngestOptions, type InjectOptions, type InjectionResult, type KnowledgeAnchor, type LearnContext, type LlmFunction, type PackManifest, Plur, type PlurConfig, type RecallOptions, type StatusResult, type TimelineQuery, engramSearchText };
|
|
798
|
+
export { type Association, type CaptureContext, type Engram, type Episode, type IngestCandidate, type IngestOptions, type InjectOptions, type InjectionResult, type KnowledgeAnchor, type LearnContext, type LlmFunction, type PackManifest, Plur, type PlurConfig, type RecallOptions, type StatusResult, type SyncResult, type SyncStatus, type TimelineQuery, type VersionCheckResult, checkForUpdate, clearVersionCache, engramSearchText, getCachedUpdateCheck };
|
package/dist/index.js
CHANGED
|
@@ -759,10 +759,10 @@ function loadCache(cachePath) {
|
|
|
759
759
|
return {};
|
|
760
760
|
}
|
|
761
761
|
}
|
|
762
|
-
function saveCache(cachePath,
|
|
762
|
+
function saveCache(cachePath, cache2) {
|
|
763
763
|
const dir = cachePath.substring(0, cachePath.lastIndexOf("/"));
|
|
764
764
|
if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
|
|
765
|
-
writeFileSync3(cachePath, JSON.stringify(
|
|
765
|
+
writeFileSync3(cachePath, JSON.stringify(cache2));
|
|
766
766
|
}
|
|
767
767
|
function hashStatement(statement) {
|
|
768
768
|
let hash = 0;
|
|
@@ -774,18 +774,18 @@ function hashStatement(statement) {
|
|
|
774
774
|
async function embeddingSearch(engrams, query, limit, storagePath) {
|
|
775
775
|
if (engrams.length === 0) return [];
|
|
776
776
|
const cachePath = storagePath ? join2(storagePath, ".embeddings-cache.json") : ".embeddings-cache.json";
|
|
777
|
-
const
|
|
777
|
+
const cache2 = loadCache(cachePath);
|
|
778
778
|
const queryEmbedding = await embed(query);
|
|
779
779
|
const similarities = [];
|
|
780
780
|
for (const engram of engrams) {
|
|
781
781
|
const searchText = engramSearchText(engram);
|
|
782
782
|
const hash = hashStatement(searchText);
|
|
783
783
|
let engramEmbedding;
|
|
784
|
-
if (
|
|
785
|
-
engramEmbedding = new Float32Array(
|
|
784
|
+
if (cache2[engram.id]?.hash === hash) {
|
|
785
|
+
engramEmbedding = new Float32Array(cache2[engram.id].embedding);
|
|
786
786
|
} else {
|
|
787
787
|
engramEmbedding = await embed(searchText);
|
|
788
|
-
|
|
788
|
+
cache2[engram.id] = {
|
|
789
789
|
hash,
|
|
790
790
|
embedding: Array.from(engramEmbedding)
|
|
791
791
|
};
|
|
@@ -793,7 +793,7 @@ async function embeddingSearch(engrams, query, limit, storagePath) {
|
|
|
793
793
|
const score = cosineSimilarity(queryEmbedding, engramEmbedding);
|
|
794
794
|
similarities.push({ engram, score });
|
|
795
795
|
}
|
|
796
|
-
saveCache(cachePath,
|
|
796
|
+
saveCache(cachePath, cache2);
|
|
797
797
|
similarities.sort((a, b) => b.score - a.score);
|
|
798
798
|
return similarities.slice(0, limit).map((s) => s.engram);
|
|
799
799
|
}
|
|
@@ -996,6 +996,203 @@ ${manifest.description || ""}
|
|
|
996
996
|
return { path: outputDir, engram_count: engrams.length };
|
|
997
997
|
}
|
|
998
998
|
|
|
999
|
+
// src/sync.ts
|
|
1000
|
+
import { execSync } from "child_process";
|
|
1001
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1002
|
+
import { join as join4 } from "path";
|
|
1003
|
+
var GITIGNORE = `# PLUR \u2014 derived/cache files (regenerated automatically)
|
|
1004
|
+
embeddings/
|
|
1005
|
+
*.db
|
|
1006
|
+
*.sqlite
|
|
1007
|
+
exchange/
|
|
1008
|
+
`;
|
|
1009
|
+
function git(args, cwd) {
|
|
1010
|
+
return execSync(`git ${args}`, { cwd, encoding: "utf8", timeout: 3e4 }).trim();
|
|
1011
|
+
}
|
|
1012
|
+
function gitSafe(args, cwd) {
|
|
1013
|
+
try {
|
|
1014
|
+
return git(args, cwd);
|
|
1015
|
+
} catch {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function isGitRepo(root) {
|
|
1020
|
+
return existsSync7(join4(root, ".git"));
|
|
1021
|
+
}
|
|
1022
|
+
function hasGitCli() {
|
|
1023
|
+
try {
|
|
1024
|
+
execSync("git --version", { encoding: "utf8", timeout: 5e3 });
|
|
1025
|
+
return true;
|
|
1026
|
+
} catch {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function getRemote(root) {
|
|
1031
|
+
return gitSafe("remote get-url origin", root);
|
|
1032
|
+
}
|
|
1033
|
+
function isDirty(root) {
|
|
1034
|
+
const status = gitSafe("status --porcelain", root);
|
|
1035
|
+
return status !== null && status.length > 0;
|
|
1036
|
+
}
|
|
1037
|
+
function countDiff(root, direction) {
|
|
1038
|
+
const tracking = gitSafe("rev-parse --abbrev-ref @{u}", root);
|
|
1039
|
+
if (!tracking) return 0;
|
|
1040
|
+
const flag = direction === "ahead" ? "left" : "right";
|
|
1041
|
+
const count = gitSafe(`rev-list --${flag}-only --count HEAD...@{u}`, root);
|
|
1042
|
+
return count ? parseInt(count, 10) : 0;
|
|
1043
|
+
}
|
|
1044
|
+
function getSyncStatus(root) {
|
|
1045
|
+
if (!isGitRepo(root)) {
|
|
1046
|
+
return { initialized: false, remote: null, dirty: false, branch: null, ahead: 0, behind: 0 };
|
|
1047
|
+
}
|
|
1048
|
+
const branch = gitSafe("rev-parse --abbrev-ref HEAD", root);
|
|
1049
|
+
const remote = getRemote(root);
|
|
1050
|
+
if (remote) gitSafe("fetch origin --quiet", root);
|
|
1051
|
+
return {
|
|
1052
|
+
initialized: true,
|
|
1053
|
+
remote,
|
|
1054
|
+
dirty: isDirty(root),
|
|
1055
|
+
branch,
|
|
1056
|
+
ahead: countDiff(root, "ahead"),
|
|
1057
|
+
behind: countDiff(root, "behind")
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function initRepo(root) {
|
|
1061
|
+
git("init", root);
|
|
1062
|
+
writeFileSync5(join4(root, ".gitignore"), GITIGNORE);
|
|
1063
|
+
git("add -A", root);
|
|
1064
|
+
git('commit -m "Initial PLUR engram store"', root);
|
|
1065
|
+
}
|
|
1066
|
+
function commitChanges(root) {
|
|
1067
|
+
if (!isDirty(root)) return 0;
|
|
1068
|
+
git("add -A", root);
|
|
1069
|
+
const diff = gitSafe("diff --cached --stat --shortstat", root);
|
|
1070
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
|
|
1071
|
+
git(`commit -m "plur sync ${now}"`, root);
|
|
1072
|
+
const match = diff?.match(/(\d+) file/);
|
|
1073
|
+
return match ? parseInt(match[1], 10) : 1;
|
|
1074
|
+
}
|
|
1075
|
+
function pullRebase(root) {
|
|
1076
|
+
const result = gitSafe("pull --rebase origin main", root);
|
|
1077
|
+
if (result !== null) return true;
|
|
1078
|
+
gitSafe("rebase --abort", root);
|
|
1079
|
+
const mergeResult = gitSafe("pull origin main --no-edit", root);
|
|
1080
|
+
if (mergeResult !== null) return true;
|
|
1081
|
+
git("add -A", root);
|
|
1082
|
+
gitSafe('commit -m "plur sync: merge conflict resolved (kept both)"', root);
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
function sync(root, remote) {
|
|
1086
|
+
if (!hasGitCli()) {
|
|
1087
|
+
throw new Error("git is not installed. Install git to enable sync.");
|
|
1088
|
+
}
|
|
1089
|
+
if (!isGitRepo(root)) {
|
|
1090
|
+
initRepo(root);
|
|
1091
|
+
if (remote) {
|
|
1092
|
+
git(`remote add origin ${remote}`, root);
|
|
1093
|
+
const branch = git("rev-parse --abbrev-ref HEAD", root);
|
|
1094
|
+
git(`push -u origin ${branch}`, root);
|
|
1095
|
+
return { action: "initialized", message: `Initialized and pushed to ${remote}`, remote, files_changed: 0 };
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
action: "initialized",
|
|
1099
|
+
message: "Initialized local git repo. Call plur.sync with remote to enable cross-device sync.",
|
|
1100
|
+
remote: null,
|
|
1101
|
+
files_changed: 0
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
const existingRemote = getRemote(root);
|
|
1105
|
+
if (remote && !existingRemote) {
|
|
1106
|
+
git(`remote add origin ${remote}`, root);
|
|
1107
|
+
const filesChanged2 = commitChanges(root);
|
|
1108
|
+
const branch = git("rev-parse --abbrev-ref HEAD", root);
|
|
1109
|
+
git(`push -u origin ${branch}`, root);
|
|
1110
|
+
return { action: "synced", message: `Remote added and pushed to ${remote}`, remote, files_changed: filesChanged2 };
|
|
1111
|
+
}
|
|
1112
|
+
if (!existingRemote) {
|
|
1113
|
+
const filesChanged2 = commitChanges(root);
|
|
1114
|
+
if (filesChanged2 === 0) {
|
|
1115
|
+
return { action: "up-to-date", message: 'No changes to commit. Add a remote with plur.sync({ remote: "..." }) to enable cross-device sync.', remote: null, files_changed: 0 };
|
|
1116
|
+
}
|
|
1117
|
+
return { action: "committed", message: `Committed ${filesChanged2} file(s) locally.`, remote: null, files_changed: filesChanged2 };
|
|
1118
|
+
}
|
|
1119
|
+
const filesChanged = commitChanges(root);
|
|
1120
|
+
gitSafe("fetch origin --quiet", root);
|
|
1121
|
+
const behind = countDiff(root, "behind");
|
|
1122
|
+
const aheadBefore = countDiff(root, "ahead");
|
|
1123
|
+
if (behind > 0) {
|
|
1124
|
+
pullRebase(root);
|
|
1125
|
+
}
|
|
1126
|
+
const aheadAfter = countDiff(root, "ahead");
|
|
1127
|
+
if (aheadAfter > 0) {
|
|
1128
|
+
gitSafe("push origin", root);
|
|
1129
|
+
}
|
|
1130
|
+
if (filesChanged === 0 && behind === 0 && aheadBefore === 0) {
|
|
1131
|
+
return { action: "up-to-date", message: "Already in sync.", remote: existingRemote, files_changed: 0 };
|
|
1132
|
+
}
|
|
1133
|
+
const parts = [];
|
|
1134
|
+
if (filesChanged > 0) parts.push(`${filesChanged} file(s) committed`);
|
|
1135
|
+
if (behind > 0) parts.push(`pulled ${behind} remote commit(s)`);
|
|
1136
|
+
if (aheadAfter === 0 && aheadBefore > 0) parts.push("pushed");
|
|
1137
|
+
return {
|
|
1138
|
+
action: "synced",
|
|
1139
|
+
message: `Synced. ${parts.join(", ")}.`,
|
|
1140
|
+
remote: existingRemote,
|
|
1141
|
+
files_changed: filesChanged
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/version-check.ts
|
|
1146
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1147
|
+
async function checkForUpdate(packageName, currentVersion, onResult) {
|
|
1148
|
+
const cached = cache.get(packageName);
|
|
1149
|
+
if (cached) {
|
|
1150
|
+
if (onResult) onResult(cached);
|
|
1151
|
+
return cached;
|
|
1152
|
+
}
|
|
1153
|
+
const result = { current: currentVersion, latest: null, updateAvailable: false, checkedAt: null };
|
|
1154
|
+
try {
|
|
1155
|
+
const controller = new AbortController();
|
|
1156
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
1157
|
+
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
|
|
1158
|
+
signal: controller.signal,
|
|
1159
|
+
headers: { Accept: "application/json" }
|
|
1160
|
+
});
|
|
1161
|
+
clearTimeout(timeout);
|
|
1162
|
+
if (!res.ok) {
|
|
1163
|
+
cache.set(packageName, result);
|
|
1164
|
+
return result;
|
|
1165
|
+
}
|
|
1166
|
+
const data = await res.json();
|
|
1167
|
+
if (!data.version) {
|
|
1168
|
+
cache.set(packageName, result);
|
|
1169
|
+
return result;
|
|
1170
|
+
}
|
|
1171
|
+
result.latest = data.version;
|
|
1172
|
+
result.updateAvailable = isNewer(data.version, currentVersion);
|
|
1173
|
+
result.checkedAt = Date.now();
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
cache.set(packageName, result);
|
|
1177
|
+
if (onResult) onResult(result);
|
|
1178
|
+
return result;
|
|
1179
|
+
}
|
|
1180
|
+
function getCachedUpdateCheck(packageName) {
|
|
1181
|
+
return cache.get(packageName) ?? null;
|
|
1182
|
+
}
|
|
1183
|
+
function clearVersionCache() {
|
|
1184
|
+
cache.clear();
|
|
1185
|
+
}
|
|
1186
|
+
function isNewer(a, b) {
|
|
1187
|
+
const pa = a.split(".").map(Number);
|
|
1188
|
+
const pb = b.split(".").map(Number);
|
|
1189
|
+
for (let i = 0; i < 3; i++) {
|
|
1190
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
|
|
1191
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
|
|
1192
|
+
}
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
999
1196
|
// src/index.ts
|
|
1000
1197
|
var INGEST_PATTERNS = [
|
|
1001
1198
|
{ re: /(?:we decided|the decision is|agreed to)\s+(.+?)\.?$/gim, type: "architectural" },
|
|
@@ -1245,6 +1442,14 @@ var Plur = class {
|
|
|
1245
1442
|
listPacks() {
|
|
1246
1443
|
return listPacks(this.paths.packs);
|
|
1247
1444
|
}
|
|
1445
|
+
/** Sync engrams to git. Initializes repo on first call, commits + push/pull on subsequent calls. */
|
|
1446
|
+
sync(remote) {
|
|
1447
|
+
return sync(this.paths.root, remote);
|
|
1448
|
+
}
|
|
1449
|
+
/** Get git sync status without making changes. */
|
|
1450
|
+
syncStatus() {
|
|
1451
|
+
return getSyncStatus(this.paths.root);
|
|
1452
|
+
}
|
|
1248
1453
|
/** Return system health info. */
|
|
1249
1454
|
status() {
|
|
1250
1455
|
const engrams = loadEngrams(this.paths.engrams);
|
|
@@ -1261,5 +1466,8 @@ var Plur = class {
|
|
|
1261
1466
|
};
|
|
1262
1467
|
export {
|
|
1263
1468
|
Plur,
|
|
1264
|
-
|
|
1469
|
+
checkForUpdate,
|
|
1470
|
+
clearVersionCache,
|
|
1471
|
+
engramSearchText,
|
|
1472
|
+
getCachedUpdateCheck
|
|
1265
1473
|
};
|