denchclaw 2.4.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/apps/web/.next/standalone/apps/web/.next/BUILD_ID +1 -1
  2. package/apps/web/.next/standalone/apps/web/.next/app-build-manifest.json +99 -99
  3. package/apps/web/.next/standalone/apps/web/.next/app-path-routes-manifest.json +28 -28
  4. package/apps/web/.next/standalone/apps/web/.next/build-manifest.json +2 -2
  5. package/apps/web/.next/standalone/apps/web/.next/cache/config.json +3 -3
  6. package/apps/web/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
  7. package/apps/web/.next/standalone/apps/web/.next/required-server-files.json +1 -1
  8. package/apps/web/.next/standalone/apps/web/.next/routes-manifest.json +1 -1
  9. package/apps/web/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  10. package/apps/web/.next/standalone/apps/web/.next/server/app/api/apps/cron/route_client-reference-manifest.js +1 -1
  11. package/apps/web/.next/standalone/apps/web/.next/server/app/api/apps/proxy/route_client-reference-manifest.js +1 -1
  12. package/apps/web/.next/standalone/apps/web/.next/server/app/api/apps/route_client-reference-manifest.js +1 -1
  13. package/apps/web/.next/standalone/apps/web/.next/server/app/api/apps/serve/[...path]/route_client-reference-manifest.js +1 -1
  14. package/apps/web/.next/standalone/apps/web/.next/server/app/api/apps/store/route_client-reference-manifest.js +1 -1
  15. package/apps/web/.next/standalone/apps/web/.next/server/app/api/apps/webhooks/[...path]/route_client-reference-manifest.js +1 -1
  16. package/apps/web/.next/standalone/apps/web/.next/server/app/api/chat/active/route_client-reference-manifest.js +1 -1
  17. package/apps/web/.next/standalone/apps/web/.next/server/app/api/chat/route.js +1 -1
  18. package/apps/web/.next/standalone/apps/web/.next/server/app/api/chat/route_client-reference-manifest.js +1 -1
  19. package/apps/web/.next/standalone/apps/web/.next/server/app/api/chat/runs/route_client-reference-manifest.js +1 -1
  20. package/apps/web/.next/standalone/apps/web/.next/server/app/api/chat/stop/route.js +1 -1
  21. package/apps/web/.next/standalone/apps/web/.next/server/app/api/chat/stop/route_client-reference-manifest.js +1 -1
  22. package/apps/web/.next/standalone/apps/web/.next/server/app/api/chat/stream/route_client-reference-manifest.js +1 -1
  23. package/apps/web/.next/standalone/apps/web/.next/server/app/api/chat/subagents/route_client-reference-manifest.js +1 -1
  24. package/apps/web/.next/standalone/apps/web/.next/server/app/api/cron/jobs/[jobId]/runs/route_client-reference-manifest.js +1 -1
  25. package/apps/web/.next/standalone/apps/web/.next/server/app/api/cron/jobs/route_client-reference-manifest.js +1 -1
  26. package/apps/web/.next/standalone/apps/web/.next/server/app/api/cron/runs/[sessionId]/route_client-reference-manifest.js +1 -1
  27. package/apps/web/.next/standalone/apps/web/.next/server/app/api/cron/runs/search-transcript/route_client-reference-manifest.js +1 -1
  28. package/apps/web/.next/standalone/apps/web/.next/server/app/api/feedback/route.js +1 -1
  29. package/apps/web/.next/standalone/apps/web/.next/server/app/api/feedback/route_client-reference-manifest.js +1 -1
  30. package/apps/web/.next/standalone/apps/web/.next/server/app/api/gateway/channels/route_client-reference-manifest.js +1 -1
  31. package/apps/web/.next/standalone/apps/web/.next/server/app/api/gateway/chat/route_client-reference-manifest.js +1 -1
  32. package/apps/web/.next/standalone/apps/web/.next/server/app/api/gateway/chat/stream/route_client-reference-manifest.js +1 -1
  33. package/apps/web/.next/standalone/apps/web/.next/server/app/api/gateway/sessions/[id]/route_client-reference-manifest.js +1 -1
  34. package/apps/web/.next/standalone/apps/web/.next/server/app/api/gateway/sessions/route_client-reference-manifest.js +1 -1
  35. package/apps/web/.next/standalone/apps/web/.next/server/app/api/memories/route_client-reference-manifest.js +1 -1
  36. package/apps/web/.next/standalone/apps/web/.next/server/app/api/profiles/route_client-reference-manifest.js +1 -1
  37. package/apps/web/.next/standalone/apps/web/.next/server/app/api/profiles/switch/route_client-reference-manifest.js +1 -1
  38. package/apps/web/.next/standalone/apps/web/.next/server/app/api/sessions/[sessionId]/route_client-reference-manifest.js +1 -1
  39. package/apps/web/.next/standalone/apps/web/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  40. package/apps/web/.next/standalone/apps/web/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  41. package/apps/web/.next/standalone/apps/web/.next/server/app/api/terminal/port/route_client-reference-manifest.js +1 -1
  42. package/apps/web/.next/standalone/apps/web/.next/server/app/api/web-sessions/[id]/messages/route_client-reference-manifest.js +1 -1
  43. package/apps/web/.next/standalone/apps/web/.next/server/app/api/web-sessions/[id]/route_client-reference-manifest.js +1 -1
  44. package/apps/web/.next/standalone/apps/web/.next/server/app/api/web-sessions/route.js +1 -1
  45. package/apps/web/.next/standalone/apps/web/.next/server/app/api/web-sessions/route_client-reference-manifest.js +1 -1
  46. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/assets/[...path]/route_client-reference-manifest.js +1 -1
  47. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/browse/route_client-reference-manifest.js +1 -1
  48. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/browse-file/route_client-reference-manifest.js +1 -1
  49. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/context/route_client-reference-manifest.js +1 -1
  50. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/copy/route_client-reference-manifest.js +1 -1
  51. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/db/introspect/route_client-reference-manifest.js +1 -1
  52. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/db/query/route_client-reference-manifest.js +1 -1
  53. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/delete/route.js +1 -1
  54. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/delete/route_client-reference-manifest.js +1 -1
  55. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/execute/route_client-reference-manifest.js +1 -1
  56. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/file/route_client-reference-manifest.js +1 -1
  57. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/init/route.js +1 -1
  58. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/init/route_client-reference-manifest.js +1 -1
  59. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/link-preview/route_client-reference-manifest.js +1 -1
  60. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/list/route_client-reference-manifest.js +1 -1
  61. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/mkdir/route_client-reference-manifest.js +1 -1
  62. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/move/route_client-reference-manifest.js +1 -1
  63. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/actions/route_client-reference-manifest.js +1 -1
  64. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/actions/runs/route_client-reference-manifest.js +1 -1
  65. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/display-field/route_client-reference-manifest.js +1 -1
  66. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/entries/[id]/content/route.js +46 -1
  67. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/entries/[id]/content/route_client-reference-manifest.js +1 -1
  68. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/entries/[id]/route_client-reference-manifest.js +1 -1
  69. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/entries/bulk-delete/route_client-reference-manifest.js +1 -1
  70. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/entries/options/route_client-reference-manifest.js +1 -1
  71. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/entries/route.js +1 -1
  72. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/entries/route_client-reference-manifest.js +1 -1
  73. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/fields/[fieldId]/enum-rename/route_client-reference-manifest.js +1 -1
  74. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/fields/[fieldId]/route_client-reference-manifest.js +1 -1
  75. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/fields/reorder/route_client-reference-manifest.js +1 -1
  76. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/fields/route_client-reference-manifest.js +1 -1
  77. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/route_client-reference-manifest.js +1 -1
  78. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/objects/[name]/views/route_client-reference-manifest.js +1 -1
  79. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/open-file/route_client-reference-manifest.js +1 -1
  80. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/path-info/route_client-reference-manifest.js +1 -1
  81. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/query/route_client-reference-manifest.js +1 -1
  82. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/raw-file/route_client-reference-manifest.js +1 -1
  83. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/rename/route_client-reference-manifest.js +1 -1
  84. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/reports/execute/route.js +1 -1
  85. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/reports/execute/route_client-reference-manifest.js +1 -1
  86. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/search-index/route_client-reference-manifest.js +1 -1
  87. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/suggest-files/route_client-reference-manifest.js +1 -1
  88. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/switch/route.js +1 -1
  89. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/switch/route_client-reference-manifest.js +1 -1
  90. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/thumbnail/route_client-reference-manifest.js +1 -1
  91. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/tree/route_client-reference-manifest.js +1 -1
  92. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/upload/route.js +1 -1
  93. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/upload/route_client-reference-manifest.js +1 -1
  94. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/virtual-file/route_client-reference-manifest.js +1 -1
  95. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/watch/route_client-reference-manifest.js +1 -1
  96. package/apps/web/.next/standalone/apps/web/.next/server/app/api/workspace/write-binary/route_client-reference-manifest.js +1 -1
  97. package/apps/web/.next/standalone/apps/web/.next/server/app/page.js +2 -2
  98. package/apps/web/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  99. package/apps/web/.next/standalone/apps/web/.next/server/app/workspace/page_client-reference-manifest.js +1 -1
  100. package/apps/web/.next/standalone/apps/web/.next/server/app-paths-manifest.json +28 -28
  101. package/apps/web/.next/standalone/apps/web/.next/server/chunks/6787.js +1 -1
  102. package/apps/web/.next/standalone/apps/web/.next/server/functions-config-manifest.json +3 -3
  103. package/apps/web/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  104. package/apps/web/.next/standalone/apps/web/.next/server/pages-manifest.json +1 -1
  105. package/apps/web/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
  106. package/apps/web/.next/standalone/apps/web/.next/static/chunks/app/{layout-f08988a91b85294a.js → layout-63a50b5e299be036.js} +1 -1
  107. package/apps/web/.next/standalone/apps/web/.next/static/chunks/app/page-95ba68825db8821d.js +1 -0
  108. package/apps/web/.next/standalone/apps/web/server.js +1 -1
  109. package/apps/web/.next/standalone/package.json +2 -1
  110. package/apps/web/.next/static/chunks/app/{layout-f08988a91b85294a.js → layout-63a50b5e299be036.js} +1 -1
  111. package/apps/web/.next/static/chunks/app/page-95ba68825db8821d.js +1 -0
  112. package/assets/seed/schema.sql +1 -0
  113. package/extensions/dench-identity/index.test.ts +136 -0
  114. package/extensions/dench-identity/index.ts +3 -2
  115. package/extensions/posthog-analytics/lib/build-env.js +1 -1
  116. package/extensions/vitest.config.ts +13 -0
  117. package/package.json +2 -1
  118. package/skills/crm/SKILL.md +48 -2
  119. package/skills/crm/documents/SKILL.md +211 -20
  120. package/skills/crm/duckdb-operations/SKILL.md +3 -0
  121. package/apps/web/.next/standalone/apps/web/.next/static/chunks/app/page-64cad81306c18be9.js +0 -1
  122. package/apps/web/.next/static/chunks/app/page-64cad81306c18be9.js +0 -1
  123. /package/apps/web/.next/standalone/apps/web/.next/static/{xzgogMMcs02a6BQa5tMBA → TdAnmJDH-qLpIRoeLSxFc}/_buildManifest.js +0 -0
  124. /package/apps/web/.next/standalone/apps/web/.next/static/{xzgogMMcs02a6BQa5tMBA → TdAnmJDH-qLpIRoeLSxFc}/_ssgManifest.js +0 -0
  125. /package/apps/web/.next/static/{xzgogMMcs02a6BQa5tMBA → TdAnmJDH-qLpIRoeLSxFc}/_buildManifest.js +0 -0
  126. /package/apps/web/.next/static/{xzgogMMcs02a6BQa5tMBA → TdAnmJDH-qLpIRoeLSxFc}/_ssgManifest.js +0 -0
@@ -83,6 +83,7 @@ CREATE TABLE IF NOT EXISTS documents (
83
83
  file_path VARCHAR NOT NULL UNIQUE,
84
84
  parent_id VARCHAR REFERENCES documents(id),
85
85
  parent_object_id VARCHAR REFERENCES objects(id),
86
+ entry_id VARCHAR REFERENCES entries(id),
86
87
  sort_order INTEGER DEFAULT 0,
87
88
  is_published BOOLEAN DEFAULT false,
88
89
  created_at TIMESTAMPTZ DEFAULT now(),
@@ -0,0 +1,136 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { buildIdentityPrompt, resolveWorkspaceDir } from "./index.ts";
3
+ import register from "./index.ts";
4
+ import path from "node:path";
5
+
6
+ describe("buildIdentityPrompt", () => {
7
+ const workspaceDir = "/home/user/workspace";
8
+
9
+ it("includes chat history path so agent can reference past conversations", () => {
10
+ const prompt = buildIdentityPrompt(workspaceDir);
11
+ expect(prompt).toContain(".openclaw/web-chat/");
12
+ expect(prompt).toContain(
13
+ path.join(workspaceDir, ".openclaw/web-chat/"),
14
+ );
15
+ });
16
+
17
+ it("includes all workspace context paths (prevents agent losing orientation)", () => {
18
+ const prompt = buildIdentityPrompt(workspaceDir);
19
+ expect(prompt).toContain(`**Root**: \`${workspaceDir}\``);
20
+ expect(prompt).toContain(path.join(workspaceDir, "workspace.duckdb"));
21
+ expect(prompt).toContain(path.join(workspaceDir, "skills"));
22
+ expect(prompt).toContain(path.join(workspaceDir, "apps"));
23
+ });
24
+
25
+ it("includes CRM skill path for delegation (prevents agent using wrong skill path)", () => {
26
+ const prompt = buildIdentityPrompt(workspaceDir);
27
+ expect(prompt).toContain(
28
+ path.join(workspaceDir, "skills", "crm", "SKILL.md"),
29
+ );
30
+ });
31
+
32
+ it("includes exec approval policy (prevents agent stalling on exec confirmation)", () => {
33
+ const prompt = buildIdentityPrompt(workspaceDir);
34
+ expect(prompt).toContain("elevated: true");
35
+ expect(prompt).toContain("automatically approved");
36
+ });
37
+
38
+ it("references DenchClaw branding, not OpenClaw (prevents identity confusion)", () => {
39
+ const prompt = buildIdentityPrompt(workspaceDir);
40
+ expect(prompt).toContain("You are **DenchClaw**");
41
+ expect(prompt).toContain("always use **DenchClaw** (not OpenClaw)");
42
+ });
43
+ });
44
+
45
+ describe("resolveWorkspaceDir", () => {
46
+ it("returns workspace path when config is a valid string", () => {
47
+ const api = { config: { agents: { defaults: { workspace: "/home/user/ws" } } } };
48
+ expect(resolveWorkspaceDir(api)).toBe("/home/user/ws");
49
+ });
50
+
51
+ it("returns undefined when api is null (prevents crash on missing config)", () => {
52
+ expect(resolveWorkspaceDir(null)).toBeUndefined();
53
+ });
54
+
55
+ it("returns undefined when api is undefined (prevents crash on missing config)", () => {
56
+ expect(resolveWorkspaceDir(undefined)).toBeUndefined();
57
+ });
58
+
59
+ it("returns undefined when config chain is missing (prevents crash on partial config)", () => {
60
+ expect(resolveWorkspaceDir({})).toBeUndefined();
61
+ expect(resolveWorkspaceDir({ config: {} })).toBeUndefined();
62
+ expect(resolveWorkspaceDir({ config: { agents: {} } })).toBeUndefined();
63
+ expect(resolveWorkspaceDir({ config: { agents: { defaults: {} } } })).toBeUndefined();
64
+ });
65
+
66
+ it("returns undefined when workspace is empty string (prevents empty path injection)", () => {
67
+ const api = { config: { agents: { defaults: { workspace: "" } } } };
68
+ expect(resolveWorkspaceDir(api)).toBeUndefined();
69
+ });
70
+
71
+ it("returns undefined when workspace is whitespace-only (prevents whitespace path injection)", () => {
72
+ const api = { config: { agents: { defaults: { workspace: " " } } } };
73
+ expect(resolveWorkspaceDir(api)).toBeUndefined();
74
+ });
75
+
76
+ it("returns undefined when workspace is not a string (prevents type coercion)", () => {
77
+ expect(resolveWorkspaceDir({ config: { agents: { defaults: { workspace: 42 } } } })).toBeUndefined();
78
+ expect(resolveWorkspaceDir({ config: { agents: { defaults: { workspace: true } } } })).toBeUndefined();
79
+ expect(resolveWorkspaceDir({ config: { agents: { defaults: { workspace: null } } } })).toBeUndefined();
80
+ });
81
+
82
+ it("trims leading/trailing whitespace from valid paths", () => {
83
+ const api = { config: { agents: { defaults: { workspace: " /home/user/ws " } } } };
84
+ expect(resolveWorkspaceDir(api)).toBe("/home/user/ws");
85
+ });
86
+ });
87
+
88
+ describe("register", () => {
89
+ it("hooks into before_prompt_build event when enabled", () => {
90
+ const api = {
91
+ config: { plugins: { entries: {} }, agents: { defaults: { workspace: "/ws" } } },
92
+ on: vi.fn(),
93
+ };
94
+ register(api);
95
+ expect(api.on).toHaveBeenCalledWith(
96
+ "before_prompt_build",
97
+ expect.any(Function),
98
+ { priority: 100 },
99
+ );
100
+ });
101
+
102
+ it("does not register handler when plugin is explicitly disabled (respects config)", () => {
103
+ const api = {
104
+ config: { plugins: { entries: { "dench-identity": { config: { enabled: false } } } } },
105
+ on: vi.fn(),
106
+ };
107
+ register(api);
108
+ expect(api.on).not.toHaveBeenCalled();
109
+ });
110
+
111
+ it("handler returns system context when workspace is configured", () => {
112
+ const api = {
113
+ config: { plugins: { entries: {} }, agents: { defaults: { workspace: "/ws" } } },
114
+ on: vi.fn(),
115
+ };
116
+ register(api);
117
+
118
+ const handler = api.on.mock.calls[0][1];
119
+ const result = handler({}, {});
120
+ expect(result).toEqual({
121
+ prependSystemContext: expect.stringContaining("DenchClaw"),
122
+ });
123
+ });
124
+
125
+ it("handler returns undefined when workspace is not configured (prevents empty prompt)", () => {
126
+ const api = {
127
+ config: { plugins: { entries: {} }, agents: { defaults: {} } },
128
+ on: vi.fn(),
129
+ };
130
+ register(api);
131
+
132
+ const handler = api.on.mock.calls[0][1];
133
+ const result = handler({}, {});
134
+ expect(result).toBeUndefined();
135
+ });
136
+ });
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
 
3
3
  export const id = "dench-identity";
4
4
 
5
- function buildIdentityPrompt(workspaceDir: string): string {
5
+ export function buildIdentityPrompt(workspaceDir: string): string {
6
6
  const skillsDir = path.join(workspaceDir, "skills");
7
7
  const crmSkillPath = path.join(skillsDir, "crm", "SKILL.md");
8
8
  const browserSkillPath = path.join(skillsDir, "browser", "SKILL.md");
@@ -94,6 +94,7 @@ For multi-session projects, write a session handoff summary to \`${workspaceDir}
94
94
  - **Skills**: \`${skillsDir}\` — scan this directory for all available skills; new skills may be installed at any time
95
95
  - **Apps**: \`${appsDir}\` — \`.dench.app\` folders with \`.dench.yaml\` manifests
96
96
  - **Exec Approvals**: All commands are automatically approved by default. All exec calls must be elevated: true.
97
+ - **Chat History**: All chat history is stored in \`${workspaceDir}/.openclaw/web-chat/\`. This is where you can find all your past conversations and the context for the current session.
97
98
 
98
99
  ## Links
99
100
 
@@ -102,7 +103,7 @@ For multi-session projects, write a session handoff summary to \`${workspaceDir}
102
103
  - Skills Store: https://skills.sh`;
103
104
  }
104
105
 
105
- function resolveWorkspaceDir(api: any): string | undefined {
106
+ export function resolveWorkspaceDir(api: any): string | undefined {
106
107
  const ws = api?.config?.agents?.defaults?.workspace;
107
108
  return typeof ws === "string" ? ws.trim() || undefined : undefined;
108
109
  }
@@ -1,3 +1,3 @@
1
1
  export const POSTHOG_KEY = "phc_Nt8s1Ny8062TY9aKT8LWiB4nBuoHNYQXRU9MypWfQCW";
2
- export const DENCHCLAW_VERSION = "2.4.0";
2
+ export const DENCHCLAW_VERSION = "2.4.2";
3
3
  export const OPENCLAW_VERSION = "";
@@ -0,0 +1,13 @@
1
+ import baseConfig from "../vitest.config.ts";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ const base = baseConfig as unknown as Record<string, unknown>;
5
+ const baseTest = (baseConfig as { test?: Record<string, unknown> }).test ?? {};
6
+
7
+ export default defineConfig({
8
+ ...base,
9
+ test: {
10
+ ...baseTest,
11
+ include: ["extensions/**/*.test.ts"],
12
+ },
13
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "denchclaw",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "Fully Managed OpenClaw Framework for managing your CRM, Sales Automation and Outreach agents. The only local productivity tool you need.",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/DenchHQ/DenchClaw#readme",
@@ -62,6 +62,7 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@clack/prompts": "^1.0.1",
65
+ "axios": "^1.13.5",
65
66
  "chalk": "^5.6.2",
66
67
  "commander": "^14.0.3",
67
68
  "gradient-string": "^3.0.0",
@@ -95,6 +95,51 @@ Never rename partially. If you can't complete all steps, don't start the rename
95
95
  - **DuckDB SQL errors**: See the "Common DuckDB Pitfalls" section in the **duckdb-operations** child skill. The most frequent causes: unquoted field names with spaces (use `"Full Name"` not `Full Name`), wrong transaction syntax (`BEGIN TRANSACTION` not `BEGIN`), unescaped single quotes, and PIVOT views without the `IN (...)` field list.
96
96
  - **"Script not found" on action buttons**: The `.actions/` directory and/or script files were not created. See the **actions** child skill post-creation checklist.
97
97
 
98
+ ## Relation Fields — Link Objects Automatically
99
+
100
+ **Every time you create or modify objects, check whether relation fields should connect them. Do NOT wait for the user to ask — proactively link objects that are obviously related.**
101
+
102
+ Relation fields (`type: "relation"`) let an entry in one object reference an entry in another. They render as a searchable dropdown in the UI and are the backbone of any useful CRM.
103
+
104
+ ### Mandatory auto-linking rules
105
+
106
+ When you create an object, scan all existing objects (run `SELECT name FROM objects`) and add relation fields for every obvious link:
107
+
108
+ | If you're creating... | And this object exists... | Auto-create this relation field |
109
+ |---|---|---|
110
+ | people / contact | company | "Company" on people → company (`many_to_one`) |
111
+ | lead / prospect | company | "Company" on lead → company (`many_to_one`) |
112
+ | deal / opportunity | people | "Primary Contact" on deal → people (`many_to_one`) |
113
+ | deal / opportunity | company | "Company" on deal → company (`many_to_one`) |
114
+ | task | people | "Assigned Contact" on task → people (`many_to_one`) |
115
+ | task | project / deal | "Related To" on task → parent (`many_to_one`) |
116
+ | case | people or company | "Client" on case → people/company (`many_to_one`) |
117
+ | invoice / payment | company | "Company" on invoice → company (`many_to_one`) |
118
+ | invoice / payment | deal | "Deal" on invoice → deal (`many_to_one`) |
119
+ | property / listing | people | "Agent" on property → people (`many_to_one`) |
120
+ | any child concept | its parent concept | relation to parent (`many_to_one`) |
121
+
122
+ **Use this SQL pattern** (safe — does nothing if the target object doesn't exist):
123
+
124
+ ```sql
125
+ INSERT INTO fields (object_id, name, type, related_object_id, relationship_type, sort_order)
126
+ SELECT
127
+ (SELECT id FROM objects WHERE name = 'people'),
128
+ 'Company',
129
+ 'relation',
130
+ (SELECT id FROM objects WHERE name = 'company'),
131
+ 'many_to_one',
132
+ 3
133
+ WHERE EXISTS (SELECT 1 FROM objects WHERE name = 'company')
134
+ ON CONFLICT (object_id, name) DO NOTHING;
135
+ ```
136
+
137
+ **General rule**: If you're creating object B and object A already exists, ask: "Would an entry in B logically belong to or reference an entry in A?" If yes, add a `many_to_one` relation. Use `many_to_many` only when an entry can link to multiple entries (e.g. project → team members).
138
+
139
+ Relation fields **must be created via SQL** — the API does not support the `relation` type.
140
+
141
+ ---
142
+
98
143
  ## Critical Reminders
99
144
 
100
145
  - Handle the ENTIRE CRM operation from analysis to SQL execution to filesystem projection to summary
@@ -107,10 +152,11 @@ Never rename partially. If you can't complete all steps, don't start the rename
107
152
  - Extract ALL data from user messages — don't leave information unused
108
153
  - **REPORTS vs DOCUMENTS**: When the user asks for "reports", "analytics", "charts", "graphs", "metrics", "insights", or "breakdown" — use `.report.json` format (see **reports** child skill), NOT markdown. Only use markdown `.md` for SOPs, guides, notes, and prose documents.
109
154
  - **INLINE CHART ARTIFACTS**: When answering analytics questions in chat, ALWAYS emit a `report-json` fenced code block so the UI renders interactive charts inline.
110
- - **NOTES**: Always use type "richtext" for Notes fields
155
+ - **NOTES FIELD vs ENTRY DOCUMENTS**: These are different. `Notes` is a DuckDB `richtext` field stored in `entry_fields`. Entry documents are markdown files linked through the `documents` table. When the user asks for long-form notes, draft emails, writeups, or entry pages, default to entry documents unless they explicitly say "update the Notes field/column".
156
+ - **ENTRY DOCUMENTS**: New entry detail pages should use human-readable filenames (for example `yt-mikemurphy-001.md`, `jane-smith-001.md`, `acme-corp-001.md`) and MUST be registered in DuckDB `documents` with `file_path`, `parent_object_id`, and `entry_id`. Do NOT default to raw `{entry_id}.md` filenames for new docs. See **documents** child skill.
111
157
  - **USER FIELDS**: Resolve member name to ID from `workspace_context.yaml` BEFORE inserting
112
158
  - **ENUM FIELDS**: Use type "enum" with `enum_values` JSON array
113
- - **RELATION FIELDS (PROACTIVE)**: Use type "relation" with `related_object_id` and `relationship_type`. **ALWAYS create relation fields when objects are obviously linked** — don't wait for the user to ask. Examples: people→company, deal→contact, deal→company, task→project, case→client, invoice→company. If you're creating object B and object A exists, ask: "Would B entries reference A entries?" If yes, add a relation. Relation fields must be created via SQL (not the API). See **object-builder** child skill for the full relation pattern and common relation pairs.
159
+ - **RELATION FIELDS (PROACTIVE SEE SECTION ABOVE)**: ALWAYS create relation fields when objects are obviously linked — don't wait for the user to ask. See the **"Relation Fields Link Objects Automatically"** section above for the full table of mandatory auto-linking rules and the SQL pattern.
114
160
  - **TAGS FIELDS**: Use type "tags" for free-form string arrays. Value stored as `'["tag1","tag2"]'`
115
161
  - **URL FIELDS**: Use type "url" for website addresses and links
116
162
  - **FILE FIELDS**: Use type "file" for file attachments (stores file path or URL)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: documents
3
- description: Document management with markdown files, cross-nesting between documents and objects, and optional per-entry detail pages.
3
+ description: Document management with markdown files, cross-nesting between documents and objects, and human-readable entry detail pages linked through the documents table.
4
4
  metadata: { "openclaw": { "inject": true, "always": true, "emoji": "📄" } }
5
5
  ---
6
6
 
@@ -31,34 +31,225 @@ VALUES ('Roadmap', 'map', 'projects/roadmap.md', '<parent_doc_id>', 0);
31
31
 
32
32
  ---
33
33
 
34
- ## Entry Detail Pages (Optional Markdown Files)
34
+ ## Notes Field vs Entry Documents
35
35
 
36
- Each entry in an object can have an optional markdown file that acts as a detail page (like a Notion row page). The file is stored inside the object's directory at `{{WORKSPACE_PATH}}/{object_name}/{entry_id}.md`.
36
+ These are **not the same thing**:
37
37
 
38
- **Key rules:**
38
+ - **`Notes` field**: a `richtext` value stored in DuckDB `entry_fields`
39
+ - **Entry document**: a standalone `.md` file on disk, linked to the entry through the `documents` table
39
40
 
40
- - Entry `.md` files are **optional** — they are only created when the user writes content via the entry detail panel in the UI.
41
- - You do NOT need to create `.md` files for every entry during seeding. Only create them when you want to add detailed notes, descriptions, or documentation for a specific entry.
42
- - The UI automatically shows an empty markdown editor for entries without a `.md` file, and creates the file on first write.
43
- - Entry `.md` files are plain markdown (no frontmatter required). The filename must be exactly `{entry_id}.md`.
41
+ When the user says:
44
42
 
45
- **When to create entry `.md` files during seeding:**
43
+ - "fill in notes for each entry"
44
+ - "write descriptions for all rows"
45
+ - "add detailed writeups"
46
+ - "put the outreach draft into each influencer notes page"
47
+ - "create entry docs"
46
48
 
47
- - For entries that have rich descriptions or meeting notes
48
- - For entries that need SOPs, playbooks, or documentation attached
49
- - For key entries like important leads, major projects, or critical tasks
49
+ default to **entry documents** unless they explicitly say "update the Notes column/field".
50
50
 
51
- **Example:**
51
+ If the user truly means the `Notes` field, they will usually talk about:
52
+
53
+ - a table column
54
+ - richtext field values
55
+ - filtering/sorting by Notes
56
+ - SQL updates to `entry_fields`
57
+
58
+ If they mean entry documents, they are talking about:
59
+
60
+ - markdown pages
61
+ - the entry detail panel
62
+ - prose content, drafts, meeting notes, SOPs, long-form notes
63
+ - files visible under the object in the sidebar
64
+
65
+ ---
66
+
67
+ ## Entry Detail Pages
68
+
69
+ Each entry in an object can have an optional markdown file that acts as its detail page in the entry panel.
70
+
71
+ **CRITICAL:** New entry documents should use **human-readable filenames**, not raw UUID filenames. The file must also be registered in DuckDB `documents` with `entry_id`, `parent_object_id`, and `file_path`.
72
+
73
+ ### Required storage model
74
+
75
+ For every entry document:
76
+
77
+ 1. Write a human-readable markdown file inside the object directory
78
+ 2. Insert/update a row in `documents` linking the entry to that file
79
+
80
+ ```sql
81
+ INSERT INTO documents (title, file_path, parent_object_id, entry_id)
82
+ VALUES (
83
+ 'Mike Murphy',
84
+ 'marketing/influencer/yt-mikemurphy-001.md',
85
+ (SELECT id FROM objects WHERE name = 'influencer'),
86
+ 'yt-mikemurphy-001'
87
+ );
88
+ ```
89
+
90
+ ### Naming convention (MANDATORY)
91
+
92
+ Use this filename structure for entry documents:
93
+
94
+ `{human_readable_slug}-{sequence}.md`
95
+
96
+ Examples:
97
+
98
+ - `acme-corp-001.md`
99
+ - `jane-smith-001.md`
100
+ - `q2-renewal-deal-001.md`
101
+
102
+ If the entry clearly belongs to a source/domain where a prefix helps, include it:
103
+
104
+ - `yt-mikemurphy-001.md` for YouTube creators
105
+ - `x-somehandle-001.md` for X/Twitter creators
106
+
107
+ ### How to choose the slug
108
+
109
+ Use the first strong human-readable identifier available:
110
+
111
+ 1. `Document Slug`, `Slug`, or `File Slug` field if it exists
112
+ 2. For YouTube creators: extract the handle from `YouTube URL` and prefix `yt-`
113
+ 3. Otherwise use the primary text label, such as:
114
+ - `Title`
115
+ - `Channel Name`
116
+ - `Creator Name`
117
+ - `Full Name`
118
+ - `Name`
119
+ - `Company Name`
120
+ - `Deal Name`
121
+ - `Case Number`
122
+ - `Invoice Number`
123
+
124
+ Examples:
125
+
126
+ - `https://www.youtube.com/@MikeMurphy` -> `yt-mikemurphy-001.md`
127
+ - `Creator Name = Jane Smith` -> `jane-smith-001.md`
128
+ - `Company Name = Acme Corp` -> `acme-corp-001.md`
129
+
130
+ ### NEVER do these
131
+
132
+ - Do **NOT** default to `{entry_id}.md` for new documents
133
+ - Do **NOT** confuse entry documents with the `Notes` richtext field
134
+ - Do **NOT** create a markdown file without also inserting/updating the `documents` table row
135
+ - Do **NOT** write human-readable files and leave them orphaned from metadata
136
+
137
+ ### Backward compatibility
138
+
139
+ Older workspaces may still have legacy `{entry_id}.md` files. Those can continue to work, but **new** entry documents should follow the human-readable naming convention above.
140
+
141
+ ---
142
+
143
+ ## Creating One Entry Document
144
+
145
+ Only create entry `.md` files when the user wants detailed prose for a specific entry or group of entries.
146
+
147
+ Example:
52
148
 
53
149
  ```bash
54
- # Only if you want to add detailed notes for this specific entry
55
- cat > {{WORKSPACE_PATH}}/lead/abc123.md << 'MD'
56
- # Meeting Notes - Acme Corp
150
+ cat > {{WORKSPACE_PATH}}/marketing/influencer/yt-mikemurphy-001.md << 'MD'
151
+ # Draft Outreach Email
152
+
153
+ To: hello@mikemurphy.co
154
+ Subject: Partnership idea
57
155
 
58
- Met with John on 2026-03-15. Key takeaways:
156
+ Hi Mike,
59
157
 
60
- - Interested in enterprise plan
61
- - Budget approved for Q2
62
- - Follow up next week with pricing proposal
158
+ I loved your AI Handyman breakdowns. DenchClaw is launching a workflow-native AI platform for builders who want serious control over execution, memory, and automation.
159
+
160
+ Would you be open to testing it and discussing a possible sponsorship?
63
161
  MD
64
162
  ```
163
+
164
+ Then register it:
165
+
166
+ ```sql
167
+ INSERT INTO documents (title, file_path, parent_object_id, entry_id)
168
+ VALUES (
169
+ 'Mike Murphy',
170
+ 'marketing/influencer/yt-mikemurphy-001.md',
171
+ (SELECT id FROM objects WHERE name = 'influencer'),
172
+ 'yt-mikemurphy-001'
173
+ )
174
+ ON CONFLICT (file_path) DO UPDATE
175
+ SET title = excluded.title,
176
+ parent_object_id = excluded.parent_object_id,
177
+ entry_id = excluded.entry_id,
178
+ updated_at = now();
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Batch Creating Entry Documents
184
+
185
+ When the user asks for docs for many entries, do **not** update the `Notes` field in SQL. Create markdown files plus `documents` rows.
186
+
187
+ ### Workflow
188
+
189
+ 1. Query entries and the fields needed to build filenames/titles
190
+ 2. Derive a human-readable filename for each entry
191
+ 3. Write one `.md` file per entry under the object directory
192
+ 4. Insert/update one `documents` row per file
193
+
194
+ ### Example: create docs for every influencer
195
+
196
+ ```bash
197
+ duckdb {{WORKSPACE_PATH}}/workspace.duckdb -json "
198
+ SELECT
199
+ entry_id,
200
+ \"Creator Name\",
201
+ \"Channel Name\",
202
+ \"YouTube URL\"
203
+ FROM v_influencer
204
+ ORDER BY \"Creator Name\"
205
+ "
206
+ ```
207
+
208
+ Then for each row:
209
+
210
+ ```bash
211
+ # Example row:
212
+ # entry_id = yt-mikemurphy-001
213
+ # youtube url = https://www.youtube.com/@MikeMurphy
214
+
215
+ cat > {{WORKSPACE_PATH}}/marketing/influencer/yt-mikemurphy-001.md << 'MD'
216
+ # Influencer Notes
217
+
218
+ ## Outreach draft
219
+
220
+ ...
221
+ MD
222
+ ```
223
+
224
+ Register each file:
225
+
226
+ ```sql
227
+ INSERT INTO documents (title, file_path, parent_object_id, entry_id)
228
+ VALUES (
229
+ 'Mike Murphy',
230
+ 'marketing/influencer/yt-mikemurphy-001.md',
231
+ (SELECT id FROM objects WHERE name = 'influencer'),
232
+ 'yt-mikemurphy-001'
233
+ )
234
+ ON CONFLICT (file_path) DO UPDATE
235
+ SET title = excluded.title,
236
+ parent_object_id = excluded.parent_object_id,
237
+ entry_id = excluded.entry_id,
238
+ updated_at = now();
239
+ ```
240
+
241
+ ---
242
+
243
+ ## Standalone Documents vs Entry Documents
244
+
245
+ Not every markdown file under an object directory is automatically an entry document.
246
+
247
+ - **Entry document**: has a corresponding `documents` row with `entry_id` set
248
+ - **Standalone document under an object**: has no `entry_id` link; it is just a nested document in that folder
249
+
250
+ Examples:
251
+
252
+ - `marketing/influencer/yt-mikemurphy-001.md` with `documents.entry_id = 'yt-mikemurphy-001'` -> entry document
253
+ - `marketing/influencer/outreach-playbook.md` with no `entry_id` -> standalone object-level document
254
+
255
+ If a markdown file is meant to be the entry page, always register it in `documents`.
@@ -96,6 +96,7 @@ CREATE TABLE IF NOT EXISTS documents (
96
96
  file_path VARCHAR NOT NULL UNIQUE,
97
97
  parent_id VARCHAR REFERENCES documents(id),
98
98
  parent_object_id VARCHAR REFERENCES objects(id),
99
+ entry_id VARCHAR REFERENCES entries(id),
99
100
  sort_order INTEGER DEFAULT 0,
100
101
  is_published BOOLEAN DEFAULT false,
101
102
  created_at TIMESTAMPTZ DEFAULT now(),
@@ -122,6 +123,8 @@ CREATE TABLE IF NOT EXISTS action_runs (
122
123
 
123
124
  All ID columns default to `gen_random_uuid()::VARCHAR`. You can also use `nanoid32()` if the macro is loaded. When inserting manually, always generate an ID — never use a placeholder.
124
125
 
126
+ For entry detail markdown pages, `documents.entry_id` links a document row to a specific CRM entry while `file_path` stores the human-readable markdown path (for example `marketing/influencer/yt-mikemurphy-001.md`).
127
+
125
128
  ---
126
129
 
127
130
  ## Auto-Generated PIVOT Views