project-mgr 0.1.6 → 0.1.7

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.7 - 2026-08-07
4
+
5
+ ### Added
6
+
7
+ - Added project metadata management (`displayName`, `description`, `notes`) and fuzzy search via `projects` / `list --query`.
8
+ - Added explicit field clearing for project metadata via `--clear-description` and `--clear-notes` flags in `pmgr set-project`.
9
+
3
10
  ## 0.1.6 - 2026-08-03
4
11
 
5
12
  ### Fixed
package/README.md CHANGED
@@ -176,6 +176,8 @@ pnpm start at \
176
176
 
177
177
  Optional arguments:
178
178
 
179
+ - `--description`: Project introduction or description (updates project metadata).
180
+ - `--notes`: Project notes/remarks (updates project metadata).
179
181
  - `--progress`: Work already completed.
180
182
  - `--blocker`, `--next`, `--decision`, and `--tag`: May be specified repeatedly.
181
183
  - `--at`: When the status actually occurred. It must be an ISO 8601 timestamp with a timezone; defaults to the current time.
@@ -183,6 +185,29 @@ Optional arguments:
183
185
  - `--external-ref`: A globally idempotent key. A duplicate write to the same project returns the existing record; use by another project is an error.
184
186
  - `--json`: Emit stable JSON.
185
187
 
188
+ ### Project Metadata & Fuzzy Search
189
+
190
+ Set or update a project's display name, description, and notes with `set-project`, or clear metadata using `--clear-description` / `--clear-notes`:
191
+
192
+ ```bash
193
+ pnpm start set-project \
194
+ --project project-mgr \
195
+ --display-name "Project Manager" \
196
+ --description "Local-first append-only project status ledger" \
197
+ --notes "Core CLI and Agent Skill are published"
198
+
199
+ # Clear description or notes
200
+ pnpm start set-project --project project-mgr --clear-description --clear-notes
201
+ ```
202
+
203
+ List projects or search projects using fuzzy matching across slug, display name, description, notes, latest status summary, and tags with `projects` (or `list`):
204
+
205
+ ```bash
206
+ pnpm start projects
207
+ pnpm start list --query "ledger"
208
+ pnpm start projects --query "mvp" --json
209
+ ```
210
+
186
211
  ## Bulk import
187
212
 
188
213
  `import` accepts a JSON object, a JSON array, or a JSONL file containing one object per line. Every record is validated before the whole batch is written in one SQLite transaction; any failure rolls back the entire batch.
@@ -213,14 +238,16 @@ Unknown fields, unknown CLI options, extra positional arguments, invalid statuse
213
238
 
214
239
  ## Query and export
215
240
 
216
- | Command | Purpose | Key options |
217
- | --------- | ----------------------------------------------------- | ------------------------------------------------------------------ |
218
- | `current` | Current status for all projects or one project | `--project`, `--json` |
219
- | `history` | History for all projects or one project, newest first | `--project`, `--since`, `--until`, `--limit`, `--offset`, `--json` |
220
- | `at` | The latest status at or before a specified time | `--project`, `--at`, `--json` |
221
- | `export` | Export all records or one project's records | `--project`, `--format json\|jsonl` |
241
+ | Command | Purpose | Key options |
242
+ | ------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
243
+ | `projects` | List all projects or fuzzy search by query | `--query`, `--json` |
244
+ | `set-project` | Set/update project display name, description, & notes | `--project`, `--display-name`, `--description`, `--notes`, `--clear-description`, `--clear-notes`, `--json` |
245
+ | `current` | Current status for all projects or one project | `--project`, `--json` |
246
+ | `history` | History for all projects or one project, newest first | `--project`, `--since`, `--until`, `--limit`, `--offset`, `--json` |
247
+ | `at` | The latest status at or before a specified time | `--project`, `--at`, `--json` |
248
+ | `export` | Export all records or one project's records | `--project`, `--format json\|jsonl` |
222
249
 
223
- All six actions can also use the compatible `status <action>` form, for example `pmgr status export`. Human-readable multi-record results list project, status, occurrence time, and summary, or `No entries found.` when empty. `add`, `import`, `current`, `history`, and `at` support `--json`; `export` uses `--format` to select its format.
250
+ All actions can also use the compatible `status <action>` form, for example `pmgr status projects`. Human-readable multi-record results list projects and their latest status, or `No entries found.` / `No projects found.` when empty. `add`, `import`, `current`, `projects`, `list`, `set-project`, `history`, and `at` support `--json`; `export` uses `--format` to select its format.
224
251
 
225
252
  Exports omit the database-internal `id` and `recordedAt` fields and can be imported directly. Records are exported from oldest to newest so that restoring them preserves the insertion order of records with the same timestamp:
226
253
 
package/README_CN.md CHANGED
@@ -176,6 +176,8 @@ pnpm start at \
176
176
 
177
177
  可选参数:
178
178
 
179
+ - `--description`:项目简介/描述(更新项目元数据)。
180
+ - `--notes`:项目备注(更新项目元数据)。
179
181
  - `--progress`:已经完成的工作。
180
182
  - `--blocker`、`--next`、`--decision`、`--tag`:可以重复传入。
181
183
  - `--at`:状态实际发生时间,必须是带时区的 ISO 8601 时间;缺省为当前时间。
@@ -183,6 +185,29 @@ pnpm start at \
183
185
  - `--external-ref`:全局幂等键。重复写入同一项目时返回原记录;若被其他项目占用则报错。
184
186
  - `--json`:输出稳定 JSON。
185
187
 
188
+ ## 项目元数据与模糊查询
189
+
190
+ 可通过 `set-project` 设置或更新项目的名称、简介与备注,或使用 `--clear-description` / `--clear-notes` 清空:
191
+
192
+ ```bash
193
+ pnpm start set-project \
194
+ --project project-mgr \
195
+ --display-name "Project Manager" \
196
+ --description "本地优先、只追加的项目状态账本" \
197
+ --notes "核心 CLI 与 Agent Skill 已上线"
198
+
199
+ # 清空简介或备注
200
+ pnpm start set-project --project project-mgr --clear-description --clear-notes
201
+ ```
202
+
203
+ 可通过 `projects`(或 `list`)列出所有项目或使用 `--query` 进行模糊匹配搜索(匹配项目名称 slug、显示名称、简介、备注、最新状态摘要及标签):
204
+
205
+ ```bash
206
+ pnpm start projects
207
+ pnpm start list --query "账本"
208
+ pnpm start projects --query "mvp" --json
209
+ ```
210
+
186
211
  ## 批量导入
187
212
 
188
213
  `import` 接受一个 JSON 对象、JSON 数组,或每行一个对象的 JSONL 文件。所有记录先校验,再在同一个 SQLite 事务中写入;任一记录失败时整批回滚。
@@ -213,14 +238,16 @@ pnpm start import --file statuses.jsonl --json
213
238
 
214
239
  ## 查询与导出
215
240
 
216
- | 命令 | 用途 | 主要选项 |
217
- | --------- | ---------------------------------- | ------------------------------------------------------------------ |
218
- | `current` | 所有项目或单项目的当前状态 | `--project`、`--json` |
219
- | `history` | 全部或单个项目的历史,最新记录在前 | `--project`、`--since`、`--until`、`--limit`、`--offset`、`--json` |
220
- | `at` | 指定时间点及之前的最后状态 | `--project`、`--at`、`--json` |
221
- | `export` | 导出全部或单项目记录 | `--project`、`--format json\|jsonl` |
241
+ | 命令 | 用途 | 主要选项 |
242
+ | ------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
243
+ | `projects` | 列出所有项目或按关键词模糊匹配搜索项目 | `--query`、`--json` |
244
+ | `set-project` | 设置/更新/清空项目的显示名称、简介与备注 | `--project`、`--display-name`、`--description`、`--notes`、`--clear-description`、`--clear-notes`、`--json` |
245
+ | `current` | 所有项目或单项目的当前状态 | `--project`、`--json` |
246
+ | `history` | 全部或单个项目的历史,最新记录在前 | `--project`、`--since`、`--until`、`--limit`、`--offset`、`--json` |
247
+ | `at` | 指定时间点及之前的最后状态 | `--project`、`--at`、`--json` |
248
+ | `export` | 导出全部或单项目记录 | `--project`、`--format json\|jsonl` |
222
249
 
223
- 全部六个 action 仍可使用 `status <action>` 的兼容形式,例如 `pmgr status export`。人类可读的多条结果按“项目、状态、发生时间、摘要”输出;为空时输出 `No entries found.`。`add`、`import`、`current`、`history`、`at` 支持 `--json`;`export` 由 `--format` 控制格式。
250
+ 全部 action 仍可使用 `status <action>` 的兼容形式,例如 `pmgr status projects`。人类可读的多条结果按项目及其最新状态输出;为空时输出 `No entries found.` 或 `No projects found.`。`add`、`import`、`current`、`projects`、`list`、`set-project`、`history`、`at` 支持 `--json`;`export` 由 `--format` 控制格式。
224
251
 
225
252
  导出内容不包含数据库内部的 `id` 和 `recordedAt`,可以直接重新导入。记录按时间从旧到新导出,从而在恢复时保留相同时间戳记录的先后关系:
226
253
 
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readFileSync, realpathSync } from "node:fs";
3
3
  import { basename } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import { addStatus, at, current, history, importStatuses, openLedger, parseTime, toPortableStatusEntry, ValidationError, } from "./index.js";
5
+ import { addStatus, at, current, history, importStatuses, listProjects, openLedger, parseTime, setProjectMetadata, toPortableStatusEntry, ValidationError, } from "./index.js";
6
6
  const repeats = new Set(["next", "blocker", "decision", "tag"]);
7
7
  function parse(argv) {
8
8
  const command = [];
@@ -14,7 +14,9 @@ function parse(argv) {
14
14
  continue;
15
15
  }
16
16
  const key = token.slice(2);
17
- if (key === "json") {
17
+ if (key === "json" ||
18
+ key === "clear-description" ||
19
+ key === "clear-notes") {
18
20
  if (flags[key] !== undefined)
19
21
  throw new ValidationError(`--${key} may only be supplied once`);
20
22
  flags[key] = true;
@@ -39,6 +41,8 @@ const allowedFlags = new Map([
39
41
  "project",
40
42
  "status",
41
43
  "summary",
44
+ "description",
45
+ "notes",
42
46
  "progress",
43
47
  "blocker",
44
48
  "next",
@@ -52,6 +56,20 @@ const allowedFlags = new Map([
52
56
  ],
53
57
  ["import", new Set(["file", "json"])],
54
58
  ["current", new Set(["project", "json"])],
59
+ ["projects", new Set(["query", "json"])],
60
+ ["list", new Set(["query", "json"])],
61
+ [
62
+ "set-project",
63
+ new Set([
64
+ "project",
65
+ "display-name",
66
+ "description",
67
+ "notes",
68
+ "clear-description",
69
+ "clear-notes",
70
+ "json",
71
+ ]),
72
+ ],
55
73
  [
56
74
  "history",
57
75
  new Set(["project", "since", "until", "limit", "offset", "json"]),
@@ -64,7 +82,7 @@ function validateInvocation(command, flags) {
64
82
  const action = command[statusCommand ? 1 : 0];
65
83
  const allowed = typeof action === "string" ? allowedFlags.get(action) : undefined;
66
84
  if (typeof action !== "string" || !allowed)
67
- throw new ValidationError("usage: pmgr <add|import|current|history|at|export> (or: pmgr status <action>)");
85
+ throw new ValidationError("usage: pmgr <add|import|current|projects|list|set-project|history|at|export> (or: pmgr status <action>)");
68
86
  const positional = command.slice(statusCommand ? 2 : 1);
69
87
  if (action === "import") {
70
88
  if (positional.length > 1)
@@ -110,6 +128,28 @@ function output(value, json, write) {
110
128
  else
111
129
  write(`${JSON.stringify(value, null, 2)}\n`);
112
130
  }
131
+ function formatProjectOverview(item) {
132
+ const lines = [`${item.project}\t${item.displayName}`];
133
+ if (item.description)
134
+ lines.push(` Description: ${item.description}`);
135
+ if (item.notes)
136
+ lines.push(` Notes: ${item.notes}`);
137
+ if (item.latestStatus) {
138
+ lines.push(` Latest Status: ${item.latestStatus.status}\t${item.latestStatus.occurredAt}\t${item.latestStatus.summary}`);
139
+ }
140
+ else {
141
+ lines.push(` Latest Status: (none)`);
142
+ }
143
+ return lines.join("\n");
144
+ }
145
+ function outputProjects(items, json, write) {
146
+ if (json)
147
+ write(`${JSON.stringify(items)}\n`);
148
+ else if (items.length === 0)
149
+ write("No projects found.\n");
150
+ else
151
+ write(`${items.map(formatProjectOverview).join("\n")}\n`);
152
+ }
113
153
  function imported(file) {
114
154
  if (!file)
115
155
  throw new ValidationError("import file is required");
@@ -141,9 +181,15 @@ export function run(argv, write = process.stdout.write.bind(process.stdout)) {
141
181
  const json = flags.json === true;
142
182
  try {
143
183
  switch (action) {
144
- case "add":
184
+ case "add": {
185
+ const project = stringFlag(flags, "project", true);
186
+ const description = stringFlag(flags, "description");
187
+ const notes = stringFlag(flags, "notes");
188
+ if (description !== undefined || notes !== undefined) {
189
+ setProjectMetadata(db, { project, description, notes });
190
+ }
145
191
  output(addStatus(db, {
146
- project: stringFlag(flags, "project", true),
192
+ project,
147
193
  status: stringFlag(flags, "status", true),
148
194
  summary: stringFlag(flags, "summary", true),
149
195
  progress: stringFlag(flags, "progress"),
@@ -156,6 +202,7 @@ export function run(argv, write = process.stdout.write.bind(process.stdout)) {
156
202
  externalRef: stringFlag(flags, "external-ref"),
157
203
  }).entry, json, write);
158
204
  break;
205
+ }
159
206
  case "import":
160
207
  output(importStatuses(db, imported(stringFlag(flags, "file") ??
161
208
  command[command[0] === "status" ? 2 : 1] ??
@@ -164,6 +211,39 @@ export function run(argv, write = process.stdout.write.bind(process.stdout)) {
164
211
  case "current":
165
212
  output(current(db, stringFlag(flags, "project")), json, write);
166
213
  break;
214
+ case "projects":
215
+ case "list":
216
+ outputProjects(listProjects(db, { query: stringFlag(flags, "query") }), json, write);
217
+ break;
218
+ case "set-project": {
219
+ const project = stringFlag(flags, "project", true);
220
+ const displayName = stringFlag(flags, "display-name");
221
+ let description = undefined;
222
+ if (flags["clear-description"] === true) {
223
+ if (flags.description !== undefined)
224
+ throw new ValidationError("cannot specify both --description and --clear-description");
225
+ description = null;
226
+ }
227
+ else if (flags.description !== undefined) {
228
+ description = stringFlag(flags, "description");
229
+ }
230
+ let notes = undefined;
231
+ if (flags["clear-notes"] === true) {
232
+ if (flags.notes !== undefined)
233
+ throw new ValidationError("cannot specify both --notes and --clear-notes");
234
+ notes = null;
235
+ }
236
+ else if (flags.notes !== undefined) {
237
+ notes = stringFlag(flags, "notes");
238
+ }
239
+ output(setProjectMetadata(db, {
240
+ project,
241
+ displayName,
242
+ description,
243
+ notes,
244
+ }), json, write);
245
+ break;
246
+ }
167
247
  case "history":
168
248
  output(history(db, stringFlag(flags, "project"), {
169
249
  since: stringFlag(flags, "since") &&
package/dist/db.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import Database from "better-sqlite3";
2
- import { type Source, type StatusEntry } from "./domain.js";
2
+ import { type ProjectMetadata, type ProjectOverview, type Source, type StatusEntry } from "./domain.js";
3
3
  export type Ledger = Database.Database;
4
4
  export type InsertResult = {
5
5
  entry: StatusEntry;
@@ -13,6 +13,10 @@ export type HistoryOptions = {
13
13
  };
14
14
  export declare function databasePath(): string;
15
15
  export declare function openLedger(path?: string): Ledger;
16
+ export declare function setProjectMetadata(db: Ledger, value: unknown): ProjectMetadata;
17
+ export declare function listProjects(db: Ledger, options?: {
18
+ query?: string;
19
+ }): ProjectOverview[];
16
20
  export declare function addStatus(db: Ledger, value: unknown, defaultSource?: Source): InsertResult;
17
21
  export declare function importStatuses(db: Ledger, values: unknown[]): InsertResult[];
18
22
  export declare function current(db: Ledger, project?: string): StatusEntry[];
package/dist/db.js CHANGED
@@ -2,8 +2,8 @@ import Database from "better-sqlite3";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, resolve } from "node:path";
5
- import { validateStatusEntry, ValidationError, } from "./domain.js";
6
- const migration = `
5
+ import { validateProjectMetadataInput, validateStatusEntry, ValidationError, } from "./domain.js";
6
+ const migration1 = `
7
7
  CREATE TABLE IF NOT EXISTS projects (
8
8
  id INTEGER PRIMARY KEY,
9
9
  slug TEXT NOT NULL UNIQUE,
@@ -27,6 +27,10 @@ CREATE TABLE IF NOT EXISTS status_entries (
27
27
  );
28
28
  CREATE INDEX IF NOT EXISTS idx_status_entries_project_occurred ON status_entries(project_id, occurred_at DESC, id DESC);
29
29
  `;
30
+ const migration2 = `
31
+ ALTER TABLE projects ADD COLUMN description TEXT;
32
+ ALTER TABLE projects ADD COLUMN notes TEXT;
33
+ `;
30
34
  export function databasePath() {
31
35
  return (process.env.PMGR_DB_PATH ||
32
36
  resolve(homedir(), ".local", "share", "project-mgr", "project-mgr.sqlite"));
@@ -39,9 +43,13 @@ export function openLedger(path = databasePath()) {
39
43
  db.exec("CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY)");
40
44
  db.transaction(() => {
41
45
  if (!db.prepare("SELECT 1 FROM schema_migrations WHERE version = 1").get()) {
42
- db.exec(migration);
46
+ db.exec(migration1);
43
47
  db.prepare("INSERT INTO schema_migrations(version) VALUES (1)").run();
44
48
  }
49
+ if (!db.prepare("SELECT 1 FROM schema_migrations WHERE version = 2").get()) {
50
+ db.exec(migration2);
51
+ db.prepare("INSERT INTO schema_migrations(version) VALUES (2)").run();
52
+ }
45
53
  }).immediate();
46
54
  return db;
47
55
  }
@@ -67,6 +75,97 @@ function one(db, sql, ...params) {
67
75
  const row = db.prepare(sql).get(...params);
68
76
  return row && entryFromRow(row);
69
77
  }
78
+ function upsertProjectMetadata(db, input) {
79
+ const existing = db
80
+ .prepare("SELECT 1 FROM projects WHERE slug = ?")
81
+ .get(input.project);
82
+ if (!existing) {
83
+ const now = new Date().toISOString();
84
+ db.prepare("INSERT INTO projects(slug, display_name, description, notes, created_at) VALUES (?, ?, ?, ?, ?)").run(input.project, input.displayName ?? input.project, input.description ?? null, input.notes ?? null, now);
85
+ }
86
+ else {
87
+ const updates = [];
88
+ const params = [];
89
+ if (input.displayName !== undefined) {
90
+ updates.push("display_name = ?");
91
+ params.push(input.displayName);
92
+ }
93
+ if (input.description !== undefined) {
94
+ updates.push("description = ?");
95
+ params.push(input.description);
96
+ }
97
+ if (input.notes !== undefined) {
98
+ updates.push("notes = ?");
99
+ params.push(input.notes);
100
+ }
101
+ if (updates.length > 0) {
102
+ params.push(input.project);
103
+ db.prepare(`UPDATE projects SET ${updates.join(", ")} WHERE slug = ?`).run(...params);
104
+ }
105
+ }
106
+ }
107
+ export function setProjectMetadata(db, value) {
108
+ const input = validateProjectMetadataInput(value);
109
+ return db.transaction(() => {
110
+ upsertProjectMetadata(db, input);
111
+ const row = db
112
+ .prepare("SELECT * FROM projects WHERE slug = ?")
113
+ .get(input.project);
114
+ return {
115
+ project: row.slug,
116
+ displayName: row.display_name,
117
+ ...(row.description ? { description: row.description } : {}),
118
+ ...(row.notes ? { notes: row.notes } : {}),
119
+ createdAt: row.created_at,
120
+ };
121
+ })();
122
+ }
123
+ export function listProjects(db, options = {}) {
124
+ const query = options.query?.trim().toLowerCase();
125
+ const projects = db
126
+ .prepare("SELECT * FROM projects ORDER BY slug ASC")
127
+ .all();
128
+ const latestStatuses = current(db);
129
+ const statusMap = new Map();
130
+ for (const entry of latestStatuses) {
131
+ statusMap.set(entry.project, entry);
132
+ }
133
+ const overviews = projects.map((row) => {
134
+ const slug = row.slug;
135
+ const latestStatus = statusMap.get(slug);
136
+ return {
137
+ project: slug,
138
+ displayName: row.display_name,
139
+ ...(row.description ? { description: row.description } : {}),
140
+ ...(row.notes ? { notes: row.notes } : {}),
141
+ createdAt: row.created_at,
142
+ ...(latestStatus ? { latestStatus } : {}),
143
+ };
144
+ });
145
+ if (!query)
146
+ return overviews;
147
+ return overviews.filter((overview) => {
148
+ if (overview.project.toLowerCase().includes(query))
149
+ return true;
150
+ if (overview.displayName.toLowerCase().includes(query))
151
+ return true;
152
+ if (overview.description?.toLowerCase().includes(query))
153
+ return true;
154
+ if (overview.notes?.toLowerCase().includes(query))
155
+ return true;
156
+ if (overview.latestStatus) {
157
+ if (overview.latestStatus.status.toLowerCase().includes(query))
158
+ return true;
159
+ if (overview.latestStatus.summary.toLowerCase().includes(query))
160
+ return true;
161
+ if (overview.latestStatus.progress?.toLowerCase().includes(query))
162
+ return true;
163
+ if (overview.latestStatus.tags.some((t) => t.toLowerCase().includes(query)))
164
+ return true;
165
+ }
166
+ return false;
167
+ });
168
+ }
70
169
  export function addStatus(db, value, defaultSource = "human") {
71
170
  const input = validateStatusEntry(value, defaultSource);
72
171
  return db.transaction(() => addValidatedStatus(db, input))();
@@ -81,7 +180,7 @@ function addValidatedStatus(db, input) {
81
180
  }
82
181
  }
83
182
  const now = new Date().toISOString();
84
- db.prepare("INSERT INTO projects(slug, display_name, created_at) VALUES (?, ?, ?) ON CONFLICT(slug) DO NOTHING").run(input.project, input.project, now);
183
+ upsertProjectMetadata(db, { project: input.project });
85
184
  const project = db
86
185
  .prepare("SELECT id FROM projects WHERE slug = ?")
87
186
  .get(input.project);
package/dist/domain.d.ts CHANGED
@@ -2,6 +2,22 @@ export declare const statuses: readonly ["idea", "planned", "active", "blocked",
2
2
  export declare const sources: readonly ["human", "agent", "import"];
3
3
  export type ProjectStatus = (typeof statuses)[number];
4
4
  export type Source = (typeof sources)[number];
5
+ export type ProjectMetadata = {
6
+ project: string;
7
+ displayName: string;
8
+ description?: string;
9
+ notes?: string;
10
+ createdAt: string;
11
+ };
12
+ export type ProjectOverview = ProjectMetadata & {
13
+ latestStatus?: StatusEntry;
14
+ };
15
+ export type ProjectMetadataInput = {
16
+ project: string;
17
+ displayName?: string;
18
+ description?: string | null;
19
+ notes?: string | null;
20
+ };
5
21
  export type StatusEntryInput = {
6
22
  project: string;
7
23
  status: ProjectStatus;
@@ -26,5 +42,6 @@ export declare class ValidationError extends Error {
26
42
  constructor(message: string);
27
43
  }
28
44
  export declare function parseTime(value: unknown, field: string): string;
45
+ export declare function validateProjectMetadataInput(value: unknown): ProjectMetadataInput;
29
46
  export declare function validateStatusEntry(value: unknown, defaultSource?: Source): StatusEntryInput & Required<Pick<StatusEntryInput, "occurredAt" | "source">>;
30
47
  export declare function toPortableStatusEntry(entry: StatusEntry): PortableStatusEntry;
package/dist/domain.js CHANGED
@@ -21,6 +21,12 @@ const keys = new Set([
21
21
  "source",
22
22
  "externalRef",
23
23
  ]);
24
+ const projectMetadataKeys = new Set([
25
+ "project",
26
+ "displayName",
27
+ "description",
28
+ "notes",
29
+ ]);
24
30
  const projectName = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
25
31
  export class ValidationError extends Error {
26
32
  constructor(message) {
@@ -38,6 +44,20 @@ function text(value, field, required = false) {
38
44
  throw new ValidationError(`${field} must be at most 2000 characters`);
39
45
  return normalized;
40
46
  }
47
+ function nullableText(value, field) {
48
+ if (value === undefined)
49
+ return undefined;
50
+ if (value === null)
51
+ return null;
52
+ if (typeof value !== "string")
53
+ throw new ValidationError(`${field} must be a string or null`);
54
+ const normalized = value.trim();
55
+ if (normalized === "")
56
+ return null;
57
+ if (normalized.length > 2_000)
58
+ throw new ValidationError(`${field} must be at most 2000 characters`);
59
+ return normalized;
60
+ }
41
61
  function textList(value, field) {
42
62
  if (value === undefined)
43
63
  return [];
@@ -53,6 +73,26 @@ export function parseTime(value, field) {
53
73
  }
54
74
  return new Date(value).toISOString();
55
75
  }
76
+ export function validateProjectMetadataInput(value) {
77
+ if (!value || typeof value !== "object" || Array.isArray(value))
78
+ throw new ValidationError("project metadata must be an object");
79
+ const input = value;
80
+ for (const key of Object.keys(input))
81
+ if (!projectMetadataKeys.has(key))
82
+ throw new ValidationError(`unknown field: ${key}`);
83
+ const project = text(input.project, "project", true);
84
+ if (!projectName.test(project))
85
+ throw new ValidationError("project must use letters, numbers, dots, underscores, or hyphens");
86
+ const displayName = text(input.displayName, "displayName");
87
+ const description = nullableText(input.description, "description");
88
+ const notes = nullableText(input.notes, "notes");
89
+ return {
90
+ project,
91
+ ...(displayName !== undefined ? { displayName } : {}),
92
+ ...(description !== undefined ? { description } : {}),
93
+ ...(notes !== undefined ? { notes } : {}),
94
+ };
95
+ }
56
96
  export function validateStatusEntry(value, defaultSource = "human") {
57
97
  if (!value || typeof value !== "object" || Array.isArray(value))
58
98
  throw new ValidationError("entry must be an object");
@@ -88,19 +128,20 @@ export function validateStatusEntry(value, defaultSource = "human") {
88
128
  };
89
129
  }
90
130
  export function toPortableStatusEntry(entry) {
91
- return {
131
+ const result = {
92
132
  project: entry.project,
93
133
  status: entry.status,
94
134
  summary: entry.summary,
95
- ...(entry.progress === undefined ? {} : { progress: entry.progress }),
96
135
  blockers: entry.blockers,
97
136
  nextSteps: entry.nextSteps,
98
137
  decisions: entry.decisions,
99
138
  tags: entry.tags,
100
139
  occurredAt: entry.occurredAt,
101
140
  source: entry.source,
102
- ...(entry.externalRef === undefined
103
- ? {}
104
- : { externalRef: entry.externalRef }),
105
141
  };
142
+ if (entry.progress !== undefined)
143
+ result.progress = entry.progress;
144
+ if (entry.externalRef !== undefined)
145
+ result.externalRef = entry.externalRef;
146
+ return result;
106
147
  }
@@ -36,8 +36,8 @@ The CLI accepts `pmgr <action>` for `add`, `import`, `current`, `history`, `at`,
36
36
 
37
37
  The database contains three structures:
38
38
 
39
- - `schema_migrations`: Applied migration versions.
40
- - `projects`: Project slug, display name, and creation time.
39
+ - `schema_migrations`: Applied migration versions (1: base tables, 2: project description & notes columns).
40
+ - `projects`: Project slug, display name, description, notes, and creation time.
41
41
  - `status_entries`: Immutable status snapshots and their project foreign keys.
42
42
 
43
43
  Key `status_entries` fields:
@@ -55,7 +55,7 @@ The `project_id, occurred_at DESC, id DESC` index supports current-status and hi
55
55
 
56
56
  ## Initialization and migrations
57
57
 
58
- After opening the database, `openLedger()` enables foreign keys, creates the migration table, and checks for and applies migration 1 within a `BEGIN IMMEDIATE` transaction. The immediate transaction prevents migration checks and writes from interleaving when several local processes open the database for the first time at once.
58
+ After opening the database, `openLedger()` enables foreign keys, creates the migration table, and checks for and applies migrations 1 and 2 within a `BEGIN IMMEDIATE` transaction. The immediate transaction prevents migration checks and writes from interleaving when several local processes open the database for the first time at once.
59
59
 
60
60
  The database path is read from `PMGR_DB_PATH` first; otherwise, it uses the absolute path `~/.local/share/project-mgr/project-mgr.sqlite`, resolved from the current user's home directory. Non-memory databases create their parent directory automatically.
61
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-mgr",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Local-first append-only project status ledger",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: project-mgr
3
- description: Manage project status with the local-first project-mgr CLI. Use when an AI agent needs to add an append-only project update, import status records, query current or historical project state, or export a status ledger.
3
+ description: Manage project status with the local-first project-mgr CLI. Use when an AI agent needs to add an append-only project update, set project descriptions/notes, list or fuzzy-search projects, import status records, query current or historical project state, or export a status ledger.
4
4
  ---
5
5
 
6
6
  # project-mgr
@@ -19,9 +19,18 @@ Use `npx --yes --package project-mgr pmgr`. In a source checkout, use `pnpm pmgr
19
19
  Prefer `--json` for data that must be interpreted or reported programmatically.
20
20
 
21
21
  ```bash
22
- # Add an agent-authored status snapshot.
22
+ # Set, update, or clear project metadata (description, notes, display name).
23
+ npx --yes --package project-mgr pmgr set-project --project my-project --description "Project summary" --notes "Internal notes" --json
24
+ npx --yes --package project-mgr pmgr set-project --project my-project --clear-description --clear-notes --json
25
+
26
+
27
+ # List all projects or fuzzy search across project names, descriptions, notes, status summaries, and tags.
28
+ npx --yes --package project-mgr pmgr projects --json
29
+ npx --yes --package project-mgr pmgr list --query "keyword" --json
30
+
31
+ # Add an agent-authored status snapshot (optionally with project description or notes).
23
32
  npx --yes --package project-mgr pmgr add --project my-project --status active --summary "Implemented the CLI" \
24
- --progress "Tests pass" --next "Publish the package" --source agent --json
33
+ --description "Project description" --progress "Tests pass" --next "Publish the package" --source agent --json
25
34
 
26
35
  # Query current state, history, or state at a time.
27
36
  npx --yes --package project-mgr pmgr current --project my-project --json