peaks-cli 1.0.28 → 1.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/bin/peaks.js CHANGED
File without changes
@@ -227,11 +227,11 @@ export function registerCoreAndArtifactCommands(program, io) {
227
227
  const memory = program.command('memory').description('Manage project-local Peaks memory');
228
228
  addJsonOption(memory
229
229
  .command('extract')
230
- .description('Extract stable project memory from skill artifacts into project .claude/memory')
230
+ .description('Extract stable project memory from skill artifacts into project .peaks/memory')
231
231
  .requiredOption('--project <path>', 'target project root')
232
232
  .requiredOption('--artifact <path...>', 'skill artifact paths inside the project')
233
233
  .option('--dry-run', 'preview writes without changing files', true)
234
- .option('--apply', 'write extracted memories into project .claude/memory')).action((options) => {
234
+ .option('--apply', 'write extracted memories into project .peaks/memory')).action((options) => {
235
235
  try {
236
236
  const result = executeProjectMemoryExtract({ projectRoot: options.project, artifactPaths: options.artifact, apply: options.apply === true });
237
237
  printResult(io, ok('memory.extract', summarizeProjectMemoryExtractResult(result)), options.json);
@@ -243,11 +243,11 @@ export function registerCoreAndArtifactCommands(program, io) {
243
243
  });
244
244
  addJsonOption(memory
245
245
  .command('sync')
246
- .description('Back up project .claude/memory into the artifact workspace')
246
+ .description('Back up project .peaks/memory into the artifact workspace')
247
247
  .requiredOption('--project <path>', 'target project root')
248
248
  .requiredOption('--workspace <path>', 'artifact workspace path')
249
249
  .option('--dry-run', 'preview copies without changing files', true)
250
- .option('--apply', 'copy project .claude/memory into artifact workspace backup')).action((options) => {
250
+ .option('--apply', 'copy project .peaks/memory into artifact workspace backup')).action((options) => {
251
251
  try {
252
252
  const result = executeProjectMemoryBackup({ projectRoot: options.project, artifactWorkspacePath: options.workspace, apply: options.apply === true });
253
253
  printResult(io, ok('memory.sync', summarizeProjectMemoryBackupResult(result)), options.json);
@@ -1,15 +1,8 @@
1
1
  import { loadProjectDashboard } from '../../services/dashboard/project-dashboard-service.js';
2
- import { generateProjectContext, loadOntology, readProjectContext, upsertDecision, upsertModule } from '../../services/memory/project-context-service.js';
2
+ import { generateProjectContext, readProjectContext } from '../../services/memory/project-context-service.js';
3
+ import { readProjectMemories } from '../../services/memory/project-memory-service.js';
3
4
  import { fail, ok } from '../../shared/result.js';
4
5
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
5
- function defined(obj) {
6
- const result = {};
7
- for (const [k, v] of Object.entries(obj)) {
8
- if (v !== undefined)
9
- result[k] = v;
10
- }
11
- return result;
12
- }
13
6
  export function registerProjectCommands(program, io) {
14
7
  const project = program.command('project').description('Aggregate Peaks state for a target project (read-only)');
15
8
  addJsonOption(project
@@ -75,104 +68,33 @@ export function registerProjectCommands(program, io) {
75
68
  process.exitCode = 1;
76
69
  }
77
70
  });
78
- // --- Ontology commands (structured project memory for LLM consumption) ---
79
- const ontology = project.command('ontology').description('Query structured project memory (modules, decisions, conventions)');
80
- addJsonOption(ontology
81
- .command('show')
82
- .description('Read the full ontology JSON for LLM consumption')
83
- .requiredOption('--project <path>', 'target project root')).action((options) => {
84
- try {
85
- const onto = loadOntology(options.project);
86
- if (onto === null) {
87
- // Auto-generate if missing
88
- const result = generateProjectContext(options.project);
89
- printResult(io, ok('project.ontology', result.ontology), options.json);
90
- return;
91
- }
92
- printResult(io, ok('project.ontology', onto), options.json);
93
- }
94
- catch (error) {
95
- printResult(io, fail('project.ontology', 'ONTOLOGY_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path']), options.json);
96
- process.exitCode = 1;
97
- }
98
- });
99
- addJsonOption(ontology
100
- .command('module')
101
- .description('Record or query a project module')
102
- .requiredOption('--project <path>', 'target project root')
103
- .option('--id <id>', 'module id (kebab-case)')
104
- .option('--path <path>', 'file path for the module')
105
- .option('--risk <level>', 'risk level: low, medium, high')
106
- .option('--summary <text>', 'brief module description')
107
- .option('--session <id>', 'session id')
108
- .option('--put', 'write/update the module entry')).action((options) => {
109
- try {
110
- if (options.put) {
111
- if (!options.id || !options.path || !options.session) {
112
- printResult(io, fail('project.ontology.module', 'MISSING_FIELDS', '--id, --path, --session required with --put', {}, ['Provide all required fields']), options.json);
113
- process.exitCode = 1;
114
- return;
115
- }
116
- const risk = (options.risk === 'low' || options.risk === 'medium' || options.risk === 'high') ? options.risk : undefined;
117
- const result = upsertModule(options.project, defined({
118
- id: options.id,
119
- path: options.path,
120
- session: options.session,
121
- risk,
122
- summary: options.summary
123
- }));
124
- printResult(io, ok('project.ontology.module', { modules: result.modules }), options.json);
125
- return;
126
- }
127
- const onto = loadOntology(options.project) ?? generateProjectContext(options.project).ontology;
128
- if (options.id) {
129
- const mod = onto.modules.find((m) => m.id === options.id);
130
- printResult(io, ok('project.ontology.module', mod ?? { notFound: true, id: options.id }), options.json);
131
- return;
132
- }
133
- printResult(io, ok('project.ontology.module', { modules: onto.modules }), options.json);
134
- }
135
- catch (error) {
136
- printResult(io, fail('project.ontology.module', 'ONTOLOGY_MODULE_FAILED', getErrorMessage(error), {}, []), options.json);
137
- process.exitCode = 1;
138
- }
139
- });
140
- addJsonOption(ontology
141
- .command('decision')
142
- .description('Record or query architectural decisions')
71
+ // --- Structured project memory (durable, LLM-authored, stored under .peaks/memory) ---
72
+ addJsonOption(project
73
+ .command('memories')
74
+ .description('Read durable project memories (decisions, conventions, modules, rules) from .peaks/memory for LLM consumption')
143
75
  .requiredOption('--project <path>', 'target project root')
144
- .option('--id <id>', 'decision id')
145
- .option('--what <text>', 'what was decided')
146
- .option('--why <text>', 'rationale behind the decision')
147
- .option('--scope <modules>', 'comma-separated module ids')
148
- .option('--session <id>', 'session id')
149
- .option('--date <date>', 'decision date')
150
- .option('--put', 'write/update the decision')).action((options) => {
76
+ .option('--kind <kind>', 'filter by kind: project, rule, decision, reference, feedback, convention, module')).action((options) => {
151
77
  try {
152
- if (options.put) {
153
- if (!options.id || !options.what || !options.session || !options.date) {
154
- printResult(io, fail('project.ontology.decision', 'MISSING_FIELDS', '--id, --what, --session, --date required with --put', {}, []), options.json);
155
- process.exitCode = 1;
156
- return;
157
- }
158
- const result = upsertDecision(options.project, defined({
159
- id: options.id, what: options.what, why: options.why,
160
- scope: options.scope ? options.scope.split(',').map((s) => s.trim()).filter(Boolean) : [],
161
- session: options.session, date: options.date
162
- }));
163
- printResult(io, ok('project.ontology.decision', { decisions: result.decisions }), options.json);
78
+ const result = readProjectMemories(options.project);
79
+ if (options.kind) {
80
+ const memories = result.memories.filter((memory) => memory.kind === options.kind);
81
+ printResult(io, ok('project.memories', {
82
+ memoryDir: result.memoryDir,
83
+ kind: options.kind,
84
+ total: memories.length,
85
+ memories
86
+ }), options.json);
164
87
  return;
165
88
  }
166
- const onto = loadOntology(options.project) ?? generateProjectContext(options.project).ontology;
167
- if (options.id) {
168
- const dec = onto.decisions.find((d) => d.id === options.id);
169
- printResult(io, ok('project.ontology.decision', dec ?? { notFound: true, id: options.id }), options.json);
170
- return;
171
- }
172
- printResult(io, ok('project.ontology.decision', { decisions: onto.decisions }), options.json);
89
+ printResult(io, ok('project.memories', {
90
+ memoryDir: result.memoryDir,
91
+ total: result.total,
92
+ byKind: result.byKind,
93
+ memories: result.memories
94
+ }), options.json);
173
95
  }
174
96
  catch (error) {
175
- printResult(io, fail('project.ontology.decision', 'ONTOLOGY_DECISION_FAILED', getErrorMessage(error), {}, []), options.json);
97
+ printResult(io, fail('project.memories', 'PROJECT_MEMORIES_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path and .peaks/memory directory']), options.json);
176
98
  process.exitCode = 1;
177
99
  }
178
100
  });
@@ -2,48 +2,10 @@ export type ProjectContextSection = {
2
2
  heading: string;
3
3
  body: string;
4
4
  };
5
- export type Module = {
6
- id: string;
7
- path: string;
8
- risk?: 'low' | 'medium' | 'high';
9
- sessions: string[];
10
- summary?: string;
11
- };
12
- export type Decision = {
13
- id: string;
14
- what: string;
15
- why?: string;
16
- scope: string[];
17
- session: string;
18
- date: string;
19
- };
20
- export type Convention = {
21
- id: string;
22
- rule: string;
23
- category: 'code-style' | 'architecture' | 'naming' | 'tooling' | 'other';
24
- source: string;
25
- date: string;
26
- };
27
- export type Ontology = {
28
- version: 1;
29
- updated: string;
30
- project: string;
31
- modules: Module[];
32
- decisions: Decision[];
33
- conventions: Convention[];
34
- };
35
- export declare function loadOntology(projectRoot: string): Ontology | null;
36
- export declare function saveOntology(projectRoot: string, onto: Ontology): void;
37
- export declare function upsertModule(projectRoot: string, mod: Omit<Module, 'sessions'> & {
38
- session: string;
39
- }): Ontology;
40
- export declare function upsertDecision(projectRoot: string, dec: Decision): Ontology;
41
- export declare function upsertConvention(projectRoot: string, conv: Convention): Ontology;
42
5
  export declare function generateProjectContext(projectRoot: string): {
43
6
  path: string;
44
7
  content: string;
45
8
  sessionCount: number;
46
- ontology: Ontology;
47
9
  };
48
10
  export declare function readProjectContext(projectRoot: string): string | null;
49
11
  export declare function getProjectContextPath(projectRoot: string): string;
@@ -1,8 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join, relative } from 'node:path';
2
+ import { join } from 'node:path';
3
3
  import { listSessionMetas } from '../session/session-manager.js';
4
4
  const PROJECT_CONTEXT_FILE = '.peaks/PROJECT.md';
5
- const ONTOLOGY_FILE = '.peaks/ontology.json';
6
5
  const CONTEXT_HEADER = `# Peaks Project Context
7
6
 
8
7
  > Auto-generated project memory. Peaks reads this at the start of each session to understand
@@ -46,26 +45,6 @@ function listMdFiles(dir, maxDepth = 3) {
46
45
  }
47
46
  return results;
48
47
  }
49
- function extractArtifactSummary(filePath, sessionRoot) {
50
- try {
51
- const content = readFileSync(filePath, 'utf8');
52
- const lines = content.split(/\r?\n/);
53
- const firstHeading = lines.find((l) => /^#\s/.test(l))?.replace(/^#\s+/, '').trim();
54
- const stateLine = lines.find((l) => /^\-\s*state:\s*/.test(l))?.trim();
55
- const relPath = relative(sessionRoot, filePath).split(/[\\/]/).join('/');
56
- const parts = [];
57
- if (firstHeading)
58
- parts.push(firstHeading);
59
- if (stateLine)
60
- parts.push(stateLine.replace(/^-\s*state:\s*/, ''));
61
- if (parts.length === 0)
62
- return `- \`${relPath}\``;
63
- return `- \`${relPath}\` — ${parts.join(' | ')}`;
64
- }
65
- catch {
66
- return null;
67
- }
68
- }
69
48
  function extractOneLineSummary(sessionRoot) {
70
49
  const artifacts = listMdFiles(sessionRoot, 4);
71
50
  for (const artifact of artifacts.slice(0, 5)) {
@@ -88,27 +67,6 @@ function extractOneLineSummary(sessionRoot) {
88
67
  }
89
68
  return null;
90
69
  }
91
- function renderSessionBlock(meta, projectRoot) {
92
- const title = meta.title ?? 'Untitled';
93
- const date = meta.createdAt ? meta.createdAt.slice(0, 10) : '?';
94
- const skill = meta.skill ?? '-';
95
- const mode = meta.mode ?? '-';
96
- let block = `### ${date} — ${title}\n`;
97
- block += `- ${skill} (${mode})`;
98
- const sessionRoot = join(projectRoot, '.peaks', meta.sessionId);
99
- const summary = extractOneLineSummary(sessionRoot);
100
- if (summary) {
101
- block += ` — ${summary.slice(0, 120)}`;
102
- }
103
- block += '\n';
104
- // Key artifact paths only
105
- const artifacts = listMdFiles(sessionRoot, 3);
106
- if (artifacts.length > 0) {
107
- const paths = artifacts.slice(0, 8).map((f) => relative(sessionRoot, f).split(/[\\/]/).join('/'));
108
- block += ` ${paths.join(' ')}\n`;
109
- }
110
- return block;
111
- }
112
70
  function buildSessionHistory(projectRoot) {
113
71
  const metas = listSessionMetas(projectRoot);
114
72
  if (metas.length === 0) {
@@ -139,263 +97,6 @@ function buildSessionHistory(projectRoot) {
139
97
  body += `\n${MANAGED_BLOCK_END}`;
140
98
  return body;
141
99
  }
142
- // --- Ontology engine ---
143
- function emptyOntology(projectName) {
144
- return {
145
- version: 1,
146
- updated: new Date().toISOString(),
147
- project: projectName,
148
- modules: [],
149
- decisions: [],
150
- conventions: []
151
- };
152
- }
153
- function ontoPath(projectRoot) {
154
- return join(projectRoot, ONTOLOGY_FILE);
155
- }
156
- export function loadOntology(projectRoot) {
157
- const path = ontoPath(projectRoot);
158
- if (!existsSync(path))
159
- return null;
160
- try {
161
- const raw = JSON.parse(readFileSync(path, 'utf8'));
162
- if (raw?.version === 1 && Array.isArray(raw.modules)) {
163
- return raw;
164
- }
165
- return null;
166
- }
167
- catch {
168
- return null;
169
- }
170
- }
171
- function slugify(text) {
172
- return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
173
- }
174
- function scanModulesFromArtifacts(sessionRoot, sessionId) {
175
- const artifacts = listMdFiles(sessionRoot, 4);
176
- const modules = [];
177
- const seen = new Set();
178
- for (const artifact of artifacts.slice(0, 10)) {
179
- try {
180
- const content = readFileSync(artifact, 'utf8');
181
- // Extract file paths — non-capturing groups for extensions
182
- const patterns = [
183
- /\b(src\/[^\s)`\]}"]+\.(?:tsx?|jsx?|css|less|scss|vue|svelte))\b/g,
184
- /\b(packages\/[^\s)`\]}"]+\.(?:tsx?|jsx?))\b/g
185
- ];
186
- for (const pattern of patterns) {
187
- let m;
188
- while ((m = pattern.exec(content)) !== null) {
189
- const filePath = m[1] ?? '';
190
- if (!filePath || filePath.length > 120 || filePath.length < 5)
191
- continue;
192
- const id = slugify(filePath.replace(/\.[^.]+$/, '').replace(/[\/\\]/g, '-'));
193
- if (seen.has(id))
194
- continue;
195
- seen.add(id);
196
- modules.push({ id, path: filePath });
197
- if (modules.length >= 30)
198
- break;
199
- }
200
- if (modules.length >= 30)
201
- break;
202
- }
203
- }
204
- catch {
205
- // skip unreadable
206
- }
207
- }
208
- return modules;
209
- }
210
- function scanDecisionsFromArtifacts(sessionRoot, session) {
211
- const artifacts = listMdFiles(sessionRoot, 4);
212
- const decisions = [];
213
- const date = session.createdAt ? session.createdAt.slice(0, 10) : new Date().toISOString().slice(0, 10);
214
- for (const artifact of artifacts.slice(0, 10)) {
215
- try {
216
- const content = readFileSync(artifact, 'utf8');
217
- // Look for decision markers: "- Decision: ..." or "Decision: ..." or "ADR: ..."
218
- const decRegex = /^[\s-]*(?:decision|adr|决定|决策)\s*:\s*(.+?)$/gim;
219
- let m;
220
- while ((m = decRegex.exec(content)) !== null) {
221
- const what = (m[1] ?? '').trim().slice(0, 200);
222
- if (what.length < 5)
223
- continue;
224
- const id = slugify(what.slice(0, 40));
225
- // Collect scope from surrounding context (modules mentioned within 3 lines before/after)
226
- const scope = [];
227
- const lineIdx = content.slice(0, m.index).split('\n').length;
228
- const lines = content.split('\n');
229
- for (let i = Math.max(0, lineIdx - 3); i < Math.min(lines.length, lineIdx + 3); i++) {
230
- const line = lines[i] ?? '';
231
- const pathMatch = /src\/[^\s)`\]}"]+\.(tsx?|jsx?)/.exec(line);
232
- if (pathMatch?.[0])
233
- scope.push(pathMatch[0]);
234
- }
235
- decisions.push({ id, what, scope: [...new Set(scope)].slice(0, 5), session: session.sessionId, date });
236
- if (decisions.length >= 10)
237
- break;
238
- }
239
- }
240
- catch {
241
- // skip unreadable
242
- }
243
- }
244
- return decisions;
245
- }
246
- function scanConventionsFromArtifacts(sessionRoot, session) {
247
- const artifacts = listMdFiles(sessionRoot, 4);
248
- const conventions = [];
249
- const date = session.createdAt ? session.createdAt.slice(0, 10) : new Date().toISOString().slice(0, 10);
250
- for (const artifact of artifacts.slice(0, 10)) {
251
- try {
252
- const content = readFileSync(artifact, 'utf8');
253
- const convRegex = /^[\s-]*(?:convention|约定|规范)\s*:\s*(.+?)$/gim;
254
- let m;
255
- while ((m = convRegex.exec(content)) !== null) {
256
- const rule = (m[1] ?? '').trim().slice(0, 200);
257
- if (rule.length < 5)
258
- continue;
259
- const id = slugify(rule.slice(0, 40));
260
- // Infer category from keywords
261
- let category = 'other';
262
- if (/class|function|interface|type|hook|component/i.test(rule))
263
- category = 'code-style';
264
- else if (/service|layer|package|module|shared|extract/i.test(rule))
265
- category = 'architecture';
266
- else if (/naming|命名|文件名|prefix|suffix/i.test(rule))
267
- category = 'naming';
268
- else if (/tooling|lint|format|build|test/i.test(rule))
269
- category = 'tooling';
270
- conventions.push({ id, rule, category, source: session.sessionId, date });
271
- if (conventions.length >= 10)
272
- break;
273
- }
274
- }
275
- catch {
276
- // skip unreadable
277
- }
278
- }
279
- return conventions;
280
- }
281
- function buildOntology(projectRoot) {
282
- const name = projectName(projectRoot);
283
- const existing = loadOntology(projectRoot);
284
- const onto = existing ?? emptyOntology(name);
285
- onto.updated = new Date().toISOString();
286
- onto.project = name;
287
- const metas = listSessionMetas(projectRoot);
288
- const knownSessions = new Set(metas.map((m) => m.sessionId));
289
- // Prune: remove modules/decisions/conventions from sessions that no longer exist
290
- onto.modules = onto.modules.filter((m) => m.sessions.some((s) => knownSessions.has(s)));
291
- onto.decisions = onto.decisions.filter((d) => knownSessions.has(d.session));
292
- onto.conventions = onto.conventions.filter((c) => knownSessions.has(c.source));
293
- // Merge: scan each session for new modules and decisions
294
- const moduleMap = new Map();
295
- for (const m of onto.modules)
296
- moduleMap.set(m.id, m);
297
- const decisionMap = new Map();
298
- for (const d of onto.decisions)
299
- decisionMap.set(d.id, d);
300
- for (const meta of metas) {
301
- const sessionRoot = join(projectRoot, '.peaks', meta.sessionId);
302
- // Modules
303
- const foundModules = scanModulesFromArtifacts(sessionRoot, meta.sessionId);
304
- for (const fm of foundModules) {
305
- if (moduleMap.has(fm.id)) {
306
- const existing = moduleMap.get(fm.id);
307
- if (!existing.sessions.includes(meta.sessionId)) {
308
- existing.sessions.push(meta.sessionId);
309
- }
310
- }
311
- else {
312
- moduleMap.set(fm.id, {
313
- id: fm.id,
314
- path: fm.path,
315
- sessions: [meta.sessionId]
316
- });
317
- }
318
- }
319
- // Decisions
320
- const foundDecisions = scanDecisionsFromArtifacts(sessionRoot, meta);
321
- for (const fd of foundDecisions) {
322
- if (!decisionMap.has(fd.id)) {
323
- decisionMap.set(fd.id, fd);
324
- }
325
- }
326
- // Conventions
327
- const foundConventions = scanConventionsFromArtifacts(sessionRoot, meta);
328
- const convMap = new Map();
329
- for (const c of onto.conventions)
330
- convMap.set(c.id, c);
331
- for (const fc of foundConventions) {
332
- if (!convMap.has(fc.id)) {
333
- convMap.set(fc.id, fc);
334
- }
335
- }
336
- onto.conventions = [...convMap.values()].sort((a, b) => a.date.localeCompare(b.date));
337
- }
338
- // Dedup: remove shorter paths that are substring-matches of longer paths
339
- const modules = [...moduleMap.values()];
340
- const deduped = modules.filter((m) => {
341
- return !modules.some((other) => other !== m && other.path.length > m.path.length && other.path.endsWith(m.path));
342
- });
343
- onto.modules = deduped.sort((a, b) => b.sessions.length - a.sessions.length);
344
- onto.decisions = [...decisionMap.values()].sort((a, b) => b.date.localeCompare(a.date));
345
- return onto;
346
- }
347
- export function saveOntology(projectRoot, onto) {
348
- const peaksDir = join(projectRoot, '.peaks');
349
- if (!existsSync(peaksDir))
350
- mkdirSync(peaksDir, { recursive: true });
351
- writeFileSync(ontoPath(projectRoot), JSON.stringify(onto, null, 2), 'utf8');
352
- }
353
- // Mutations for skills to call when they discover new facts
354
- export function upsertModule(projectRoot, mod) {
355
- const onto = buildOntology(projectRoot);
356
- const existing = onto.modules.find((m) => m.id === mod.id);
357
- if (existing) {
358
- if (!existing.sessions.includes(mod.session))
359
- existing.sessions.push(mod.session);
360
- if (mod.risk)
361
- existing.risk = mod.risk;
362
- if (mod.summary)
363
- existing.summary = mod.summary;
364
- }
365
- else {
366
- onto.modules.push({ ...mod, sessions: [mod.session] });
367
- }
368
- onto.updated = new Date().toISOString();
369
- saveOntology(projectRoot, onto);
370
- return onto;
371
- }
372
- export function upsertDecision(projectRoot, dec) {
373
- const onto = buildOntology(projectRoot);
374
- const idx = onto.decisions.findIndex((d) => d.id === dec.id);
375
- if (idx >= 0) {
376
- onto.decisions[idx] = dec;
377
- }
378
- else {
379
- onto.decisions.push(dec);
380
- }
381
- onto.updated = new Date().toISOString();
382
- saveOntology(projectRoot, onto);
383
- return onto;
384
- }
385
- export function upsertConvention(projectRoot, conv) {
386
- const onto = buildOntology(projectRoot);
387
- const idx = onto.conventions.findIndex((c) => c.id === conv.id);
388
- if (idx >= 0) {
389
- onto.conventions[idx] = conv;
390
- }
391
- else {
392
- onto.conventions.push(conv);
393
- }
394
- onto.updated = new Date().toISOString();
395
- saveOntology(projectRoot, onto);
396
- return onto;
397
- }
398
- // --- Context generator (unified: PROJECT.md + ontology.json) ---
399
100
  export function generateProjectContext(projectRoot) {
400
101
  const peaksDir = join(projectRoot, '.peaks');
401
102
  if (!existsSync(peaksDir)) {
@@ -428,10 +129,7 @@ export function generateProjectContext(projectRoot) {
428
129
  content = header + '\n' + sessionHistory + '\n';
429
130
  }
430
131
  writeFileSync(contextPath, content, 'utf8');
431
- // Build and save ontology alongside PROJECT.md
432
- const ontology = buildOntology(projectRoot);
433
- saveOntology(projectRoot, ontology);
434
- return { path: contextPath, content, sessionCount: listSessionMetas(projectRoot).length, ontology };
132
+ return { path: contextPath, content, sessionCount: listSessionMetas(projectRoot).length };
435
133
  }
436
134
  export function readProjectContext(projectRoot) {
437
135
  const contextPath = join(projectRoot, PROJECT_CONTEXT_FILE);
@@ -1,4 +1,4 @@
1
- export type ProjectMemoryKind = 'project' | 'rule' | 'decision' | 'reference' | 'feedback';
1
+ export type ProjectMemoryKind = 'project' | 'rule' | 'decision' | 'reference' | 'feedback' | 'convention' | 'module';
2
2
  export type ExtractedProjectMemory = {
3
3
  title: string;
4
4
  kind: ProjectMemoryKind;
@@ -59,6 +59,21 @@ export type ProjectMemoryBackupPlan = {
59
59
  export type ProjectMemoryBackupResult = ProjectMemoryBackupPlan & {
60
60
  copiedFiles: string[];
61
61
  };
62
+ export type StoredProjectMemory = {
63
+ name: string;
64
+ title: string;
65
+ kind: ProjectMemoryKind;
66
+ sourceArtifact: string | null;
67
+ body: string;
68
+ filePath: string;
69
+ };
70
+ export type ProjectMemoryReadResult = {
71
+ projectRoot: string;
72
+ memoryDir: string;
73
+ total: number;
74
+ byKind: Record<ProjectMemoryKind, StoredProjectMemory[]>;
75
+ memories: StoredProjectMemory[];
76
+ };
62
77
  type ExtractPlanOptions = {
63
78
  projectRoot: string;
64
79
  artifactPaths: string[];
@@ -76,4 +91,5 @@ export declare function createProjectMemoryBackupPlan(options: BackupPlanOptions
76
91
  export declare function executeProjectMemoryBackup(options: BackupPlanOptions): ProjectMemoryBackupResult;
77
92
  export declare function summarizeProjectMemoryExtractResult(result: ProjectMemoryExtractResult): ProjectMemoryExtractSummary;
78
93
  export declare function summarizeProjectMemoryBackupResult(result: ProjectMemoryBackupResult): ProjectMemoryBackupSummary;
94
+ export declare function readProjectMemories(projectRoot: string): ProjectMemoryReadResult;
79
95
  export {};
@@ -4,7 +4,7 @@ import { isInsidePath, isWindowsAbsolutePath, normalizePath, resolveInputPath, s
4
4
  import { containsSensitiveConfigValue, isSensitiveConfigPath } from '../config/config-service.js';
5
5
  const START_MARKER = '<!-- peaks-memory:start -->';
6
6
  const END_MARKER = '<!-- peaks-memory:end -->';
7
- const VALID_MEMORY_KINDS = new Set(['project', 'rule', 'decision', 'reference', 'feedback']);
7
+ const VALID_MEMORY_KINDS = new Set(['project', 'rule', 'decision', 'reference', 'feedback', 'convention', 'module']);
8
8
  function normalizeRoot(path) {
9
9
  return resolveInputPath(path);
10
10
  }
@@ -42,11 +42,11 @@ function assertInsideProject(path, projectRoot) {
42
42
  function assertSafeProjectMemoryDir(projectRoot) {
43
43
  const resolvedRoot = normalizeRoot(projectRoot);
44
44
  const realRoot = normalizeRealRoot(projectRoot);
45
- const claudeDir = join(resolvedRoot, '.claude');
46
- if (existsSync(claudeDir) && lstatSync(claudeDir).isSymbolicLink()) {
45
+ const peaksDir = join(resolvedRoot, '.peaks');
46
+ if (existsSync(peaksDir) && lstatSync(peaksDir).isSymbolicLink()) {
47
47
  throw new Error('Project memory directory must stay inside the project root');
48
48
  }
49
- const memoryDir = join(claudeDir, 'memory');
49
+ const memoryDir = join(peaksDir, 'memory');
50
50
  if (existsSync(memoryDir)) {
51
51
  if (lstatSync(memoryDir).isSymbolicLink()) {
52
52
  throw new Error('Project memory directory must stay inside the project root');
@@ -136,6 +136,41 @@ function renderMemoryFile(memory) {
136
136
  ''
137
137
  ].join('\n');
138
138
  }
139
+ function parseStoredMemoryFile(content, filePath) {
140
+ const normalized = content.replace(/\r\n/g, '\n');
141
+ if (!normalized.startsWith('---\n'))
142
+ return null;
143
+ const endIndex = normalized.indexOf('\n---\n', 4);
144
+ if (endIndex < 0)
145
+ return null;
146
+ const frontmatter = normalized.slice(4, endIndex);
147
+ const body = normalized.slice(endIndex + '\n---\n'.length).trim();
148
+ let name;
149
+ let description;
150
+ let kind;
151
+ let sourceArtifact;
152
+ for (const rawLine of frontmatter.split('\n')) {
153
+ const line = rawLine.trim();
154
+ if (line.startsWith('name:'))
155
+ name = line.slice('name:'.length).trim();
156
+ else if (line.startsWith('description:'))
157
+ description = line.slice('description:'.length).trim();
158
+ else if (line.startsWith('type:'))
159
+ kind = line.slice('type:'.length).trim();
160
+ else if (line.startsWith('sourceArtifact:'))
161
+ sourceArtifact = line.slice('sourceArtifact:'.length).trim();
162
+ }
163
+ if (!name || !kind || !VALID_MEMORY_KINDS.has(kind) || body.length === 0)
164
+ return null;
165
+ return {
166
+ name,
167
+ title: description ?? name,
168
+ kind: kind,
169
+ sourceArtifact: sourceArtifact && sourceArtifact !== 'undefined' ? sourceArtifact : null,
170
+ body,
171
+ filePath
172
+ };
173
+ }
139
174
  function summarizeExtractResult(result) {
140
175
  return {
141
176
  apply: result.apply,
@@ -304,3 +339,36 @@ export function summarizeProjectMemoryExtractResult(result) {
304
339
  export function summarizeProjectMemoryBackupResult(result) {
305
340
  return summarizeBackupResult(result);
306
341
  }
342
+ function emptyByKind() {
343
+ return {
344
+ project: [],
345
+ rule: [],
346
+ decision: [],
347
+ reference: [],
348
+ feedback: [],
349
+ convention: [],
350
+ module: []
351
+ };
352
+ }
353
+ export function readProjectMemories(projectRoot) {
354
+ const normalizedRoot = normalizeRoot(projectRoot);
355
+ const memoryDir = assertSafeProjectMemoryDir(normalizedRoot);
356
+ const memories = [];
357
+ for (const filePath of listMarkdownFiles(memoryDir)) {
358
+ const parsed = parseStoredMemoryFile(readFileSync(filePath, 'utf8'), filePath);
359
+ if (parsed)
360
+ memories.push(parsed);
361
+ }
362
+ memories.sort((left, right) => left.name.localeCompare(right.name));
363
+ const byKind = emptyByKind();
364
+ for (const memory of memories) {
365
+ byKind[memory.kind].push(memory);
366
+ }
367
+ return {
368
+ projectRoot: normalizedRoot,
369
+ memoryDir,
370
+ total: memories.length,
371
+ byKind,
372
+ memories
373
+ };
374
+ }
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.0.28";
1
+ export declare const CLI_VERSION = "1.1.0";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.0.28";
1
+ export const CLI_VERSION = "1.1.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.0.28",
3
+ "version": "1.1.0",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -14,13 +14,13 @@ Before any analysis or tool call, immediately run:
14
14
  ```bash
15
15
  peaks skill presence:set peaks-prd --project <repo> --mode <mode> --gate startup
16
16
  ```
17
- Read persistent project memory via CLI (structured ontology for LLM):
17
+ Read persistent project memory via CLI (durable, LLM-authored memories):
18
18
 
19
19
  ```bash
20
- peaks project ontology show --project <repo> --json
20
+ peaks project memories --project <repo> --json
21
21
  ```
22
22
 
23
- This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
23
+ This returns durable memories from `.peaks/memory` — decisions, conventions, modules, and rules captured in past sessions. Filter with `--kind <decision|convention|module|rule|reference|project>`. (`.peaks/PROJECT.md` is a human-readable session timeline only.)
24
24
  Then display: `Peaks-Cli Skill: peaks-prd | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-prd --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
25
25
 
26
26
  ## Responsibilities
@@ -14,13 +14,13 @@ Before any analysis or tool call, immediately run:
14
14
  ```bash
15
15
  peaks skill presence:set peaks-qa --project <repo> --mode <mode> --gate startup
16
16
  ```
17
- Read persistent project memory via CLI (structured ontology for LLM):
17
+ Read persistent project memory via CLI (durable, LLM-authored memories):
18
18
 
19
19
  ```bash
20
- peaks project ontology show --project <repo> --json
20
+ peaks project memories --project <repo> --json
21
21
  ```
22
22
 
23
- This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
23
+ This returns durable memories from `.peaks/memory` — decisions, conventions, modules, and rules captured in past sessions. Filter with `--kind <decision|convention|module|rule|reference|project>`. (`.peaks/PROJECT.md` is a human-readable session timeline only.)
24
24
  Then display: `Peaks-Cli Skill: peaks-qa | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-qa --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
25
25
 
26
26
  ## Responsibilities
@@ -14,13 +14,13 @@ Before any analysis or tool call, immediately run:
14
14
  ```bash
15
15
  peaks skill presence:set peaks-rd --project <repo> --mode <mode> --gate startup
16
16
  ```
17
- Read persistent project memory via CLI (structured ontology for LLM):
17
+ Read persistent project memory via CLI (durable, LLM-authored memories):
18
18
 
19
19
  ```bash
20
- peaks project ontology show --project <repo> --json
20
+ peaks project memories --project <repo> --json
21
21
  ```
22
22
 
23
- This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
23
+ This returns durable memories from `.peaks/memory` — decisions, conventions, modules, and rules captured in past sessions. Filter with `--kind <decision|convention|module|rule|reference|project>`. (`.peaks/PROJECT.md` is a human-readable session timeline only.)
24
24
  Then display: `Peaks-Cli Skill: peaks-rd | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-rd --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
25
25
 
26
26
  ## Responsibilities
@@ -14,13 +14,13 @@ Before any analysis or tool call, immediately run:
14
14
  ```bash
15
15
  peaks skill presence:set peaks-sc --project <repo> --mode <mode> --gate startup
16
16
  ```
17
- Read persistent project memory via CLI (structured ontology for LLM):
17
+ Read persistent project memory via CLI (durable, LLM-authored memories):
18
18
 
19
19
  ```bash
20
- peaks project ontology show --project <repo> --json
20
+ peaks project memories --project <repo> --json
21
21
  ```
22
22
 
23
- This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
23
+ This returns durable memories from `.peaks/memory` — decisions, conventions, modules, and rules captured in past sessions. Filter with `--kind <decision|convention|module|rule|reference|project>`. (`.peaks/PROJECT.md` is a human-readable session timeline only.)
24
24
  Then display: `Peaks-Cli Skill: peaks-sc | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-sc --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
25
25
 
26
26
  ## Responsibilities
@@ -56,7 +56,7 @@ Use gstack as a concrete source-control and release workflow reference for the `
56
56
 
57
57
  ## Project memory backup
58
58
 
59
- Project `.claude/memory` is the primary source for durable project memory. At approved checkpoints, use `peaks memory sync --project <path> --workspace <artifact-workspace> --apply` to back up the full project memory directory into the artifact repository workspace; do not treat the artifact backup as a second writable memory source.
59
+ Project `.peaks/memory` is the primary source for durable project memory. At approved checkpoints, use `peaks memory sync --project <path> --workspace <artifact-workspace> --apply` to back up the full project memory directory into the artifact repository workspace; do not treat the artifact backup as a second writable memory source.
60
60
 
61
61
  ## Commit boundary derivation
62
62
 
@@ -78,22 +78,23 @@ Then display the compact status header: `Peaks-Cli Skill: peaks-solo | Peaks-Cli
78
78
 
79
79
  Update with `peaks skill presence:set peaks-solo --project <repo> --mode <mode> --gate <gate>` when gates change. The presence file persists across the full workflow lifecycle — do NOT clear it at workflow end.
80
80
 
81
- ### Peaks-Cli Step 2.3: Load project memory (structured ontology for LLM)
81
+ ### Peaks-Cli Step 2.3: Load project memory (durable, LLM-authored memories)
82
82
 
83
- Before planning any work, read the project's persistent memory — structured data that survives across sessions:
83
+ Before planning any work, read the project's persistent memory — durable memories that survive across sessions:
84
84
 
85
85
  ```bash
86
- peaks project ontology show --project <repo> --json
86
+ peaks project memories --project <repo> --json
87
87
  ```
88
88
 
89
- This returns `.peaks/ontology.json` containing:
90
- - **modules** — code areas touched, with risk levels and which sessions modified them
91
- - **decisions** — architectural choices, why they were made, what modules they affect
92
- - **conventions** — discovered project patterns (code style, naming, tooling)
89
+ This returns durable memories from `.peaks/memory`, grouped by kind:
90
+ - **module** — code areas touched, with risk and rationale captured by past sessions
91
+ - **decision** — architectural choices, why they were made, what they affect
92
+ - **convention** — discovered project patterns (code style, naming, tooling)
93
+ - **rule** / **reference** / **project** — standing constraints, external pointers, and project context
93
94
 
94
- Use this to understand what exists, what was decided, and what to avoid re-litigating. The ontology is auto-updated on `peaks skill presence:clear`.
95
+ Filter with `--kind <decision|convention|module|rule|reference|project>` when you only need one slice. Use this to understand what exists, what was decided, and what to avoid re-litigating. Memories are LLM-authored at approved checkpoints via `peaks memory extract`.
95
96
 
96
- `.peaks/PROJECT.md` is a human-readable timeline only — do NOT use it for LLM context.
97
+ `.peaks/PROJECT.md` is a human-readable session timeline only — do NOT use it for LLM context.
97
98
 
98
99
  ### Peaks-Cli Step 2.5: Set session title
99
100
 
@@ -14,13 +14,13 @@ Before any analysis or tool call, immediately run:
14
14
  ```bash
15
15
  peaks skill presence:set peaks-txt --project <repo> --mode <mode> --gate startup
16
16
  ```
17
- Read persistent project memory via CLI (structured ontology for LLM):
17
+ Read persistent project memory via CLI (durable, LLM-authored memories):
18
18
 
19
19
  ```bash
20
- peaks project ontology show --project <repo> --json
20
+ peaks project memories --project <repo> --json
21
21
  ```
22
22
 
23
- This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
23
+ This returns durable memories from `.peaks/memory` — decisions, conventions, modules, and rules captured in past sessions. Filter with `--kind <decision|convention|module|rule|reference|project>`. (`.peaks/PROJECT.md` is a human-readable session timeline only.)
24
24
  Then display: `Peaks-Cli Skill: peaks-txt | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-txt --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
25
25
 
26
26
  ## Responsibilities
@@ -96,7 +96,7 @@ Each entry should include:
96
96
  - why it exists;
97
97
  - affected skills;
98
98
  - how future PRD/RD/UI/QA/SC/Solo workflows should apply it;
99
- - whether it is stable enough for `.claude/memory` extraction.
99
+ - whether it is stable enough for `.peaks/memory` extraction.
100
100
 
101
101
  ## Project memory guidance
102
102
 
@@ -111,7 +111,7 @@ Stable memory body.
111
111
  <!-- peaks-memory:end -->
112
112
  ```
113
113
 
114
- The primary write target is the target project's `.claude/memory`. Use `peaks memory extract --project <path> --artifact <artifact> --apply` only after the user or active profile allows durable project memory writes.
114
+ The primary write target is the target project's `.peaks/memory`. Use `peaks memory extract --project <path> --artifact <artifact> --apply` only after the user or active profile allows durable project memory writes.
115
115
 
116
116
  ## Matt Pocock skills integration
117
117
 
@@ -14,13 +14,13 @@ Before any analysis or tool call, immediately run:
14
14
  ```bash
15
15
  peaks skill presence:set peaks-ui --project <repo> --mode <mode> --gate startup
16
16
  ```
17
- Read persistent project memory via CLI (structured ontology for LLM):
17
+ Read persistent project memory via CLI (durable, LLM-authored memories):
18
18
 
19
19
  ```bash
20
- peaks project ontology show --project <repo> --json
20
+ peaks project memories --project <repo> --json
21
21
  ```
22
22
 
23
- This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
23
+ This returns durable memories from `.peaks/memory` — decisions, conventions, modules, and rules captured in past sessions. Filter with `--kind <decision|convention|module|rule|reference|project>`. (`.peaks/PROJECT.md` is a human-readable session timeline only.)
24
24
  Then display: `Peaks-Cli Skill: peaks-ui | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-ui --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
25
25
 
26
26
  ## Responsibilities