@joshuaswarren/openclaw-engram 8.3.35 → 8.3.36

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/dist/index.js CHANGED
@@ -16350,8 +16350,8 @@ mistakes: ${res.mistakesCount} patterns`
16350
16350
  }
16351
16351
 
16352
16352
  // src/cli.ts
16353
- import path38 from "path";
16354
- import { access as access2, readFile as readFile27, readdir as readdir16, unlink as unlink5 } from "fs/promises";
16353
+ import path39 from "path";
16354
+ import { access as access2, readFile as readFile28, readdir as readdir17, unlink as unlink5 } from "fs/promises";
16355
16355
 
16356
16356
  // src/transfer/export-json.ts
16357
16357
  import path26 from "path";
@@ -17230,8 +17230,8 @@ function gatherCandidates(input, warnings) {
17230
17230
  const record = rec;
17231
17231
  const content = typeof record.content === "string" ? record.content : null;
17232
17232
  if (!content) continue;
17233
- const path40 = typeof record.path === "string" ? record.path : "";
17234
- if (!path40.startsWith("transcripts/") && !path40.includes("/transcripts/")) continue;
17233
+ const path41 = typeof record.path === "string" ? record.path : "";
17234
+ if (!path41.startsWith("transcripts/") && !path41.includes("/transcripts/")) continue;
17235
17235
  rows.push(...parseJsonl(content, warnings));
17236
17236
  }
17237
17237
  return rows;
@@ -17699,6 +17699,351 @@ async function migrateObservations(options) {
17699
17699
  };
17700
17700
  }
17701
17701
 
17702
+ // src/work/storage.ts
17703
+ import path38 from "path";
17704
+ import { randomUUID } from "crypto";
17705
+ import { mkdir as mkdir27, readdir as readdir16, readFile as readFile27, rm as rm3, writeFile as writeFile24 } from "fs/promises";
17706
+ var TASK_TRANSITIONS = {
17707
+ todo: /* @__PURE__ */ new Set(["in_progress", "blocked", "cancelled"]),
17708
+ in_progress: /* @__PURE__ */ new Set(["todo", "blocked", "done", "cancelled"]),
17709
+ blocked: /* @__PURE__ */ new Set(["todo", "in_progress", "cancelled"]),
17710
+ done: /* @__PURE__ */ new Set(),
17711
+ cancelled: /* @__PURE__ */ new Set()
17712
+ };
17713
+ function serializeFrontmatter2(values) {
17714
+ const lines = Object.entries(values).map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
17715
+ return `---
17716
+ ${lines.join("\n")}
17717
+ ---`;
17718
+ }
17719
+ function parseFrontmatter3(raw) {
17720
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
17721
+ if (!match) return null;
17722
+ const fm = match[1] ?? "";
17723
+ const body = match[2] ?? "";
17724
+ const data = {};
17725
+ for (const line of fm.split("\n")) {
17726
+ if (!line.trim()) continue;
17727
+ const idx = line.indexOf(":");
17728
+ if (idx <= 0) continue;
17729
+ const key = line.slice(0, idx).trim();
17730
+ const rawValue = line.slice(idx + 1).trim();
17731
+ try {
17732
+ data[key] = JSON.parse(rawValue);
17733
+ } catch {
17734
+ data[key] = rawValue;
17735
+ }
17736
+ }
17737
+ return { data, body };
17738
+ }
17739
+ function toSafeSlug(value) {
17740
+ return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 80);
17741
+ }
17742
+ var WORK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{0,127}$/;
17743
+ function makeId(prefix, titleOrName, now) {
17744
+ const slug = toSafeSlug(titleOrName) || "item";
17745
+ const nonce = randomUUID().slice(0, 8);
17746
+ return `${prefix}-${now.getTime()}-${slug}-${nonce}`;
17747
+ }
17748
+ function assertValidWorkId(id, kind) {
17749
+ if (!WORK_ID_PATTERN.test(id)) {
17750
+ throw new Error(`invalid ${kind} id: ${id}`);
17751
+ }
17752
+ }
17753
+ function ensureString(value, fallback = "") {
17754
+ return typeof value === "string" ? value : fallback;
17755
+ }
17756
+ function ensureStringArray(value) {
17757
+ if (!Array.isArray(value)) return [];
17758
+ return value.filter((entry) => typeof entry === "string");
17759
+ }
17760
+ function ensureNullableString(value) {
17761
+ return typeof value === "string" ? value : null;
17762
+ }
17763
+ function ensureTaskStatus(value) {
17764
+ if (value === "todo" || value === "in_progress" || value === "blocked" || value === "done" || value === "cancelled") {
17765
+ return value;
17766
+ }
17767
+ return "todo";
17768
+ }
17769
+ function ensureTaskPriority(value) {
17770
+ if (value === "low" || value === "medium" || value === "high") return value;
17771
+ return "medium";
17772
+ }
17773
+ function ensureProjectStatus(value) {
17774
+ if (value === "active" || value === "on_hold" || value === "completed" || value === "archived") return value;
17775
+ return "active";
17776
+ }
17777
+ var WorkStorage = class {
17778
+ constructor(memoryDir) {
17779
+ this.memoryDir = memoryDir;
17780
+ this.tasksDir = path38.join(memoryDir, "work", "tasks");
17781
+ this.projectsDir = path38.join(memoryDir, "work", "projects");
17782
+ }
17783
+ tasksDir;
17784
+ projectsDir;
17785
+ async ensureDirectories() {
17786
+ await mkdir27(this.tasksDir, { recursive: true });
17787
+ await mkdir27(this.projectsDir, { recursive: true });
17788
+ }
17789
+ taskPath(id) {
17790
+ assertValidWorkId(id, "task");
17791
+ return path38.join(this.tasksDir, `${id}.md`);
17792
+ }
17793
+ projectPath(id) {
17794
+ assertValidWorkId(id, "project");
17795
+ return path38.join(this.projectsDir, `${id}.md`);
17796
+ }
17797
+ serializeTask(task) {
17798
+ return `${serializeFrontmatter2(task)}
17799
+
17800
+ ${task.description}
17801
+ `;
17802
+ }
17803
+ serializeProject(project) {
17804
+ return `${serializeFrontmatter2(project)}
17805
+
17806
+ ${project.description}
17807
+ `;
17808
+ }
17809
+ parseTask(raw) {
17810
+ const parsed = parseFrontmatter3(raw);
17811
+ if (!parsed) return null;
17812
+ const d = parsed.data;
17813
+ return {
17814
+ id: ensureString(d.id),
17815
+ title: ensureString(d.title),
17816
+ description: ensureString(d.description, parsed.body.trim()),
17817
+ status: ensureTaskStatus(d.status),
17818
+ priority: ensureTaskPriority(d.priority),
17819
+ owner: ensureNullableString(d.owner),
17820
+ assignee: ensureNullableString(d.assignee),
17821
+ projectId: ensureNullableString(d.projectId),
17822
+ tags: ensureStringArray(d.tags),
17823
+ dueAt: ensureNullableString(d.dueAt),
17824
+ createdAt: ensureString(d.createdAt),
17825
+ updatedAt: ensureString(d.updatedAt)
17826
+ };
17827
+ }
17828
+ parseProject(raw) {
17829
+ const parsed = parseFrontmatter3(raw);
17830
+ if (!parsed) return null;
17831
+ const d = parsed.data;
17832
+ return {
17833
+ id: ensureString(d.id),
17834
+ name: ensureString(d.name),
17835
+ description: ensureString(d.description, parsed.body.trim()),
17836
+ status: ensureProjectStatus(d.status),
17837
+ owner: ensureNullableString(d.owner),
17838
+ tags: ensureStringArray(d.tags),
17839
+ taskIds: ensureStringArray(d.taskIds),
17840
+ createdAt: ensureString(d.createdAt),
17841
+ updatedAt: ensureString(d.updatedAt)
17842
+ };
17843
+ }
17844
+ async createTask(input, now = /* @__PURE__ */ new Date()) {
17845
+ await this.ensureDirectories();
17846
+ const timestamp = now.toISOString();
17847
+ const task = {
17848
+ id: input.id ?? makeId("task", input.title, now),
17849
+ title: input.title,
17850
+ description: input.description ?? "",
17851
+ status: input.status ?? "todo",
17852
+ priority: input.priority ?? "medium",
17853
+ owner: input.owner ?? null,
17854
+ assignee: input.assignee ?? null,
17855
+ projectId: input.projectId ?? null,
17856
+ tags: input.tags ?? [],
17857
+ dueAt: input.dueAt ?? null,
17858
+ createdAt: timestamp,
17859
+ updatedAt: timestamp
17860
+ };
17861
+ if (task.projectId) {
17862
+ const project = await this.getProject(task.projectId);
17863
+ if (!project) {
17864
+ throw new Error(`project not found: ${task.projectId}`);
17865
+ }
17866
+ }
17867
+ await writeFile24(this.taskPath(task.id), this.serializeTask(task), "utf-8");
17868
+ if (task.projectId) {
17869
+ await this.addTaskIdToProject(task.projectId, task.id, now);
17870
+ }
17871
+ return task;
17872
+ }
17873
+ async getTask(id) {
17874
+ try {
17875
+ const raw = await readFile27(this.taskPath(id), "utf-8");
17876
+ return this.parseTask(raw);
17877
+ } catch {
17878
+ return null;
17879
+ }
17880
+ }
17881
+ async listTasks(filter) {
17882
+ await this.ensureDirectories();
17883
+ const entries = await readdir16(this.tasksDir, { withFileTypes: true });
17884
+ const out = [];
17885
+ for (const entry of entries) {
17886
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
17887
+ const raw = await readFile27(path38.join(this.tasksDir, entry.name), "utf-8");
17888
+ const task = this.parseTask(raw);
17889
+ if (!task) continue;
17890
+ if (filter?.status && task.status !== filter.status) continue;
17891
+ if (filter?.owner && task.owner !== filter.owner) continue;
17892
+ if (filter?.assignee && task.assignee !== filter.assignee) continue;
17893
+ if (filter?.projectId && task.projectId !== filter.projectId) continue;
17894
+ out.push(task);
17895
+ }
17896
+ out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
17897
+ return out;
17898
+ }
17899
+ async updateTask(id, patch, now = /* @__PURE__ */ new Date()) {
17900
+ const existing = await this.getTask(id);
17901
+ if (!existing) return null;
17902
+ const projectIdPatched = Object.prototype.hasOwnProperty.call(patch, "projectId");
17903
+ const statusPatched = Object.prototype.hasOwnProperty.call(patch, "status");
17904
+ const nextProjectId = projectIdPatched ? patch.projectId ?? null : existing.projectId;
17905
+ if (statusPatched && patch.status && existing.status !== patch.status && !TASK_TRANSITIONS[existing.status].has(patch.status)) {
17906
+ throw new Error(`invalid task status transition: ${existing.status} -> ${patch.status}`);
17907
+ }
17908
+ if (projectIdPatched && nextProjectId) {
17909
+ const nextProject = await this.getProject(nextProjectId);
17910
+ if (!nextProject) {
17911
+ throw new Error(`project not found: ${nextProjectId}`);
17912
+ }
17913
+ }
17914
+ if (projectIdPatched && existing.projectId !== nextProjectId) {
17915
+ if (existing.projectId) {
17916
+ await this.removeTaskIdFromProject(existing.projectId, id, now);
17917
+ }
17918
+ if (nextProjectId) {
17919
+ await this.addTaskIdToProject(nextProjectId, id, now);
17920
+ }
17921
+ }
17922
+ const next = {
17923
+ ...existing,
17924
+ ...patch,
17925
+ projectId: nextProjectId,
17926
+ tags: patch.tags ?? existing.tags,
17927
+ updatedAt: now.toISOString()
17928
+ };
17929
+ await writeFile24(this.taskPath(id), this.serializeTask(next), "utf-8");
17930
+ return next;
17931
+ }
17932
+ async transitionTask(id, nextStatus, now = /* @__PURE__ */ new Date()) {
17933
+ const existing = await this.getTask(id);
17934
+ if (!existing) throw new Error(`task not found: ${id}`);
17935
+ if (existing.status === nextStatus) return existing;
17936
+ if (!TASK_TRANSITIONS[existing.status].has(nextStatus)) {
17937
+ throw new Error(`invalid task status transition: ${existing.status} -> ${nextStatus}`);
17938
+ }
17939
+ const updated = await this.updateTask(id, { status: nextStatus }, now);
17940
+ if (!updated) throw new Error(`task not found after update: ${id}`);
17941
+ return updated;
17942
+ }
17943
+ async deleteTask(id) {
17944
+ try {
17945
+ const existing = await this.getTask(id);
17946
+ await rm3(this.taskPath(id));
17947
+ if (existing?.projectId) {
17948
+ await this.removeTaskIdFromProject(existing.projectId, id);
17949
+ }
17950
+ return true;
17951
+ } catch {
17952
+ return false;
17953
+ }
17954
+ }
17955
+ async createProject(input, now = /* @__PURE__ */ new Date()) {
17956
+ await this.ensureDirectories();
17957
+ const timestamp = now.toISOString();
17958
+ const project = {
17959
+ id: input.id ?? makeId("project", input.name, now),
17960
+ name: input.name,
17961
+ description: input.description ?? "",
17962
+ status: input.status ?? "active",
17963
+ owner: input.owner ?? null,
17964
+ tags: input.tags ?? [],
17965
+ taskIds: [],
17966
+ createdAt: timestamp,
17967
+ updatedAt: timestamp
17968
+ };
17969
+ await writeFile24(this.projectPath(project.id), this.serializeProject(project), "utf-8");
17970
+ return project;
17971
+ }
17972
+ async getProject(id) {
17973
+ try {
17974
+ const raw = await readFile27(this.projectPath(id), "utf-8");
17975
+ return this.parseProject(raw);
17976
+ } catch {
17977
+ return null;
17978
+ }
17979
+ }
17980
+ async listProjects() {
17981
+ await this.ensureDirectories();
17982
+ const entries = await readdir16(this.projectsDir, { withFileTypes: true });
17983
+ const out = [];
17984
+ for (const entry of entries) {
17985
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
17986
+ const raw = await readFile27(path38.join(this.projectsDir, entry.name), "utf-8");
17987
+ const project = this.parseProject(raw);
17988
+ if (project) out.push(project);
17989
+ }
17990
+ out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
17991
+ return out;
17992
+ }
17993
+ async updateProject(id, patch, now = /* @__PURE__ */ new Date()) {
17994
+ const existing = await this.getProject(id);
17995
+ if (!existing) return null;
17996
+ const next = {
17997
+ ...existing,
17998
+ ...patch,
17999
+ tags: patch.tags ?? existing.tags,
18000
+ taskIds: patch.taskIds ? [...patch.taskIds].sort() : existing.taskIds,
18001
+ updatedAt: now.toISOString()
18002
+ };
18003
+ await writeFile24(this.projectPath(id), this.serializeProject(next), "utf-8");
18004
+ return next;
18005
+ }
18006
+ async deleteProject(id) {
18007
+ try {
18008
+ const existing = await this.getProject(id);
18009
+ if (existing) {
18010
+ for (const taskId of existing.taskIds) {
18011
+ await this.updateTask(taskId, { projectId: null });
18012
+ }
18013
+ }
18014
+ await rm3(this.projectPath(id));
18015
+ return true;
18016
+ } catch {
18017
+ return false;
18018
+ }
18019
+ }
18020
+ async linkTaskToProject(taskId, projectId, now = /* @__PURE__ */ new Date()) {
18021
+ const task = await this.getTask(taskId);
18022
+ if (!task) throw new Error(`task not found: ${taskId}`);
18023
+ const project = await this.getProject(projectId);
18024
+ if (!project) throw new Error(`project not found: ${projectId}`);
18025
+ const updatedTask = await this.updateTask(taskId, { projectId }, now);
18026
+ if (!updatedTask) throw new Error(`task not found after update: ${taskId}`);
18027
+ const updatedProject = await this.getProject(projectId);
18028
+ if (!updatedProject) throw new Error(`project not found after update: ${projectId}`);
18029
+ return { task: updatedTask, project: updatedProject };
18030
+ }
18031
+ async removeTaskIdFromProject(projectId, taskId, now = /* @__PURE__ */ new Date()) {
18032
+ const project = await this.getProject(projectId);
18033
+ if (!project) return;
18034
+ const filtered = project.taskIds.filter((id) => id !== taskId);
18035
+ if (filtered.length === project.taskIds.length) return;
18036
+ await this.updateProject(projectId, { taskIds: filtered }, now);
18037
+ }
18038
+ async addTaskIdToProject(projectId, taskId, now = /* @__PURE__ */ new Date()) {
18039
+ const project = await this.getProject(projectId);
18040
+ if (!project) return;
18041
+ const taskIds = new Set(project.taskIds);
18042
+ taskIds.add(taskId);
18043
+ await this.updateProject(projectId, { taskIds: Array.from(taskIds) }, now);
18044
+ }
18045
+ };
18046
+
17702
18047
  // src/cli.ts
17703
18048
  function rankCandidateForKeep(a, b) {
17704
18049
  const aConfidence = typeof a.frontmatter.confidence === "number" ? a.frontmatter.confidence : 0;
@@ -17748,6 +18093,29 @@ function planExactDuplicateDeletions(memories) {
17748
18093
  function planAggressiveDuplicateDeletions(memories) {
17749
18094
  return buildDedupePlan(memories, (memory) => normalizeAggressiveBody(memory.content));
17750
18095
  }
18096
+ function normalizeNullableCliValue(value) {
18097
+ if (value === void 0) return void 0;
18098
+ const trimmed = value.trim();
18099
+ if (trimmed.length === 0 || trimmed.toLowerCase() === "null") return null;
18100
+ return trimmed;
18101
+ }
18102
+ function parseTagsCsv(raw, preserveEmpty = false) {
18103
+ if (raw === void 0) return void 0;
18104
+ const tags = raw.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
18105
+ if (tags.length === 0) {
18106
+ return preserveEmpty ? [] : void 0;
18107
+ }
18108
+ return tags;
18109
+ }
18110
+ function isWorkTaskStatus(value) {
18111
+ return value === "todo" || value === "in_progress" || value === "blocked" || value === "done" || value === "cancelled";
18112
+ }
18113
+ function isWorkTaskPriority(value) {
18114
+ return value === "low" || value === "medium" || value === "high";
18115
+ }
18116
+ function isWorkProjectStatus(value) {
18117
+ return value === "active" || value === "on_hold" || value === "completed" || value === "archived";
18118
+ }
17751
18119
  async function runArchiveObservationsCliCommand(options) {
17752
18120
  return archiveObservations({
17753
18121
  memoryDir: options.memoryDir,
@@ -17770,6 +18138,128 @@ async function runMigrateObservationsCliCommand(options) {
17770
18138
  now: options.now
17771
18139
  });
17772
18140
  }
18141
+ async function runWorkTaskCliCommand(options) {
18142
+ const storage = new WorkStorage(options.memoryDir);
18143
+ if (options.action === "create") {
18144
+ if (!options.title || options.title.trim().length === 0) throw new Error("missing title");
18145
+ if (options.status !== void 0 && !isWorkTaskStatus(options.status)) throw new Error(`invalid task status: ${options.status}`);
18146
+ if (options.priority !== void 0 && !isWorkTaskPriority(options.priority)) {
18147
+ throw new Error(`invalid task priority: ${options.priority}`);
18148
+ }
18149
+ const explicitId = options.id?.trim();
18150
+ if (explicitId && explicitId.length > 0) {
18151
+ const existing = await storage.getTask(explicitId);
18152
+ if (existing) throw new Error(`task already exists: ${explicitId}`);
18153
+ }
18154
+ return storage.createTask({
18155
+ id: explicitId && explicitId.length > 0 ? explicitId : void 0,
18156
+ title: options.title.trim(),
18157
+ description: options.description?.trim(),
18158
+ status: options.status,
18159
+ priority: options.priority,
18160
+ owner: normalizeNullableCliValue(options.owner),
18161
+ assignee: normalizeNullableCliValue(options.assignee),
18162
+ projectId: normalizeNullableCliValue(options.projectId),
18163
+ tags: options.tags,
18164
+ dueAt: normalizeNullableCliValue(options.dueAt)
18165
+ });
18166
+ }
18167
+ if (options.action === "get") {
18168
+ if (!options.id || options.id.trim().length === 0) throw new Error("missing id");
18169
+ return storage.getTask(options.id.trim());
18170
+ }
18171
+ if (options.action === "list") {
18172
+ if (options.status !== void 0 && !isWorkTaskStatus(options.status)) throw new Error(`invalid task status: ${options.status}`);
18173
+ return storage.listTasks({
18174
+ status: options.status,
18175
+ owner: options.owner?.trim() || void 0,
18176
+ assignee: options.assignee?.trim() || void 0,
18177
+ projectId: options.projectId?.trim() || void 0
18178
+ });
18179
+ }
18180
+ if (options.action === "update") {
18181
+ if (!options.id || options.id.trim().length === 0) throw new Error("missing id");
18182
+ const patch = options.patch ?? {};
18183
+ if (patch.status !== void 0 && !isWorkTaskStatus(patch.status)) throw new Error(`invalid task status: ${patch.status}`);
18184
+ if (patch.priority !== void 0 && !isWorkTaskPriority(patch.priority)) {
18185
+ throw new Error(`invalid task priority: ${patch.priority}`);
18186
+ }
18187
+ const sparsePatch = {};
18188
+ if (Object.prototype.hasOwnProperty.call(patch, "title")) sparsePatch.title = patch.title;
18189
+ if (Object.prototype.hasOwnProperty.call(patch, "description")) sparsePatch.description = patch.description;
18190
+ if (Object.prototype.hasOwnProperty.call(patch, "status")) sparsePatch.status = patch.status;
18191
+ if (Object.prototype.hasOwnProperty.call(patch, "priority")) sparsePatch.priority = patch.priority;
18192
+ if (Object.prototype.hasOwnProperty.call(patch, "owner")) sparsePatch.owner = patch.owner;
18193
+ if (Object.prototype.hasOwnProperty.call(patch, "assignee")) sparsePatch.assignee = patch.assignee;
18194
+ if (Object.prototype.hasOwnProperty.call(patch, "projectId")) sparsePatch.projectId = patch.projectId;
18195
+ if (Object.prototype.hasOwnProperty.call(patch, "tags")) sparsePatch.tags = patch.tags;
18196
+ if (Object.prototype.hasOwnProperty.call(patch, "dueAt")) sparsePatch.dueAt = patch.dueAt;
18197
+ return storage.updateTask(options.id.trim(), sparsePatch);
18198
+ }
18199
+ if (options.action === "transition") {
18200
+ if (!options.id || options.id.trim().length === 0) throw new Error("missing id");
18201
+ if (!options.status || !isWorkTaskStatus(options.status)) throw new Error(`invalid task status: ${options.status}`);
18202
+ return storage.transitionTask(options.id.trim(), options.status);
18203
+ }
18204
+ if (options.action === "delete") {
18205
+ if (!options.id || options.id.trim().length === 0) throw new Error("missing id");
18206
+ return storage.deleteTask(options.id.trim());
18207
+ }
18208
+ if (options.action === "link") {
18209
+ if (!options.id || options.id.trim().length === 0) throw new Error("missing id");
18210
+ if (!options.projectId || options.projectId.trim().length === 0) throw new Error("missing projectId");
18211
+ return storage.linkTaskToProject(options.id.trim(), options.projectId.trim());
18212
+ }
18213
+ throw new Error(`unsupported task action: ${options.action}`);
18214
+ }
18215
+ async function runWorkProjectCliCommand(options) {
18216
+ const storage = new WorkStorage(options.memoryDir);
18217
+ if (options.action === "create") {
18218
+ if (!options.name || options.name.trim().length === 0) throw new Error("missing name");
18219
+ if (options.status !== void 0 && !isWorkProjectStatus(options.status)) {
18220
+ throw new Error(`invalid project status: ${options.status}`);
18221
+ }
18222
+ const explicitId = options.id?.trim();
18223
+ if (explicitId && explicitId.length > 0) {
18224
+ const existing = await storage.getProject(explicitId);
18225
+ if (existing) throw new Error(`project already exists: ${explicitId}`);
18226
+ }
18227
+ return storage.createProject({
18228
+ id: explicitId && explicitId.length > 0 ? explicitId : void 0,
18229
+ name: options.name.trim(),
18230
+ description: options.description?.trim(),
18231
+ status: options.status,
18232
+ owner: normalizeNullableCliValue(options.owner),
18233
+ tags: options.tags
18234
+ });
18235
+ }
18236
+ if (options.action === "get") {
18237
+ if (!options.id || options.id.trim().length === 0) throw new Error("missing id");
18238
+ return storage.getProject(options.id.trim());
18239
+ }
18240
+ if (options.action === "list") {
18241
+ return storage.listProjects();
18242
+ }
18243
+ if (options.action === "update") {
18244
+ if (!options.id || options.id.trim().length === 0) throw new Error("missing id");
18245
+ const patch = options.patch ?? {};
18246
+ if (patch.status !== void 0 && !isWorkProjectStatus(patch.status)) {
18247
+ throw new Error(`invalid project status: ${patch.status}`);
18248
+ }
18249
+ const sparsePatch = {};
18250
+ if (Object.prototype.hasOwnProperty.call(patch, "name")) sparsePatch.name = patch.name;
18251
+ if (Object.prototype.hasOwnProperty.call(patch, "description")) sparsePatch.description = patch.description;
18252
+ if (Object.prototype.hasOwnProperty.call(patch, "status")) sparsePatch.status = patch.status;
18253
+ if (Object.prototype.hasOwnProperty.call(patch, "owner")) sparsePatch.owner = patch.owner;
18254
+ if (Object.prototype.hasOwnProperty.call(patch, "tags")) sparsePatch.tags = patch.tags;
18255
+ return storage.updateProject(options.id.trim(), sparsePatch);
18256
+ }
18257
+ if (options.action === "delete") {
18258
+ if (!options.id || options.id.trim().length === 0) throw new Error("missing id");
18259
+ return storage.deleteProject(options.id.trim());
18260
+ }
18261
+ throw new Error(`unsupported project action: ${options.action}`);
18262
+ }
17773
18263
  async function withTimeout(promise, timeoutMs, timeoutMessage) {
17774
18264
  let timer;
17775
18265
  try {
@@ -17785,7 +18275,7 @@ async function withTimeout(promise, timeoutMs, timeoutMessage) {
17785
18275
  }
17786
18276
  async function runReplayCliCommand(orchestrator, options) {
17787
18277
  const extractionIdleTimeoutMs = Number.isFinite(options.extractionIdleTimeoutMs) ? Math.max(1e3, Math.floor(options.extractionIdleTimeoutMs)) : 15 * 6e4;
17788
- const inputRaw = await readFile27(options.inputPath, "utf-8");
18278
+ const inputRaw = await readFile28(options.inputPath, "utf-8");
17789
18279
  const registry = buildReplayNormalizerRegistry([
17790
18280
  openclawReplayNormalizer,
17791
18281
  claudeReplayNormalizer,
@@ -17850,7 +18340,7 @@ async function runReplayCliCommand(orchestrator, options) {
17850
18340
  async function getPluginVersion() {
17851
18341
  try {
17852
18342
  const pkgPath = new URL("../package.json", import.meta.url);
17853
- const raw = await readFile27(pkgPath, "utf-8");
18343
+ const raw = await readFile28(pkgPath, "utf-8");
17854
18344
  const parsed = JSON.parse(raw);
17855
18345
  return parsed.version ?? "unknown";
17856
18346
  } catch {
@@ -17869,32 +18359,32 @@ async function resolveMemoryDirForNamespace(orchestrator, namespace) {
17869
18359
  const ns = (namespace ?? "").trim();
17870
18360
  if (!ns) return orchestrator.config.memoryDir;
17871
18361
  if (!orchestrator.config.namespacesEnabled) return orchestrator.config.memoryDir;
17872
- const candidate = path38.join(orchestrator.config.memoryDir, "namespaces", ns);
18362
+ const candidate = path39.join(orchestrator.config.memoryDir, "namespaces", ns);
17873
18363
  if (ns === orchestrator.config.defaultNamespace) {
17874
18364
  return await exists2(candidate) ? candidate : orchestrator.config.memoryDir;
17875
18365
  }
17876
18366
  return candidate;
17877
18367
  }
17878
18368
  async function readAllMemoryFiles(memoryDir) {
17879
- const roots = [path38.join(memoryDir, "facts"), path38.join(memoryDir, "corrections")];
18369
+ const roots = [path39.join(memoryDir, "facts"), path39.join(memoryDir, "corrections")];
17880
18370
  const out = [];
17881
18371
  const walk = async (dir) => {
17882
18372
  let entries;
17883
18373
  try {
17884
- entries = await readdir16(dir, { withFileTypes: true });
18374
+ entries = await readdir17(dir, { withFileTypes: true });
17885
18375
  } catch {
17886
18376
  return;
17887
18377
  }
17888
18378
  for (const entry of entries) {
17889
18379
  const entryName = typeof entry.name === "string" ? entry.name : entry.name.toString("utf-8");
17890
- const fullPath = path38.join(dir, entryName);
18380
+ const fullPath = path39.join(dir, entryName);
17891
18381
  if (entry.isDirectory()) {
17892
18382
  await walk(fullPath);
17893
18383
  continue;
17894
18384
  }
17895
18385
  if (!entry.isFile() || !entryName.endsWith(".md")) continue;
17896
18386
  try {
17897
- const raw = await readFile27(fullPath, "utf-8");
18387
+ const raw = await readFile28(fullPath, "utf-8");
17898
18388
  const parsed = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
17899
18389
  if (!parsed) continue;
17900
18390
  const fmRaw = parsed[1];
@@ -18191,6 +18681,82 @@ function registerCli(api, orchestrator) {
18191
18681
  if (result.backupPath) console.log(`Backup path: ${result.backupPath}`);
18192
18682
  console.log("OK");
18193
18683
  });
18684
+ cmd.command("task").description("Manage work tasks").argument("<action>", "create|get|list|update|transition|delete|link").option("--id <id>", "Task ID").option("--title <title>", "Task title").option("--description <description>", "Task description").option("--status <status>", "Task status").option("--priority <priority>", "Task priority").option("--owner <owner>", "Task owner").option("--assignee <assignee>", "Task assignee").option("--project-id <projectId>", "Project ID").option("--tags <csv>", "Comma-separated tags").option("--due-at <iso>", "Due timestamp (ISO)").action(async (...args) => {
18685
+ const actionRaw = typeof args[0] === "string" ? args[0].trim().toLowerCase() : "";
18686
+ const options = args[1] ?? {};
18687
+ const statusOptRaw = typeof options.status === "string" ? options.status.trim().toLowerCase() : void 0;
18688
+ const priorityOptRaw = typeof options.priority === "string" ? options.priority.trim().toLowerCase() : void 0;
18689
+ if (statusOptRaw !== void 0 && !isWorkTaskStatus(statusOptRaw)) {
18690
+ throw new Error(`invalid task status: ${statusOptRaw}`);
18691
+ }
18692
+ if (priorityOptRaw !== void 0 && !isWorkTaskPriority(priorityOptRaw)) {
18693
+ throw new Error(`invalid task priority: ${priorityOptRaw}`);
18694
+ }
18695
+ const patch = {};
18696
+ if (typeof options.title === "string") patch.title = options.title.trim();
18697
+ if (typeof options.description === "string") patch.description = options.description.trim();
18698
+ if (statusOptRaw !== void 0) patch.status = statusOptRaw;
18699
+ if (priorityOptRaw !== void 0) patch.priority = priorityOptRaw;
18700
+ if (typeof options.owner === "string") patch.owner = normalizeNullableCliValue(options.owner) ?? null;
18701
+ if (typeof options.assignee === "string") patch.assignee = normalizeNullableCliValue(options.assignee) ?? null;
18702
+ if (typeof options.projectId === "string") patch.projectId = normalizeNullableCliValue(options.projectId) ?? null;
18703
+ if (typeof options.tags === "string") {
18704
+ patch.tags = parseTagsCsv(options.tags, true);
18705
+ }
18706
+ if (typeof options.dueAt === "string") patch.dueAt = normalizeNullableCliValue(options.dueAt) ?? null;
18707
+ const result = await runWorkTaskCliCommand({
18708
+ memoryDir: orchestrator.config.memoryDir,
18709
+ action: actionRaw,
18710
+ id: typeof options.id === "string" ? options.id : void 0,
18711
+ title: typeof options.title === "string" ? options.title : void 0,
18712
+ description: typeof options.description === "string" ? options.description : void 0,
18713
+ status: statusOptRaw,
18714
+ priority: priorityOptRaw,
18715
+ owner: typeof options.owner === "string" ? options.owner : void 0,
18716
+ assignee: typeof options.assignee === "string" ? options.assignee : void 0,
18717
+ projectId: typeof options.projectId === "string" ? options.projectId : void 0,
18718
+ tags: typeof options.tags === "string" ? parseTagsCsv(options.tags, true) : void 0,
18719
+ dueAt: typeof options.dueAt === "string" ? options.dueAt : void 0,
18720
+ patch
18721
+ });
18722
+ if (Array.isArray(result)) {
18723
+ console.log(`Count: ${result.length}`);
18724
+ }
18725
+ console.log(JSON.stringify(result, null, 2));
18726
+ console.log("OK");
18727
+ });
18728
+ cmd.command("project").description("Manage work projects").argument("<action>", "create|get|list|update|delete").option("--id <id>", "Project ID").option("--name <name>", "Project name").option("--description <description>", "Project description").option("--status <status>", "Project status").option("--owner <owner>", "Project owner").option("--tags <csv>", "Comma-separated tags").action(async (...args) => {
18729
+ const actionRaw = typeof args[0] === "string" ? args[0].trim().toLowerCase() : "";
18730
+ const options = args[1] ?? {};
18731
+ const statusOptRaw = typeof options.status === "string" ? options.status.trim().toLowerCase() : void 0;
18732
+ if (statusOptRaw !== void 0 && !isWorkProjectStatus(statusOptRaw)) {
18733
+ throw new Error(`invalid project status: ${statusOptRaw}`);
18734
+ }
18735
+ const patch = {};
18736
+ if (typeof options.name === "string") patch.name = options.name.trim();
18737
+ if (typeof options.description === "string") patch.description = options.description.trim();
18738
+ if (statusOptRaw !== void 0) patch.status = statusOptRaw;
18739
+ if (typeof options.owner === "string") patch.owner = normalizeNullableCliValue(options.owner) ?? null;
18740
+ if (typeof options.tags === "string") {
18741
+ patch.tags = parseTagsCsv(options.tags, true);
18742
+ }
18743
+ const result = await runWorkProjectCliCommand({
18744
+ memoryDir: orchestrator.config.memoryDir,
18745
+ action: actionRaw,
18746
+ id: typeof options.id === "string" ? options.id : void 0,
18747
+ name: typeof options.name === "string" ? options.name : void 0,
18748
+ description: typeof options.description === "string" ? options.description : void 0,
18749
+ status: statusOptRaw,
18750
+ owner: typeof options.owner === "string" ? options.owner : void 0,
18751
+ tags: typeof options.tags === "string" ? parseTagsCsv(options.tags, true) : void 0,
18752
+ patch
18753
+ });
18754
+ if (Array.isArray(result)) {
18755
+ console.log(`Count: ${result.length}`);
18756
+ }
18757
+ console.log(JSON.stringify(result, null, 2));
18758
+ console.log("OK");
18759
+ });
18194
18760
  cmd.command("dedupe-exact").description("Delete exact duplicate memory entries (same body text), keeping highest-confidence/newest copy").option("--dry-run", "Show what would be deleted without deleting files").option("--namespace <ns>", "Namespace to dedupe (v3.0+, default: config defaultNamespace)", "").option("--qmd-sync", "Run QMD update/embed after deletions (default: off)").action(async (...args) => {
18195
18761
  const options = args[0] ?? {};
18196
18762
  const dryRun = options.dryRun === true;
@@ -18431,7 +18997,7 @@ function registerCli(api, orchestrator) {
18431
18997
  }
18432
18998
  });
18433
18999
  cmd.command("identity").description("Show agent identity reflections").action(async () => {
18434
- const workspaceDir = path38.join(process.env.HOME ?? "~", ".openclaw", "workspace");
19000
+ const workspaceDir = path39.join(process.env.HOME ?? "~", ".openclaw", "workspace");
18435
19001
  const identity = await orchestrator.storage.readIdentity(workspaceDir);
18436
19002
  if (!identity) {
18437
19003
  console.log("No identity file found.");
@@ -18654,8 +19220,8 @@ function registerCli(api, orchestrator) {
18654
19220
  const options = args[0] ?? {};
18655
19221
  const threadId = options.thread;
18656
19222
  const top = parseInt(options.top ?? "10", 10);
18657
- const memoryDir = path38.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
18658
- const threading = new ThreadingManager(path38.join(memoryDir, "threads"));
19223
+ const memoryDir = path39.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
19224
+ const threading = new ThreadingManager(path39.join(memoryDir, "threads"));
18659
19225
  if (threadId) {
18660
19226
  const thread = await threading.loadThread(threadId);
18661
19227
  if (!thread) {
@@ -18828,16 +19394,16 @@ function parseDuration(duration) {
18828
19394
  }
18829
19395
 
18830
19396
  // src/index.ts
18831
- import { readFile as readFile28, writeFile as writeFile24 } from "fs/promises";
19397
+ import { readFile as readFile29, writeFile as writeFile25 } from "fs/promises";
18832
19398
  import { readFileSync as readFileSync4 } from "fs";
18833
- import path39 from "path";
19399
+ import path40 from "path";
18834
19400
  import os5 from "os";
18835
19401
  var ENGRAM_REGISTERED_GUARD = "__openclawEngramRegistered";
18836
19402
  function loadPluginConfigFromFile() {
18837
19403
  try {
18838
19404
  const explicitConfigPath = process.env.OPENCLAW_ENGRAM_CONFIG_PATH || process.env.OPENCLAW_CONFIG_PATH;
18839
19405
  const homeDir = process.env.HOME ?? os5.homedir();
18840
- const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path39.join(homeDir, ".openclaw", "openclaw.json");
19406
+ const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path40.join(homeDir, ".openclaw", "openclaw.json");
18841
19407
  const content = readFileSync4(configPath, "utf-8");
18842
19408
  const config = JSON.parse(content);
18843
19409
  const pluginEntry = config?.plugins?.entries?.["openclaw-engram"];
@@ -19045,11 +19611,11 @@ Use this context naturally when relevant. Never quote or expose this memory cont
19045
19611
  );
19046
19612
  async function ensureHourlySummaryCron(api2) {
19047
19613
  const jobId = "engram-hourly-summary";
19048
- const cronFilePath = path39.join(os5.homedir(), ".openclaw", "cron", "jobs.json");
19614
+ const cronFilePath = path40.join(os5.homedir(), ".openclaw", "cron", "jobs.json");
19049
19615
  try {
19050
19616
  let jobsData = { version: 1, jobs: [] };
19051
19617
  try {
19052
- const content = await readFile28(cronFilePath, "utf-8");
19618
+ const content = await readFile29(cronFilePath, "utf-8");
19053
19619
  jobsData = JSON.parse(content);
19054
19620
  } catch {
19055
19621
  }
@@ -19086,7 +19652,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
19086
19652
  state: {}
19087
19653
  };
19088
19654
  jobsData.jobs.push(newJob);
19089
- await writeFile24(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
19655
+ await writeFile25(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
19090
19656
  log.info("auto-registered hourly summary cron job");
19091
19657
  } catch (err) {
19092
19658
  log.error("failed to auto-register hourly summary cron job:", err);