peaks-cli 1.0.24 → 1.0.25

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.
@@ -8,6 +8,7 @@ import { runDoctor } from '../../services/doctor/doctor-service.js';
8
8
  import { listSkills } from '../../services/skills/skill-registry.js';
9
9
  import { inspectSkillRunbook } from '../../services/skills/skill-runbook-service.js';
10
10
  import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode, touchSkillHeartbeat } from '../../services/skills/skill-presence-service.js';
11
+ import { ensureSession, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas } from '../../services/session/session-manager.js';
11
12
  import { fail, ok } from '../../shared/result.js';
12
13
  import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isArtifactProvider, isArtifactSetupStep, printResult } from '../cli-helpers.js';
13
14
  export function registerCoreAndArtifactCommands(program, io) {
@@ -71,13 +72,21 @@ export function registerCoreAndArtifactCommands(program, io) {
71
72
  .command('presence:set <name>')
72
73
  .description('Set the currently active Peaks skill for session-wide visibility')
73
74
  .option('--mode <mode>', 'execution mode')
74
- .option('--gate <gate>', 'current gate')).action((name, options) => {
75
+ .option('--gate <gate>', 'current gate')).action(async (name, options) => {
75
76
  if (options.mode !== undefined && !isSkillPresenceMode(options.mode)) {
76
77
  printResult(io, fail('skill.presence:set', 'INVALID_MODE', `Invalid mode: ${options.mode} (expected one of: full-auto, assisted, swarm, strict)`, { name, mode: options.mode }, ['Use a valid mode: full-auto, assisted, swarm, or strict']), options.json);
77
78
  process.exitCode = 1;
78
79
  return;
79
80
  }
80
81
  const presence = setSkillPresence(name, options.mode, options.gate);
82
+ // Also update session metadata so session dirs self-document
83
+ const projectRoot = process.cwd();
84
+ const sessionId = await ensureSession(projectRoot);
85
+ setSessionMeta(projectRoot, sessionId, {
86
+ skill: name,
87
+ ...(options.mode ? { mode: options.mode } : {}),
88
+ ...(options.gate ? { gate: options.gate } : {})
89
+ });
81
90
  printResult(io, ok('skill.presence:set', { active: true, ...presence }), options.json);
82
91
  });
83
92
  addJsonOption(skill
@@ -116,6 +125,39 @@ export function registerCoreAndArtifactCommands(program, io) {
116
125
  lastHeartbeat: updated.lastHeartbeat
117
126
  }), options.json);
118
127
  });
128
+ const session = program.command('session').description('Manage Peaks session directories');
129
+ addJsonOption(session
130
+ .command('list')
131
+ .description('List all session directories with titles and metadata')).action((options) => {
132
+ const projectRoot = process.cwd();
133
+ const metas = listSessionMetas(projectRoot);
134
+ printResult(io, ok('session.list', { sessions: metas, total: metas.length }), options.json);
135
+ });
136
+ addJsonOption(session
137
+ .command('info <sessionId>')
138
+ .description('Show full metadata for a session directory')).action((sessionId, options) => {
139
+ const projectRoot = process.cwd();
140
+ const meta = getSessionMeta(projectRoot, sessionId);
141
+ if (meta === null) {
142
+ printResult(io, fail('session.info', 'SESSION_NOT_FOUND', `Session "${sessionId}" not found or has no metadata`, { sessionId }, ['Use `peaks session list` to see available sessions']), options.json);
143
+ process.exitCode = 1;
144
+ return;
145
+ }
146
+ printResult(io, ok('session.info', meta), options.json);
147
+ });
148
+ addJsonOption(session
149
+ .command('title <sessionId> <title>')
150
+ .description('Set a human-readable title for a session directory')).action((sessionId, title, options) => {
151
+ const projectRoot = process.cwd();
152
+ try {
153
+ const meta = setSessionTitle(projectRoot, sessionId, title);
154
+ printResult(io, ok('session.title', meta), options.json);
155
+ }
156
+ catch (error) {
157
+ printResult(io, fail('session.title', 'SESSION_TITLE_FAILED', getErrorMessage(error), { sessionId }, ['Verify the sessionId exists under .peaks/']), options.json);
158
+ process.exitCode = 1;
159
+ }
160
+ });
119
161
  const profile = program.command('profile').description('Manage runtime profiles');
120
162
  addJsonOption(profile.command('list').description('List available profiles')).action((options) => {
121
163
  printResult(io, ok('profile.list', { profiles: listProfiles() }), options.json);
@@ -1 +1 @@
1
- export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getProjectScanPath, hasProjectScan, type SessionInfo } from './session-manager.js';
1
+ export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, type SessionInfo, type SessionMeta } from './session-manager.js';
@@ -1 +1 @@
1
- export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getProjectScanPath, hasProjectScan } from './session-manager.js';
1
+ export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan } from './session-manager.js';
@@ -10,6 +10,35 @@ export type SessionInfo = {
10
10
  createdAt: string;
11
11
  projectRoot: string;
12
12
  };
13
+ export type SessionMeta = {
14
+ sessionId: string;
15
+ title?: string;
16
+ skill?: string;
17
+ mode?: string;
18
+ gate?: string;
19
+ createdAt: string;
20
+ lastActivity?: string;
21
+ projectRoot: string;
22
+ };
23
+ /**
24
+ * Read metadata for a specific session directory.
25
+ * Returns null if the session directory or its session.json does not exist.
26
+ */
27
+ export declare function getSessionMeta(projectRoot: string, sessionId: string): SessionMeta | null;
28
+ /**
29
+ * Write or update metadata for a session. Fields besides sessionId and createdAt
30
+ * are merged on top of the current meta (partial update).
31
+ */
32
+ export declare function setSessionMeta(projectRoot: string, sessionId: string, partial: Partial<Omit<SessionMeta, 'sessionId' | 'createdAt' | 'projectRoot'>>): SessionMeta;
33
+ /**
34
+ * Set the display title for a session directory.
35
+ */
36
+ export declare function setSessionTitle(projectRoot: string, sessionId: string, title: string): SessionMeta;
37
+ /**
38
+ * List all session directories under .peaks with their metadata.
39
+ * Returns sessions sorted by sessionId descending (most recent first).
40
+ */
41
+ export declare function listSessionMetas(projectRoot: string): SessionMeta[];
13
42
  /**
14
43
  * Get or create the current session for a project.
15
44
  * If a valid session already exists, returns it.
@@ -5,11 +5,12 @@
5
5
  * Sessions are automatically created when any skill is invoked.
6
6
  * Each session gets a unique directory under .peaks/ with incrementing numbered files.
7
7
  */
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
10
  import { randomBytes } from 'node:crypto';
11
11
  import { initWorkspace } from '../workspace/workspace-service.js';
12
12
  const SESSION_FILE = '.session.json';
13
+ const META_FILE = 'session.json';
13
14
  /**
14
15
  * Generate a new session ID.
15
16
  * Format: YYYY-MM-DD-session-<6位hex>
@@ -60,6 +61,86 @@ function writeSessionFile(projectRoot, info) {
60
61
  }
61
62
  writeFileSync(sessionFile, JSON.stringify(info, null, 2), 'utf8');
62
63
  }
64
+ function getMetaFilePath(projectRoot, sessionId) {
65
+ return join(projectRoot, '.peaks', sessionId, META_FILE);
66
+ }
67
+ function readSessionMeta(projectRoot, sessionId) {
68
+ const metaPath = getMetaFilePath(projectRoot, sessionId);
69
+ if (!existsSync(metaPath))
70
+ return null;
71
+ try {
72
+ const raw = readFileSync(metaPath, 'utf8');
73
+ const parsed = JSON.parse(raw);
74
+ if (typeof parsed?.sessionId !== 'string' || parsed.sessionId.length === 0) {
75
+ return null;
76
+ }
77
+ return parsed;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ function writeSessionMeta(projectRoot, sessionId, meta) {
84
+ const metaPath = getMetaFilePath(projectRoot, sessionId);
85
+ const metaDir = join(projectRoot, '.peaks', sessionId);
86
+ if (!existsSync(metaDir)) {
87
+ mkdirSync(metaDir, { recursive: true });
88
+ }
89
+ writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf8');
90
+ }
91
+ /**
92
+ * Read metadata for a specific session directory.
93
+ * Returns null if the session directory or its session.json does not exist.
94
+ */
95
+ export function getSessionMeta(projectRoot, sessionId) {
96
+ return readSessionMeta(projectRoot, sessionId);
97
+ }
98
+ /**
99
+ * Write or update metadata for a session. Fields besides sessionId and createdAt
100
+ * are merged on top of the current meta (partial update).
101
+ */
102
+ export function setSessionMeta(projectRoot, sessionId, partial) {
103
+ const existing = readSessionMeta(projectRoot, sessionId);
104
+ const now = new Date().toISOString();
105
+ const meta = existing
106
+ ? { ...existing, ...partial, lastActivity: now }
107
+ : {
108
+ sessionId,
109
+ projectRoot,
110
+ createdAt: now,
111
+ ...partial,
112
+ lastActivity: now
113
+ };
114
+ writeSessionMeta(projectRoot, sessionId, meta);
115
+ return meta;
116
+ }
117
+ /**
118
+ * Set the display title for a session directory.
119
+ */
120
+ export function setSessionTitle(projectRoot, sessionId, title) {
121
+ return setSessionMeta(projectRoot, sessionId, { title });
122
+ }
123
+ /**
124
+ * List all session directories under .peaks with their metadata.
125
+ * Returns sessions sorted by sessionId descending (most recent first).
126
+ */
127
+ export function listSessionMetas(projectRoot) {
128
+ const peaksRoot = join(projectRoot, '.peaks');
129
+ if (!existsSync(peaksRoot))
130
+ return [];
131
+ const entries = readdirSync(peaksRoot, { withFileTypes: true });
132
+ return entries
133
+ .filter((entry) => entry.isDirectory() && /^\d{4}-\d{2}-\d{2}-session-[a-f0-9]+$/.test(entry.name))
134
+ .map((entry) => {
135
+ const meta = readSessionMeta(projectRoot, entry.name);
136
+ return meta ?? {
137
+ sessionId: entry.name,
138
+ projectRoot,
139
+ createdAt: ''
140
+ };
141
+ })
142
+ .sort((a, b) => b.sessionId.localeCompare(a.sessionId));
143
+ }
63
144
  /**
64
145
  * Get or create the current session for a project.
65
146
  * If a valid session already exists, returns it.
@@ -74,13 +155,20 @@ export async function ensureSession(projectRoot) {
74
155
  return existing.sessionId;
75
156
  }
76
157
  const sessionId = generateSessionId();
158
+ const now = new Date().toISOString();
77
159
  const info = {
78
160
  sessionId,
79
- createdAt: new Date().toISOString(),
161
+ createdAt: now,
80
162
  projectRoot
81
163
  };
82
164
  writeSessionFile(projectRoot, info);
83
165
  await initWorkspace({ projectRoot, sessionId });
166
+ // Initialize session metadata inside the session directory
167
+ writeSessionMeta(projectRoot, sessionId, {
168
+ sessionId,
169
+ projectRoot,
170
+ createdAt: now
171
+ });
84
172
  return sessionId;
85
173
  }
86
174
  /**
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.0.24";
1
+ export declare const CLI_VERSION = "1.0.25";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.0.24";
1
+ export const CLI_VERSION = "1.0.25";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.0.24",
3
+ "version": "1.0.25",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -78,6 +78,16 @@ 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 --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.5: Set session title
82
+
83
+ Extract a short (8-20 Chinese characters, or 4-10 English words) descriptive title from the user's first request. The title should capture the core task — e.g. "修复登录页OAuth回调异常", "添加暗色模式开关", "搭建项目基础架构". Then run:
84
+
85
+ ```bash
86
+ peaks session title $(cat .peaks/.session.json | python3 -c "import sys,json; print(json.load(sys.stdin)['sessionId'])") "<title>"
87
+ ```
88
+
89
+ If the session directory already has a title (check via `peaks session list --json`), skip this step — the title is already set.
90
+
81
91
  ## Boundaries
82
92
 
83
93
  Peaks-Cli Solo may: