🚀 RELEASE: Ironclaw self-aware / fix inject context / use main sessions / seed databases
This commit is contained in:
parent
4995eb52fe
commit
37f5d255c0
@ -3,7 +3,7 @@ import { join } from "node:path";
|
||||
import {
|
||||
resolveWorkspaceRoot,
|
||||
parseSimpleYaml,
|
||||
duckdbQuery,
|
||||
duckdbQueryAsync,
|
||||
duckdbPath,
|
||||
isDatabaseFile,
|
||||
} from "@/lib/workspace";
|
||||
@ -161,15 +161,15 @@ function flattenTree(
|
||||
}
|
||||
|
||||
/** Fetch all entries from all objects and produce search items. */
|
||||
function buildEntryItems(): SearchIndexItem[] {
|
||||
async function buildEntryItems(): Promise<SearchIndexItem[]> {
|
||||
const items: SearchIndexItem[] = [];
|
||||
|
||||
const objects = duckdbQuery<ObjectRow>(
|
||||
const objects = await duckdbQueryAsync<ObjectRow>(
|
||||
"SELECT * FROM objects ORDER BY name",
|
||||
);
|
||||
|
||||
for (const obj of objects) {
|
||||
const fields = duckdbQuery<FieldRow>(
|
||||
const fields = await duckdbQueryAsync<FieldRow>(
|
||||
`SELECT * FROM fields WHERE object_id = '${sqlEscape(obj.id)}' ORDER BY sort_order`,
|
||||
);
|
||||
const displayField = resolveDisplayField(obj, fields);
|
||||
@ -179,12 +179,12 @@ function buildEntryItems(): SearchIndexItem[] {
|
||||
.slice(0, 4);
|
||||
|
||||
// Try PIVOT view first, then raw EAV
|
||||
let entries: Record<string, unknown>[] = duckdbQuery(
|
||||
let entries: Record<string, unknown>[] = await duckdbQueryAsync(
|
||||
`SELECT * FROM v_${obj.name} ORDER BY created_at DESC LIMIT 500`,
|
||||
);
|
||||
|
||||
if (entries.length === 0) {
|
||||
const rawRows = duckdbQuery<EavRow>(
|
||||
const rawRows = await duckdbQueryAsync<EavRow>(
|
||||
`SELECT e.id as entry_id, e.created_at, e.updated_at,
|
||||
f.name as field_name, ef.value
|
||||
FROM entries e
|
||||
@ -247,7 +247,7 @@ export async function GET() {
|
||||
if (root) {
|
||||
const dbObjects = new Map<string, ObjectRow>();
|
||||
if (duckdbPath()) {
|
||||
const objs = duckdbQuery<ObjectRow>(
|
||||
const objs = await duckdbQueryAsync<ObjectRow>(
|
||||
"SELECT * FROM objects",
|
||||
);
|
||||
for (const o of objs) {dbObjects.set(o.name, o);}
|
||||
@ -259,7 +259,7 @@ export async function GET() {
|
||||
|
||||
// 2. Entries from all objects
|
||||
if (duckdbPath()) {
|
||||
items.push(...buildEntryItems());
|
||||
items.push(...await buildEntryItems());
|
||||
}
|
||||
|
||||
return Response.json({ items });
|
||||
|
||||
@ -241,9 +241,11 @@ describe("agent-runner", () => {
|
||||
"node",
|
||||
expect.arrayContaining([
|
||||
"--session-key",
|
||||
"agent:main:subagent:session-123",
|
||||
"agent:main:web:session-123",
|
||||
"--lane",
|
||||
"subagent",
|
||||
"web",
|
||||
"--channel",
|
||||
"webchat",
|
||||
]),
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
@ -105,7 +105,7 @@ export function extractToolResult(
|
||||
}
|
||||
|
||||
export type RunAgentOptions = {
|
||||
/** When set, the agent runs in an isolated session (e.g. file-scoped subagent). */
|
||||
/** When set, the agent runs in an isolated web chat session. */
|
||||
sessionId?: string;
|
||||
};
|
||||
|
||||
@ -180,8 +180,8 @@ export function spawnAgentProcess(
|
||||
];
|
||||
|
||||
if (agentSessionId) {
|
||||
const sessionKey = `agent:main:subagent:${agentSessionId}`;
|
||||
args.push("--session-key", sessionKey, "--lane", "subagent");
|
||||
const sessionKey = `agent:main:web:${agentSessionId}`;
|
||||
args.push("--session-key", sessionKey, "--lane", "web", "--channel", "webchat");
|
||||
}
|
||||
|
||||
return spawn("node", args, {
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { execSync } from "node:child_process";
|
||||
import { execSync, exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { join, resolve, normalize, relative } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* Resolve the workspace directory, checking in order:
|
||||
* 1. OPENCLAW_WORKSPACE env var
|
||||
@ -88,6 +91,9 @@ export function resolveDuckdbBin(): string | null {
|
||||
/**
|
||||
* Execute a DuckDB query and return parsed JSON rows.
|
||||
* Uses the duckdb CLI with -json output format.
|
||||
*
|
||||
* @deprecated Prefer `duckdbQueryAsync` in server route handlers to avoid
|
||||
* blocking the Node.js event loop (which freezes the standalone server).
|
||||
*/
|
||||
export function duckdbQuery<T = Record<string, unknown>>(
|
||||
sql: string,
|
||||
@ -116,6 +122,37 @@ export function duckdbQuery<T = Record<string, unknown>>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of duckdbQuery — does not block the event loop.
|
||||
* Always prefer this in Next.js route handlers (especially the standalone build
|
||||
* which is single-threaded; a blocking execSync freezes the entire server).
|
||||
*/
|
||||
export async function duckdbQueryAsync<T = Record<string, unknown>>(
|
||||
sql: string,
|
||||
): Promise<T[]> {
|
||||
const db = duckdbPath();
|
||||
if (!db) {return [];}
|
||||
|
||||
const bin = resolveDuckdbBin();
|
||||
if (!bin) {return [];}
|
||||
|
||||
try {
|
||||
const escapedSql = sql.replace(/'/g, "'\\''");
|
||||
const { stdout } = await execAsync(`'${bin}' -json '${db}' '${escapedSql}'`, {
|
||||
encoding: "utf-8",
|
||||
timeout: 10_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
shell: "/bin/sh",
|
||||
});
|
||||
|
||||
const trimmed = stdout.trim();
|
||||
if (!trimmed || trimmed === "[]") {return [];}
|
||||
return JSON.parse(trimmed) as T[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a DuckDB statement (no JSON output expected).
|
||||
* Used for INSERT/UPDATE/ALTER operations.
|
||||
@ -180,6 +217,8 @@ export function isDatabaseFile(filename: string): boolean {
|
||||
/**
|
||||
* Execute a DuckDB query against an arbitrary database file and return parsed JSON rows.
|
||||
* This is used by the database viewer to introspect any .duckdb/.sqlite/.db file.
|
||||
*
|
||||
* @deprecated Prefer `duckdbQueryOnFileAsync` in route handlers.
|
||||
*/
|
||||
export function duckdbQueryOnFile<T = Record<string, unknown>>(
|
||||
dbFilePath: string,
|
||||
@ -205,6 +244,31 @@ export function duckdbQueryOnFile<T = Record<string, unknown>>(
|
||||
}
|
||||
}
|
||||
|
||||
/** Async version of duckdbQueryOnFile — does not block the event loop. */
|
||||
export async function duckdbQueryOnFileAsync<T = Record<string, unknown>>(
|
||||
dbFilePath: string,
|
||||
sql: string,
|
||||
): Promise<T[]> {
|
||||
const bin = resolveDuckdbBin();
|
||||
if (!bin) {return [];}
|
||||
|
||||
try {
|
||||
const escapedSql = sql.replace(/'/g, "'\\''");
|
||||
const { stdout } = await execAsync(`'${bin}' -json '${dbFilePath}' '${escapedSql}'`, {
|
||||
encoding: "utf-8",
|
||||
timeout: 15_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
shell: "/bin/sh",
|
||||
});
|
||||
|
||||
const trimmed = stdout.trim();
|
||||
if (!trimmed || trimmed === "[]") {return [];}
|
||||
return JSON.parse(trimmed) as T[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and resolve a path within the workspace.
|
||||
* Prevents path traversal by ensuring the resolved path stays within root.
|
||||
|
||||
File diff suppressed because one or more lines are too long
272
assets/seed/schema.sql
Normal file
272
assets/seed/schema.sql
Normal file
@ -0,0 +1,272 @@
|
||||
-- OpenClaw workspace seed schema + sample data
|
||||
-- Used to pre-build workspace.duckdb for new workspace onboarding.
|
||||
|
||||
-- ── nanoid32 macro ──
|
||||
CREATE OR REPLACE MACRO nanoid32() AS (
|
||||
SELECT string_agg(
|
||||
substr('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-',
|
||||
(floor(random() * 64) + 1)::int, 1), '')
|
||||
FROM generate_series(1, 32)
|
||||
);
|
||||
|
||||
-- ── Core tables ──
|
||||
|
||||
CREATE TABLE IF NOT EXISTS objects (
|
||||
id VARCHAR PRIMARY KEY DEFAULT (gen_random_uuid()::VARCHAR),
|
||||
name VARCHAR NOT NULL,
|
||||
description VARCHAR,
|
||||
icon VARCHAR,
|
||||
default_view VARCHAR DEFAULT 'table',
|
||||
parent_document_id VARCHAR,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
source_app VARCHAR,
|
||||
immutable BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
UNIQUE(name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fields (
|
||||
id VARCHAR PRIMARY KEY DEFAULT (gen_random_uuid()::VARCHAR),
|
||||
object_id VARCHAR NOT NULL REFERENCES objects(id),
|
||||
name VARCHAR NOT NULL,
|
||||
description VARCHAR,
|
||||
type VARCHAR NOT NULL,
|
||||
required BOOLEAN DEFAULT false,
|
||||
default_value VARCHAR,
|
||||
related_object_id VARCHAR REFERENCES objects(id),
|
||||
relationship_type VARCHAR,
|
||||
enum_values JSON,
|
||||
enum_colors JSON,
|
||||
enum_multiple BOOLEAN DEFAULT false,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
UNIQUE(object_id, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS entries (
|
||||
id VARCHAR PRIMARY KEY DEFAULT (gen_random_uuid()::VARCHAR),
|
||||
object_id VARCHAR NOT NULL REFERENCES objects(id),
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS entry_fields (
|
||||
id VARCHAR PRIMARY KEY DEFAULT (gen_random_uuid()::VARCHAR),
|
||||
entry_id VARCHAR NOT NULL REFERENCES entries(id),
|
||||
field_id VARCHAR NOT NULL REFERENCES fields(id),
|
||||
value VARCHAR,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
UNIQUE(entry_id, field_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS statuses (
|
||||
id VARCHAR PRIMARY KEY DEFAULT (gen_random_uuid()::VARCHAR),
|
||||
object_id VARCHAR NOT NULL REFERENCES objects(id),
|
||||
name VARCHAR NOT NULL,
|
||||
color VARCHAR DEFAULT '#94a3b8',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_default BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
UNIQUE(object_id, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
id VARCHAR PRIMARY KEY DEFAULT (gen_random_uuid()::VARCHAR),
|
||||
title VARCHAR DEFAULT 'Untitled',
|
||||
icon VARCHAR,
|
||||
cover_image VARCHAR,
|
||||
file_path VARCHAR NOT NULL UNIQUE,
|
||||
parent_id VARCHAR REFERENCES documents(id),
|
||||
parent_object_id VARCHAR REFERENCES objects(id),
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_published BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── Seed: people ──
|
||||
|
||||
INSERT INTO objects (id, name, description, icon, default_view, immutable, sort_order)
|
||||
VALUES ('seed_obj_people_00000000000000', 'people', 'Contact management', 'users', 'table', true, 0);
|
||||
|
||||
INSERT INTO fields (id, object_id, name, type, required, sort_order) VALUES
|
||||
('seed_fld_people_fullname_000000', 'seed_obj_people_00000000000000', 'Full Name', 'text', true, 0),
|
||||
('seed_fld_people_email_000000000', 'seed_obj_people_00000000000000', 'Email Address', 'email', true, 1),
|
||||
('seed_fld_people_phone_000000000', 'seed_obj_people_00000000000000', 'Phone Number', 'phone', false, 2),
|
||||
('seed_fld_people_company_0000000', 'seed_obj_people_00000000000000', 'Company', 'text', false, 3);
|
||||
|
||||
INSERT INTO fields (id, object_id, name, type, required, enum_values, enum_colors, sort_order) VALUES
|
||||
('seed_fld_people_status_00000000', 'seed_obj_people_00000000000000', 'Status', 'enum', false,
|
||||
'["Active","Inactive","Lead"]'::JSON, '["#22c55e","#94a3b8","#3b82f6"]'::JSON, 4);
|
||||
|
||||
INSERT INTO fields (id, object_id, name, type, required, sort_order) VALUES
|
||||
('seed_fld_people_notes_000000000', 'seed_obj_people_00000000000000', 'Notes', 'richtext', false, 5);
|
||||
|
||||
INSERT INTO entries (id, object_id) VALUES
|
||||
('seed_ent_people_sarah_000000000', 'seed_obj_people_00000000000000'),
|
||||
('seed_ent_people_james_000000000', 'seed_obj_people_00000000000000'),
|
||||
('seed_ent_people_maria_000000000', 'seed_obj_people_00000000000000'),
|
||||
('seed_ent_people_alex_0000000000', 'seed_obj_people_00000000000000'),
|
||||
('seed_ent_people_priya_000000000', 'seed_obj_people_00000000000000');
|
||||
|
||||
INSERT INTO entry_fields (entry_id, field_id, value) VALUES
|
||||
('seed_ent_people_sarah_000000000', 'seed_fld_people_fullname_000000', 'Sarah Chen'),
|
||||
('seed_ent_people_sarah_000000000', 'seed_fld_people_email_000000000', 'sarah@acmecorp.com'),
|
||||
('seed_ent_people_sarah_000000000', 'seed_fld_people_phone_000000000', '+1 (555) 234-5678'),
|
||||
('seed_ent_people_sarah_000000000', 'seed_fld_people_company_0000000', 'Acme Corp'),
|
||||
('seed_ent_people_sarah_000000000', 'seed_fld_people_status_00000000', 'Active'),
|
||||
('seed_ent_people_james_000000000', 'seed_fld_people_fullname_000000', 'James Wilson'),
|
||||
('seed_ent_people_james_000000000', 'seed_fld_people_email_000000000', 'james@techcorp.io'),
|
||||
('seed_ent_people_james_000000000', 'seed_fld_people_phone_000000000', '+1 (555) 876-5432'),
|
||||
('seed_ent_people_james_000000000', 'seed_fld_people_company_0000000', 'TechCorp Industries'),
|
||||
('seed_ent_people_james_000000000', 'seed_fld_people_status_00000000', 'Active'),
|
||||
('seed_ent_people_maria_000000000', 'seed_fld_people_fullname_000000', 'Maria Garcia'),
|
||||
('seed_ent_people_maria_000000000', 'seed_fld_people_email_000000000', 'maria@innovate.co'),
|
||||
('seed_ent_people_maria_000000000', 'seed_fld_people_phone_000000000', '+1 (555) 345-6789'),
|
||||
('seed_ent_people_maria_000000000', 'seed_fld_people_company_0000000', 'Innovate Co'),
|
||||
('seed_ent_people_maria_000000000', 'seed_fld_people_status_00000000', 'Lead'),
|
||||
('seed_ent_people_alex_0000000000', 'seed_fld_people_fullname_000000', 'Alex Thompson'),
|
||||
('seed_ent_people_alex_0000000000', 'seed_fld_people_email_000000000', 'alex@designstudio.io'),
|
||||
('seed_ent_people_alex_0000000000', 'seed_fld_people_phone_000000000', '+1 (555) 567-8901'),
|
||||
('seed_ent_people_alex_0000000000', 'seed_fld_people_company_0000000', 'Design Studio'),
|
||||
('seed_ent_people_alex_0000000000', 'seed_fld_people_status_00000000', 'Active'),
|
||||
('seed_ent_people_priya_000000000', 'seed_fld_people_fullname_000000', 'Priya Patel'),
|
||||
('seed_ent_people_priya_000000000', 'seed_fld_people_email_000000000', 'priya@cloudnine.dev'),
|
||||
('seed_ent_people_priya_000000000', 'seed_fld_people_phone_000000000', '+1 (555) 789-0123'),
|
||||
('seed_ent_people_priya_000000000', 'seed_fld_people_company_0000000', 'CloudNine'),
|
||||
('seed_ent_people_priya_000000000', 'seed_fld_people_status_00000000', 'Lead');
|
||||
|
||||
CREATE OR REPLACE VIEW v_people AS
|
||||
PIVOT (
|
||||
SELECT e.id as entry_id, e.created_at, e.updated_at,
|
||||
f.name as field_name, ef.value
|
||||
FROM entries e
|
||||
JOIN entry_fields ef ON ef.entry_id = e.id
|
||||
JOIN fields f ON f.id = ef.field_id
|
||||
WHERE e.object_id = 'seed_obj_people_00000000000000'
|
||||
) ON field_name IN ('Full Name', 'Email Address', 'Phone Number', 'Company', 'Status', 'Notes') USING first(value);
|
||||
|
||||
-- ── Seed: company ──
|
||||
|
||||
INSERT INTO objects (id, name, description, icon, default_view, immutable, sort_order)
|
||||
VALUES ('seed_obj_company_0000000000000', 'company', 'Company tracking', 'building-2', 'table', true, 1);
|
||||
|
||||
INSERT INTO fields (id, object_id, name, type, required, sort_order) VALUES
|
||||
('seed_fld_company_name_000000000', 'seed_obj_company_0000000000000', 'Company Name', 'text', true, 0);
|
||||
|
||||
INSERT INTO fields (id, object_id, name, type, required, enum_values, enum_colors, sort_order) VALUES
|
||||
('seed_fld_company_industry_00000', 'seed_obj_company_0000000000000', 'Industry', 'enum', false,
|
||||
'["Technology","Finance","Healthcare","Education","Retail","Other"]'::JSON,
|
||||
'["#3b82f6","#22c55e","#ef4444","#f59e0b","#8b5cf6","#94a3b8"]'::JSON, 1);
|
||||
|
||||
INSERT INTO fields (id, object_id, name, type, required, sort_order) VALUES
|
||||
('seed_fld_company_website_000000', 'seed_obj_company_0000000000000', 'Website', 'text', false, 2);
|
||||
|
||||
INSERT INTO fields (id, object_id, name, type, required, enum_values, enum_colors, sort_order) VALUES
|
||||
('seed_fld_company_type_000000000', 'seed_obj_company_0000000000000', 'Type', 'enum', false,
|
||||
'["Client","Partner","Vendor","Prospect"]'::JSON,
|
||||
'["#22c55e","#3b82f6","#f59e0b","#94a3b8"]'::JSON, 3);
|
||||
|
||||
INSERT INTO fields (id, object_id, name, type, required, sort_order) VALUES
|
||||
('seed_fld_company_notes_00000000', 'seed_obj_company_0000000000000', 'Notes', 'richtext', false, 4);
|
||||
|
||||
INSERT INTO entries (id, object_id) VALUES
|
||||
('seed_ent_company_acme_000000000', 'seed_obj_company_0000000000000'),
|
||||
('seed_ent_company_tech_000000000', 'seed_obj_company_0000000000000'),
|
||||
('seed_ent_company_innov_00000000', 'seed_obj_company_0000000000000');
|
||||
|
||||
INSERT INTO entry_fields (entry_id, field_id, value) VALUES
|
||||
('seed_ent_company_acme_000000000', 'seed_fld_company_name_000000000', 'Acme Corp'),
|
||||
('seed_ent_company_acme_000000000', 'seed_fld_company_industry_00000', 'Technology'),
|
||||
('seed_ent_company_acme_000000000', 'seed_fld_company_website_000000', 'https://acmecorp.com'),
|
||||
('seed_ent_company_acme_000000000', 'seed_fld_company_type_000000000', 'Client'),
|
||||
('seed_ent_company_tech_000000000', 'seed_fld_company_name_000000000', 'TechCorp Industries'),
|
||||
('seed_ent_company_tech_000000000', 'seed_fld_company_industry_00000', 'Finance'),
|
||||
('seed_ent_company_tech_000000000', 'seed_fld_company_website_000000', 'https://techcorp.io'),
|
||||
('seed_ent_company_tech_000000000', 'seed_fld_company_type_000000000', 'Partner'),
|
||||
('seed_ent_company_innov_00000000', 'seed_fld_company_name_000000000', 'Innovate Co'),
|
||||
('seed_ent_company_innov_00000000', 'seed_fld_company_industry_00000', 'Healthcare'),
|
||||
('seed_ent_company_innov_00000000', 'seed_fld_company_website_000000', 'https://innovate.co'),
|
||||
('seed_ent_company_innov_00000000', 'seed_fld_company_type_000000000', 'Prospect');
|
||||
|
||||
CREATE OR REPLACE VIEW v_company AS
|
||||
PIVOT (
|
||||
SELECT e.id as entry_id, e.created_at, e.updated_at,
|
||||
f.name as field_name, ef.value
|
||||
FROM entries e
|
||||
JOIN entry_fields ef ON ef.entry_id = e.id
|
||||
JOIN fields f ON f.id = ef.field_id
|
||||
WHERE e.object_id = 'seed_obj_company_0000000000000'
|
||||
) ON field_name IN ('Company Name', 'Industry', 'Website', 'Type', 'Notes') USING first(value);
|
||||
|
||||
-- ── Seed: task ──
|
||||
|
||||
INSERT INTO objects (id, name, description, icon, default_view, sort_order)
|
||||
VALUES ('seed_obj_task_000000000000000', 'task', 'Task tracking board', 'check-square', 'kanban', 2);
|
||||
|
||||
INSERT INTO fields (id, object_id, name, type, required, sort_order) VALUES
|
||||
('seed_fld_task_title_00000000000', 'seed_obj_task_000000000000000', 'Title', 'text', true, 0),
|
||||
('seed_fld_task_desc_000000000000', 'seed_obj_task_000000000000000', 'Description', 'text', false, 1);
|
||||
|
||||
INSERT INTO fields (id, object_id, name, type, required, enum_values, enum_colors, sort_order) VALUES
|
||||
('seed_fld_task_status_0000000000', 'seed_obj_task_000000000000000', 'Status', 'enum', false,
|
||||
'["In Queue","In Progress","Done"]'::JSON, '["#94a3b8","#3b82f6","#22c55e"]'::JSON, 2),
|
||||
('seed_fld_task_priority_00000000', 'seed_obj_task_000000000000000', 'Priority', 'enum', false,
|
||||
'["Low","Medium","High"]'::JSON, '["#94a3b8","#f59e0b","#ef4444"]'::JSON, 3);
|
||||
|
||||
INSERT INTO fields (id, object_id, name, type, required, sort_order) VALUES
|
||||
('seed_fld_task_duedate_000000000', 'seed_obj_task_000000000000000', 'Due Date', 'date', false, 4),
|
||||
('seed_fld_task_notes_00000000000', 'seed_obj_task_000000000000000', 'Notes', 'richtext', false, 5);
|
||||
|
||||
INSERT INTO statuses (id, object_id, name, color, sort_order, is_default) VALUES
|
||||
('seed_sts_task_queue_00000000000', 'seed_obj_task_000000000000000', 'In Queue', '#94a3b8', 0, true),
|
||||
('seed_sts_task_progress_00000000', 'seed_obj_task_000000000000000', 'In Progress', '#3b82f6', 1, false),
|
||||
('seed_sts_task_done_000000000000', 'seed_obj_task_000000000000000', 'Done', '#22c55e', 2, false);
|
||||
|
||||
INSERT INTO entries (id, object_id) VALUES
|
||||
('seed_ent_task_review_0000000000', 'seed_obj_task_000000000000000'),
|
||||
('seed_ent_task_onboard_000000000', 'seed_obj_task_000000000000000'),
|
||||
('seed_ent_task_retro_00000000000', 'seed_obj_task_000000000000000'),
|
||||
('seed_ent_task_investor_00000000', 'seed_obj_task_000000000000000'),
|
||||
('seed_ent_task_dashperf_00000000', 'seed_obj_task_000000000000000');
|
||||
|
||||
INSERT INTO entry_fields (entry_id, field_id, value) VALUES
|
||||
('seed_ent_task_review_0000000000', 'seed_fld_task_title_00000000000', 'Review Q1 reports'),
|
||||
('seed_ent_task_review_0000000000', 'seed_fld_task_desc_000000000000', 'Review and summarize Q1 financial reports'),
|
||||
('seed_ent_task_review_0000000000', 'seed_fld_task_status_0000000000', 'In Progress'),
|
||||
('seed_ent_task_review_0000000000', 'seed_fld_task_priority_00000000', 'High'),
|
||||
('seed_ent_task_review_0000000000', 'seed_fld_task_duedate_000000000', '2026-03-15'),
|
||||
('seed_ent_task_onboard_000000000', 'seed_fld_task_title_00000000000', 'Update client onboarding docs'),
|
||||
('seed_ent_task_onboard_000000000', 'seed_fld_task_desc_000000000000', 'Refresh the onboarding documentation with latest screenshots'),
|
||||
('seed_ent_task_onboard_000000000', 'seed_fld_task_status_0000000000', 'In Queue'),
|
||||
('seed_ent_task_onboard_000000000', 'seed_fld_task_priority_00000000', 'Medium'),
|
||||
('seed_ent_task_onboard_000000000', 'seed_fld_task_duedate_000000000', '2026-03-20'),
|
||||
('seed_ent_task_retro_00000000000', 'seed_fld_task_title_00000000000', 'Schedule team retrospective'),
|
||||
('seed_ent_task_retro_00000000000', 'seed_fld_task_desc_000000000000', 'Organize end-of-sprint retro for the team'),
|
||||
('seed_ent_task_retro_00000000000', 'seed_fld_task_status_0000000000', 'Done'),
|
||||
('seed_ent_task_retro_00000000000', 'seed_fld_task_priority_00000000', 'Low'),
|
||||
('seed_ent_task_investor_00000000', 'seed_fld_task_title_00000000000', 'Prepare investor deck'),
|
||||
('seed_ent_task_investor_00000000', 'seed_fld_task_desc_000000000000', 'Create presentation for upcoming investor meeting'),
|
||||
('seed_ent_task_investor_00000000', 'seed_fld_task_status_0000000000', 'In Queue'),
|
||||
('seed_ent_task_investor_00000000', 'seed_fld_task_priority_00000000', 'High'),
|
||||
('seed_ent_task_investor_00000000', 'seed_fld_task_duedate_000000000', '2026-04-01'),
|
||||
('seed_ent_task_dashperf_00000000', 'seed_fld_task_title_00000000000', 'Fix dashboard performance'),
|
||||
('seed_ent_task_dashperf_00000000', 'seed_fld_task_desc_000000000000', 'Investigate and resolve slow loading on analytics dashboard'),
|
||||
('seed_ent_task_dashperf_00000000', 'seed_fld_task_status_0000000000', 'In Progress'),
|
||||
('seed_ent_task_dashperf_00000000', 'seed_fld_task_priority_00000000', 'Medium'),
|
||||
('seed_ent_task_dashperf_00000000', 'seed_fld_task_duedate_000000000', '2026-03-10');
|
||||
|
||||
CREATE OR REPLACE VIEW v_task AS
|
||||
PIVOT (
|
||||
SELECT e.id as entry_id, e.created_at, e.updated_at,
|
||||
f.name as field_name, ef.value
|
||||
FROM entries e
|
||||
JOIN entry_fields ef ON ef.entry_id = e.id
|
||||
JOIN fields f ON f.id = ef.field_id
|
||||
WHERE e.object_id = 'seed_obj_task_000000000000000'
|
||||
) ON field_name IN ('Title', 'Description', 'Status', 'Priority', 'Due Date', 'Notes') USING first(value);
|
||||
BIN
assets/seed/workspace.duckdb
Normal file
BIN
assets/seed/workspace.duckdb
Normal file
Binary file not shown.
101
pnpm-lock.yaml
generated
101
pnpm-lock.yaml
generated
@ -2696,7 +2696,7 @@ packages:
|
||||
dependencies:
|
||||
'@types/node': 24.10.9
|
||||
optionalDependencies:
|
||||
axios: 1.13.4
|
||||
axios: 1.13.5(debug@4.4.3)
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
@ -3129,6 +3129,7 @@ packages:
|
||||
/@napi-rs/canvas@0.1.89:
|
||||
resolution: {integrity: sha512-7GjmkMirJHejeALCqUnZY3QwID7bbumOiLrqq2LKgxrdjdmxWQBTc6rcASa2u8wuWrH7qo4/4n/VNrOwCoKlKg==}
|
||||
engines: {node: '>= 10'}
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas-android-arm64': 0.1.89
|
||||
'@napi-rs/canvas-darwin-arm64': 0.1.89
|
||||
@ -4120,43 +4121,43 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@oxlint-tsgolint/darwin-arm64@0.12.2:
|
||||
resolution: {integrity: sha512-XIfavTqkJPGYi/98z7ZCkZvXq2AccMAAB0iwvKDRTQqiweMXVUyeUdx46phCHHH1PgmIVJtVfysThkHq2xCyrw==}
|
||||
/@oxlint-tsgolint/darwin-arm64@0.13.0:
|
||||
resolution: {integrity: sha512-OWQ3U+oDjjupmX0WU9oYyKF2iUOKDMLW/+zan0cd0vYIGId80xTRHHA8oXnREmK8dsMMP3nV3VXME3NH/hS0lw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@oxlint-tsgolint/darwin-x64@0.12.2:
|
||||
resolution: {integrity: sha512-tytsvP6zmNShRNDo4GgQartOXmd4GPd+TylCUMdO/iWl9PZVOgRyswWbYVTNgn85Cib/aY2q3Uu+jOw+QlbxvQ==}
|
||||
/@oxlint-tsgolint/darwin-x64@0.13.0:
|
||||
resolution: {integrity: sha512-wZvgj+eVqNkCUjSq2ExlMdbGDpZfaw6J+YctQV1pkGFdn7Y9cySWdfwu5v/AW2JPsJbFMXJ8GAr+WoZbRapz2A==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@oxlint-tsgolint/linux-arm64@0.12.2:
|
||||
resolution: {integrity: sha512-3W38yJuF7taEquhEuD6mYQyCeWNAlc1pNPjFkspkhLKZVgbrhDA4V6fCxLDDRvrTHde0bXPmFvuPlUq5pSePgA==}
|
||||
/@oxlint-tsgolint/linux-arm64@0.13.0:
|
||||
resolution: {integrity: sha512-nwtf5BgHbAWSVwyIF00l6QpfyFcpDMp6D+3cpe6NTgBYMSSSC0Ip1gswUwzVccOPoQK48t+J6vHyURQ96M1KDg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@oxlint-tsgolint/linux-x64@0.12.2:
|
||||
resolution: {integrity: sha512-EjcEspeeV0NmaopEp4wcN5ntQP9VCJJDrTvzOjMP4W6ajz18M+pni9vkKvmcPIpRa/UmWobeFgKoVd/KGueeuQ==}
|
||||
/@oxlint-tsgolint/linux-x64@0.13.0:
|
||||
resolution: {integrity: sha512-Rkzgj38eVoGSBuGDaCrALS4FM19+m1Qlv0hjB4MWvXUej014XkB5ze+svYE3HX+AAm1ey9QYj/CQzfz203FPIg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@oxlint-tsgolint/win32-arm64@0.12.2:
|
||||
resolution: {integrity: sha512-a9L7iA5K/Ht/i8d9+7RTp6hbPa4cyXP0MdySVXAO6vczpL/4ildfY9Hr2m2wqL12uK6xe/uVABpVTrqay/wV+g==}
|
||||
/@oxlint-tsgolint/win32-arm64@0.13.0:
|
||||
resolution: {integrity: sha512-Y+0hFqLT5M7UIvGvTR3QFK27l17FqXk6UwwpBFOcyBGJ5bLd1RaAPWjqTmcgPvdolA6FCMeW1pxZuNtKDlYd7A==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@oxlint-tsgolint/win32-x64@0.12.2:
|
||||
resolution: {integrity: sha512-Cvt40UbTf5ib12DjGN+mMGOnjWa4Bc6Y7KEaXXp9qzckvs3HpNk2wSwMV3gnuR8Ipx4hkzkzrgzD0BAUsySAfA==}
|
||||
/@oxlint-tsgolint/win32-x64@0.13.0:
|
||||
resolution: {integrity: sha512-mXjTttzyyfl8d/XvxggmZFBq0pbQmRvHbjQEv70YECNaLEHG8j8WYUwLa641uudAnV1VoBI34pc7bmgJM7qhOA==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
dev: true
|
||||
@ -6455,66 +6456,66 @@ packages:
|
||||
dependencies:
|
||||
'@types/node': 25.2.3
|
||||
|
||||
/@typescript/native-preview-darwin-arm64@7.0.0-dev.20260214.1:
|
||||
resolution: {integrity: sha512-Jb2WcLGpTOC6x58e8QPYC/14xmDbnbFIuKqUvYoI77hVtojVyxZi8L5Y4CgYqXYx8vRWmIFk35c1OGdtPip6Sg==}
|
||||
/@typescript/native-preview-darwin-arm64@7.0.0-dev.20260215.1:
|
||||
resolution: {integrity: sha512-icVO/hEMXjWlKhmpjIpqDyCzPvtHqfrPB+2rkd6M3rz84Bmw+o8Xgd7JvRxryZhR+D0y55me/bKh9xgvsgzuhA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@typescript/native-preview-darwin-x64@7.0.0-dev.20260214.1:
|
||||
resolution: {integrity: sha512-O9l2gVuQFZsb8NIQtu0HN5Tn/Hw2fwylPOPS/0Y4oW+FUMhkqtvetUkb3zZ0qj7capilZ4YnmyGYg3TDqkP4Nw==}
|
||||
/@typescript/native-preview-darwin-x64@7.0.0-dev.20260215.1:
|
||||
resolution: {integrity: sha512-Wz73wf1o9+4KwCLg8wnnIZZDAvv2KRZlDyP4X8GfBNzajfIAwYvI0ANWuIDznUUGeDAcqhBJXNe0Bkf4H9y4mg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@typescript/native-preview-linux-arm64@7.0.0-dev.20260214.1:
|
||||
resolution: {integrity: sha512-Hl4e3yxJqzIGgFI8aH/rLGW+a7kSLHJCpAd5JOLG7hHKnamZF4SjlunnoHLV4IcMri+G6UE3W/84i0QvQP5wLA==}
|
||||
/@typescript/native-preview-linux-arm64@7.0.0-dev.20260215.1:
|
||||
resolution: {integrity: sha512-AYyXRxVwLZzfkEYN8FGdV4vqXwbTmv93nAZ6gMLvpDG4ItOybAE1R2obFjlFc+Or/rfQmVvfdkTym3c4bRJ3XQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@typescript/native-preview-linux-arm@7.0.0-dev.20260214.1:
|
||||
resolution: {integrity: sha512-TaFrVnx3iXtl/oH1hzwvFyqWj9tzkjW8Ufl2m0Vx2/7GXnzZadm2KA6tFpGbzzWbZJznmXxKHL4O3AZRQYyZqQ==}
|
||||
/@typescript/native-preview-linux-arm@7.0.0-dev.20260215.1:
|
||||
resolution: {integrity: sha512-6WVXFVSp3LBBiBgBMtAHQgTDN72mDhgjrmXH7GoABTxR9asK8oPfmy5cwTp1sPD46pYhqjnSHMrARyg2FaNSeA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@typescript/native-preview-linux-x64@7.0.0-dev.20260214.1:
|
||||
resolution: {integrity: sha512-a/JypIXTc/tdodhYdQm24WH6aTfnJJjDbwxce4BS2g6IzYSc2GFcZBvlq1CJYS2FAVLpiSxj0OFAZmgjpCDAKg==}
|
||||
/@typescript/native-preview-linux-x64@7.0.0-dev.20260215.1:
|
||||
resolution: {integrity: sha512-Ui6qbTO+nE7fwh5OGTGfL4ndaT+SpiUiv0F1m3+nMaiAKysY5GbgXUfzWzkSrOODsT8F/4jZ4wCzEzJordt8sQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@typescript/native-preview-win32-arm64@7.0.0-dev.20260214.1:
|
||||
resolution: {integrity: sha512-MJGPEDvdXj8olcWH0P+cWYcaN4r/0J4aSbcaISlen3MZ/2hrrgNl46PV4eGJKKCDniY2pH2fJzrMyJWZOcdb0w==}
|
||||
/@typescript/native-preview-win32-arm64@7.0.0-dev.20260215.1:
|
||||
resolution: {integrity: sha512-dBFyAH9h3bMUaIp/84c3gKwyQ6jQmtzVoIBamSrYNw0xinJ56A/Ln5igdNOYrH8+/Aofmeh7pAWaa8U456XMjw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@typescript/native-preview-win32-x64@7.0.0-dev.20260214.1:
|
||||
resolution: {integrity: sha512-BtF48TRUyiCKznlOcQ7r7EXhonGSanm9X2eu7d8Yq1vaWO5SDgB0e+ISQXSoIfs3a1S3d5S5QV/vTE4+vocPxA==}
|
||||
/@typescript/native-preview-win32-x64@7.0.0-dev.20260215.1:
|
||||
resolution: {integrity: sha512-bEMSwX71OGGvfsfHEa/aX7ZUWbPSI2oKEmeWcDQVY8vH1VK1ZwcFzMhKfgVJPt5pKH2bK3EO3xYnAyKkDO/Ung==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@typescript/native-preview@7.0.0-dev.20260214.1:
|
||||
resolution: {integrity: sha512-BDM0ZLf2v6ilR0tDi8OMEr4X08lFCToPk3/p1SSE4GhagzmlU/5b+9slR0kKtaKMrds01FhvaKx6U9+NmAWgbQ==}
|
||||
/@typescript/native-preview@7.0.0-dev.20260215.1:
|
||||
resolution: {integrity: sha512-grs0BbJyPR7VLNerBVteEToPku1InMKVKVKBUTJi19LfK+LU3+pkU6/fsTfZhH3xmIzIxD/sNRQHLt4x/Yb9yg==}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260215.1
|
||||
dev: true
|
||||
|
||||
/@typespec/ts-http-runtime@0.3.2:
|
||||
@ -10390,19 +10391,19 @@ packages:
|
||||
'@oxfmt/binding-win32-x64-msvc': 0.32.0
|
||||
dev: true
|
||||
|
||||
/oxlint-tsgolint@0.12.2:
|
||||
resolution: {integrity: sha512-IFiOhYZfSgiHbBznTZOhFpEHpsZFSP0j7fVRake03HEkgH0YljnTFDNoRkGWsTrnrHr7nRIomSsF4TnCI/O+kQ==}
|
||||
/oxlint-tsgolint@0.13.0:
|
||||
resolution: {integrity: sha512-VUOWP5T9R9RwuPLKvNgvhsjdPFVhr2k8no8ea84+KhDtYPmk9L/3StNP3WClyPOKJOT8bFlO3eyhTKxXK9+Oog==}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
'@oxlint-tsgolint/darwin-arm64': 0.12.2
|
||||
'@oxlint-tsgolint/darwin-x64': 0.12.2
|
||||
'@oxlint-tsgolint/linux-arm64': 0.12.2
|
||||
'@oxlint-tsgolint/linux-x64': 0.12.2
|
||||
'@oxlint-tsgolint/win32-arm64': 0.12.2
|
||||
'@oxlint-tsgolint/win32-x64': 0.12.2
|
||||
'@oxlint-tsgolint/darwin-arm64': 0.13.0
|
||||
'@oxlint-tsgolint/darwin-x64': 0.13.0
|
||||
'@oxlint-tsgolint/linux-arm64': 0.13.0
|
||||
'@oxlint-tsgolint/linux-x64': 0.13.0
|
||||
'@oxlint-tsgolint/win32-arm64': 0.13.0
|
||||
'@oxlint-tsgolint/win32-x64': 0.13.0
|
||||
dev: true
|
||||
|
||||
/oxlint@1.47.0(oxlint-tsgolint@0.12.2):
|
||||
/oxlint@1.47.0(oxlint-tsgolint@0.13.0):
|
||||
resolution: {integrity: sha512-v7xkK1iv1qdvTxJGclM97QzN8hHs5816AneFAQ0NGji1BMUquhiDAhXpMwp8+ls16uRVJtzVHxP9pAAXblDeGA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
@ -10412,7 +10413,7 @@ packages:
|
||||
oxlint-tsgolint:
|
||||
optional: true
|
||||
dependencies:
|
||||
oxlint-tsgolint: 0.12.2
|
||||
oxlint-tsgolint: 0.13.0
|
||||
optionalDependencies:
|
||||
'@oxlint/binding-android-arm-eabi': 1.47.0
|
||||
'@oxlint/binding-android-arm64': 1.47.0
|
||||
@ -11373,7 +11374,7 @@ packages:
|
||||
glob: 10.5.0
|
||||
dev: false
|
||||
|
||||
/rolldown-plugin-dts@0.22.1(@typescript/native-preview@7.0.0-dev.20260214.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3):
|
||||
/rolldown-plugin-dts@0.22.1(@typescript/native-preview@7.0.0-dev.20260215.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3):
|
||||
resolution: {integrity: sha512-5E0AiM5RSQhU6cjtkDFWH6laW4IrMu0j1Mo8x04Xo1ALHmaRMs9/7zej7P3RrryVHW/DdZAp85MA7Be55p0iUw==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
@ -11396,7 +11397,7 @@ packages:
|
||||
'@babel/helper-validator-identifier': 8.0.0-rc.1
|
||||
'@babel/parser': 8.0.0-rc.1
|
||||
'@babel/types': 8.0.0-rc.1
|
||||
'@typescript/native-preview': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview': 7.0.0-dev.20260215.1
|
||||
ast-kit: 3.0.0-beta.1
|
||||
birpc: 4.0.0
|
||||
dts-resolver: 2.1.3
|
||||
@ -12215,7 +12216,7 @@ packages:
|
||||
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||
dev: false
|
||||
|
||||
/tsdown@0.20.3(@typescript/native-preview@7.0.0-dev.20260214.1)(typescript@5.9.3):
|
||||
/tsdown@0.20.3(@typescript/native-preview@7.0.0-dev.20260215.1)(typescript@5.9.3):
|
||||
resolution: {integrity: sha512-qWOUXSbe4jN8JZEgrkc/uhJpC8VN2QpNu3eZkBWwNuTEjc/Ik1kcc54ycfcQ5QPRHeu9OQXaLfCI3o7pEJgB2w==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
hasBin: true
|
||||
@ -12249,7 +12250,7 @@ packages:
|
||||
obug: 2.1.1
|
||||
picomatch: 4.0.3
|
||||
rolldown: 1.0.0-rc.3
|
||||
rolldown-plugin-dts: 0.22.1(@typescript/native-preview@7.0.0-dev.20260214.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3)
|
||||
rolldown-plugin-dts: 0.22.1(@typescript/native-preview@7.0.0-dev.20260215.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3)
|
||||
semver: 7.7.3
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
---
|
||||
name: dench
|
||||
name: database-crm-system
|
||||
description: Manage Database and everything else in the workspace - objects, fields, entries via DuckDB and documents as markdown files in a nested knowledge tree. Acts as your second brain.
|
||||
metadata: { "openclaw": { "inject": true, "always": true, "emoji": "📊" } }
|
||||
---
|
||||
|
||||
# Dench Workspace
|
||||
# CRM / Database in Workspace / Guide on handling any data
|
||||
|
||||
You manage a Dench workspace stored at `~/.openclaw/workspace`.
|
||||
All structured data lives in **DuckDB** (`~/.openclaw/workspace/workspace.duckdb`). Documents are **markdown files** in `~/.openclaw/workspace/**`. Organization context is in `~/.openclaw/workspace/workspace_context.yaml` (READ-ONLY).
|
||||
|
||||
@ -91,6 +91,8 @@ export function resolveOpenClawMetadata(
|
||||
const osRaw = resolveOpenClawManifestOs(metadataObj);
|
||||
return {
|
||||
always: typeof metadataObj.always === "boolean" ? metadataObj.always : undefined,
|
||||
// ALWAYS KEEP THIS INJECT LOGIC EVEN IF UPSTREAM OPENCLAW REPO DOES NOT HAVE IT
|
||||
inject: typeof metadataObj.inject === "boolean" ? metadataObj.inject : undefined,
|
||||
emoji: typeof metadataObj.emoji === "string" ? metadataObj.emoji : undefined,
|
||||
homepage: typeof metadataObj.homepage === "string" ? metadataObj.homepage : undefined,
|
||||
skillKey: typeof metadataObj.skillKey === "string" ? metadataObj.skillKey : undefined,
|
||||
|
||||
@ -148,6 +148,42 @@ function buildVoiceSection(params: { isMinimal: boolean; ttsHint?: string }) {
|
||||
return ["## Voice (TTS)", hint, ""];
|
||||
}
|
||||
|
||||
function buildIronclawSection(isMinimal: boolean) {
|
||||
if (isMinimal) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
"## Ironclaw",
|
||||
"You are **Ironclaw** — a personal AI agent and a CRM Database System built by Kumar Abhirup (kumareth.com), running on top of the [OpenClaw](https://github.com/openclaw/openclaw) framework.",
|
||||
"",
|
||||
"### What you can do",
|
||||
"- **Find leads**: scrape the web using the user's Chrome profile (all auth sessions, cookies, history) — LinkedIn, company directories, etc.",
|
||||
"- **Enrich data**: fill in LinkedIn URLs, emails, education, company info for contacts in bulk.",
|
||||
"- **Send outreach**: personalized LinkedIn messages, cold emails, follow-up sequences — each customized per lead.",
|
||||
"- **Chat with the database**: translate plain-English questions to SQL against the local DuckDB workspace and return structured results.",
|
||||
"- **Generate analytics**: interactive Recharts dashboards (bar, line, area, pie, donut, funnel, scatter, radar) from live DuckDB data, rendered inline in chat.",
|
||||
"- **Automate everything**: cron jobs for follow-ups, lead scoring, pipeline reports, enrichment syncs, competitor monitoring.",
|
||||
"- **Write and review code**: produce diffs the user can approve before applying.",
|
||||
"- **Manage documents**: rich markdown with embedded live charts — SOPs, playbooks, onboarding guides.",
|
||||
"",
|
||||
"### Key architecture",
|
||||
"- **Web UI**: Next.js app that usually runs at `localhost:3100` — chat panel, workspace sidebar, object tables, kanban boards, report cards, document editor, media viewer.",
|
||||
"- **DuckDB workspace**: all structured data (objects, fields, entries, relations) in a local DuckDB database with EAV pattern and auto-generated PIVOT views (`v_<object>`).",
|
||||
"- **Skills platform**: extend capabilities via `SKILL.md` files — browse at [skills.sh](https://skills.sh) and [ClawHub](https://clawhub.com).",
|
||||
`- **Past Web Sessions**: Your past Ironclaw web chat sessions are stored in: ~/.openclaw/web-chat/ (or near wherever you store your workspace)`,
|
||||
"",
|
||||
"### Links",
|
||||
"- Website: https://ironclaw.sh",
|
||||
"- Docs: https://docs.openclaw.ai",
|
||||
"- GitHub: https://github.com/DenchHQ/ironclaw",
|
||||
"- Discord: https://discord.gg/clawd",
|
||||
"- Skills Store: https://skills.sh",
|
||||
"",
|
||||
"When referring to yourself, use **Ironclaw** (not OpenClaw). The underlying framework is OpenClaw; Ironclaw is the product the user interacts with.",
|
||||
"",
|
||||
];
|
||||
}
|
||||
|
||||
function buildDocsSection(params: {
|
||||
docsPath?: string;
|
||||
isMinimal: boolean;
|
||||
@ -401,6 +437,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
cliName: cli,
|
||||
});
|
||||
const workspaceNotes = (params.workspaceNotes ?? []).map((note) => note.trim()).filter(Boolean);
|
||||
const ironclawSection = buildIronclawSection(isMinimal);
|
||||
|
||||
// For "none" mode, return just the basic identity line
|
||||
if (promptMode === "none") {
|
||||
@ -483,7 +520,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
? "If you need the current date, time, or day of week, run session_status (📊 session_status)."
|
||||
: "",
|
||||
"## Workspace",
|
||||
`Your working directory is: ${displayWorkspaceDir}`,
|
||||
`Your working directory is: ${displayWorkspaceDir}.`,
|
||||
workspaceGuidance,
|
||||
...workspaceNotes,
|
||||
"",
|
||||
@ -550,6 +587,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
messageToolHints: params.messageToolHints,
|
||||
}),
|
||||
...buildVoiceSection({ isMinimal, ttsHint: params.ttsHint }),
|
||||
...ironclawSection,
|
||||
];
|
||||
|
||||
if (extraSystemPrompt) {
|
||||
|
||||
237
src/agents/workspace-seed.ts
Normal file
237
src/agents/workspace-seed.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Seed data (matches the pre-built assets/seed/workspace.duckdb exactly)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type SeedField = {
|
||||
name: string;
|
||||
type: string;
|
||||
required?: boolean;
|
||||
enumValues?: string[];
|
||||
};
|
||||
|
||||
type SeedObject = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
defaultView: string;
|
||||
entryCount: number;
|
||||
fields: SeedField[];
|
||||
};
|
||||
|
||||
/** Fixed seed objects matching what's baked into assets/seed/workspace.duckdb. */
|
||||
const SEED_OBJECTS: SeedObject[] = [
|
||||
{
|
||||
id: "seed_obj_people_00000000000000",
|
||||
name: "people",
|
||||
description: "Contact management",
|
||||
icon: "users",
|
||||
defaultView: "table",
|
||||
entryCount: 5,
|
||||
fields: [
|
||||
{ name: "Full Name", type: "text", required: true },
|
||||
{ name: "Email Address", type: "email", required: true },
|
||||
{ name: "Phone Number", type: "phone" },
|
||||
{ name: "Company", type: "text" },
|
||||
{ name: "Status", type: "enum", enumValues: ["Active", "Inactive", "Lead"] },
|
||||
{ name: "Notes", type: "richtext" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "seed_obj_company_0000000000000",
|
||||
name: "company",
|
||||
description: "Company tracking",
|
||||
icon: "building-2",
|
||||
defaultView: "table",
|
||||
entryCount: 3,
|
||||
fields: [
|
||||
{ name: "Company Name", type: "text", required: true },
|
||||
{
|
||||
name: "Industry",
|
||||
type: "enum",
|
||||
enumValues: ["Technology", "Finance", "Healthcare", "Education", "Retail", "Other"],
|
||||
},
|
||||
{ name: "Website", type: "text" },
|
||||
{ name: "Type", type: "enum", enumValues: ["Client", "Partner", "Vendor", "Prospect"] },
|
||||
{ name: "Notes", type: "richtext" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "seed_obj_task_000000000000000",
|
||||
name: "task",
|
||||
description: "Task tracking board",
|
||||
icon: "check-square",
|
||||
defaultView: "kanban",
|
||||
entryCount: 5,
|
||||
fields: [
|
||||
{ name: "Title", type: "text", required: true },
|
||||
{ name: "Description", type: "text" },
|
||||
{ name: "Status", type: "enum", enumValues: ["In Queue", "In Progress", "Done"] },
|
||||
{ name: "Priority", type: "enum", enumValues: ["Low", "Medium", "High"] },
|
||||
{ name: "Due Date", type: "date" },
|
||||
{ name: "Notes", type: "richtext" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filesystem projection generators
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function generateObjectYaml(obj: SeedObject): string {
|
||||
const lines: string[] = [
|
||||
`id: "${obj.id}"`,
|
||||
`name: "${obj.name}"`,
|
||||
`description: "${obj.description}"`,
|
||||
`icon: "${obj.icon}"`,
|
||||
`default_view: "${obj.defaultView}"`,
|
||||
`entry_count: ${obj.entryCount}`,
|
||||
`fields:`,
|
||||
];
|
||||
|
||||
for (const field of obj.fields) {
|
||||
lines.push(` - name: "${field.name}"`);
|
||||
lines.push(` type: ${field.type}`);
|
||||
if (field.required) {
|
||||
lines.push(` required: true`);
|
||||
}
|
||||
if (field.enumValues) {
|
||||
lines.push(` values: ${JSON.stringify(field.enumValues)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
function generateWorkspaceMd(objects: SeedObject[]): string {
|
||||
const lines: string[] = [
|
||||
"# Workspace Schema",
|
||||
"",
|
||||
"Auto-generated summary of the workspace database.",
|
||||
"",
|
||||
];
|
||||
|
||||
for (const obj of objects) {
|
||||
lines.push(`## ${obj.name}`);
|
||||
lines.push("");
|
||||
lines.push(`- **Description**: ${obj.description}`);
|
||||
lines.push(`- **View**: \`${obj.defaultView}\``);
|
||||
lines.push(`- **Entries**: ${obj.entryCount}`);
|
||||
lines.push(`- **Fields**:`);
|
||||
for (const field of obj.fields) {
|
||||
const req = field.required ? " (required)" : "";
|
||||
const vals = field.enumValues ? ` — ${field.enumValues.join(", ")}` : "";
|
||||
lines.push(` - ${field.name} (\`${field.type}\`)${req}${vals}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Path resolution for the pre-built seed database
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const _moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/** Relative-path fallbacks for source (src/agents/) and bundled (dist/) layouts. */
|
||||
const SEED_DB_FALLBACKS = [
|
||||
path.resolve(_moduleDir, "../../assets/seed/workspace.duckdb"),
|
||||
path.resolve(_moduleDir, "../assets/seed/workspace.duckdb"),
|
||||
];
|
||||
|
||||
/** Locate the pre-built workspace.duckdb shipped in assets/seed/. */
|
||||
async function resolveSeedDbPath(): Promise<string | null> {
|
||||
// Primary: use the robust package-root resolver (handles source, dist, global installs).
|
||||
const packageRoot = await resolveOpenClawPackageRoot({
|
||||
moduleUrl: import.meta.url,
|
||||
argv1: process.argv[1],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
if (packageRoot) {
|
||||
const candidate = path.join(packageRoot, "assets", "seed", "workspace.duckdb");
|
||||
try {
|
||||
await fs.access(candidate);
|
||||
return candidate;
|
||||
} catch {
|
||||
// fall through to relative fallbacks
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try relative paths from module dir.
|
||||
for (const candidate of SEED_DB_FALLBACKS) {
|
||||
try {
|
||||
await fs.access(candidate);
|
||||
return candidate;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Seed a fresh workspace by copying the pre-built DuckDB database and
|
||||
* generating filesystem projections (.object.yaml + WORKSPACE.md).
|
||||
*
|
||||
* No DuckDB CLI or npm package required — the database is a static asset.
|
||||
*
|
||||
* Skips gracefully if workspace.duckdb already exists.
|
||||
*
|
||||
* @returns true if the database was copied and projections created.
|
||||
*/
|
||||
export async function seedWorkspaceDuckDB(workspaceDir: string): Promise<boolean> {
|
||||
const dbPath = path.join(workspaceDir, "workspace.duckdb");
|
||||
|
||||
// Idempotent: skip if database already exists
|
||||
try {
|
||||
await fs.access(dbPath);
|
||||
return false;
|
||||
} catch {
|
||||
// doesn't exist yet — proceed
|
||||
}
|
||||
|
||||
// Locate the pre-built seed database
|
||||
const seedDb = await resolveSeedDbPath();
|
||||
if (!seedDb) {
|
||||
// Seed database not found (e.g. stripped install) — skip silently
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.copyFile(seedDb, dbPath);
|
||||
} catch {
|
||||
// Copy failed — clean up partial file
|
||||
await fs.unlink(dbPath).catch(() => {});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create filesystem projections
|
||||
for (const obj of SEED_OBJECTS) {
|
||||
const objDir = path.join(workspaceDir, obj.name);
|
||||
await fs.mkdir(objDir, { recursive: true });
|
||||
await fs.writeFile(path.join(objDir, ".object.yaml"), generateObjectYaml(obj), "utf-8");
|
||||
}
|
||||
|
||||
// Write WORKSPACE.md (only if missing)
|
||||
try {
|
||||
await fs.writeFile(path.join(workspaceDir, "WORKSPACE.md"), generateWorkspaceMd(SEED_OBJECTS), {
|
||||
encoding: "utf-8",
|
||||
flag: "wx",
|
||||
});
|
||||
} catch {
|
||||
// already exists — skip
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { resolveRequiredHomeDir } from "../infra/home-dir.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { isCronSessionKey, isSubagentSessionKey } from "../routing/session-key.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { seedWorkspaceDuckDB } from "./workspace-seed.js";
|
||||
import { resolveWorkspaceTemplateDir } from "./workspace-templates.js";
|
||||
|
||||
export function resolveDefaultAgentWorkspaceDir(
|
||||
@ -100,6 +101,7 @@ type WorkspaceOnboardingState = {
|
||||
version: typeof WORKSPACE_STATE_VERSION;
|
||||
bootstrapSeededAt?: string;
|
||||
onboardingCompletedAt?: string;
|
||||
duckdbSeededAt?: string;
|
||||
};
|
||||
|
||||
/** Set of recognized bootstrap filenames for runtime validation */
|
||||
@ -149,6 +151,7 @@ function parseWorkspaceOnboardingState(raw: string): WorkspaceOnboardingState |
|
||||
const parsed = JSON.parse(raw) as {
|
||||
bootstrapSeededAt?: unknown;
|
||||
onboardingCompletedAt?: unknown;
|
||||
duckdbSeededAt?: unknown;
|
||||
};
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return null;
|
||||
@ -159,6 +162,7 @@ function parseWorkspaceOnboardingState(raw: string): WorkspaceOnboardingState |
|
||||
typeof parsed.bootstrapSeededAt === "string" ? parsed.bootstrapSeededAt : undefined,
|
||||
onboardingCompletedAt:
|
||||
typeof parsed.onboardingCompletedAt === "string" ? parsed.onboardingCompletedAt : undefined,
|
||||
duckdbSeededAt: typeof parsed.duckdbSeededAt === "string" ? parsed.duckdbSeededAt : undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@ -355,6 +359,23 @@ export async function ensureAgentWorkspace(params?: {
|
||||
}
|
||||
}
|
||||
|
||||
// Seed DuckDB workspace with schema and sample data (best-effort).
|
||||
// Always verify the actual file — don't trust the state flag alone
|
||||
// (the db may have been deleted while the state persisted).
|
||||
const dbExists = await fileExists(path.join(dir, "workspace.duckdb"));
|
||||
if (!dbExists) {
|
||||
try {
|
||||
const seeded = await seedWorkspaceDuckDB(dir);
|
||||
if (seeded) {
|
||||
markState({ duckdbSeededAt: nowIso() });
|
||||
}
|
||||
} catch {
|
||||
// DuckDB seeding is best-effort; don't fail workspace creation
|
||||
}
|
||||
} else if (!state.duckdbSeededAt) {
|
||||
markState({ duckdbSeededAt: nowIso() });
|
||||
}
|
||||
|
||||
if (stateDirty) {
|
||||
await writeWorkspaceOnboardingState(statePath, state);
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ export function registerAgentCommands(program: Command, args: { agentChannelOpti
|
||||
.option("--session-id <id>", "Use an explicit session id")
|
||||
.option("--session-key <key>", "Explicit session key (e.g. agent:main:subagent:uuid)")
|
||||
.option("--agent <id>", "Agent id (overrides routing bindings)")
|
||||
.option("--lane <lane>", "Concurrency lane: main | subagent | cron | nested")
|
||||
.option("--lane <lane>", "Concurrency lane: main | web | subagent | cron | nested")
|
||||
.option("--thinking <level>", "Thinking level: off | minimal | low | medium | high")
|
||||
.option("--verbose <on|off>", "Persist agent verbose level for the session")
|
||||
.option(
|
||||
|
||||
@ -6,6 +6,7 @@ export {
|
||||
isCronSessionKey,
|
||||
isAcpSessionKey,
|
||||
isSubagentSessionKey,
|
||||
isWebSessionKey,
|
||||
parseAgentSessionKey,
|
||||
type ParsedAgentSessionKey,
|
||||
} from "../sessions/session-key-utils.js";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getSubagentDepth, isCronSessionKey } from "./session-key-utils.js";
|
||||
import { getSubagentDepth, isCronSessionKey, isWebSessionKey } from "./session-key-utils.js";
|
||||
|
||||
describe("getSubagentDepth", () => {
|
||||
it("returns 0 for non-subagent session keys", () => {
|
||||
@ -30,3 +30,22 @@ describe("isCronSessionKey", () => {
|
||||
expect(isCronSessionKey(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWebSessionKey", () => {
|
||||
it("matches web session keys with agent prefix", () => {
|
||||
expect(isWebSessionKey("agent:main:web:session-123")).toBe(true);
|
||||
expect(isWebSessionKey("agent:main:web:abc-def")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches bare web: prefix", () => {
|
||||
expect(isWebSessionKey("web:session-123")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not match non-web sessions", () => {
|
||||
expect(isWebSessionKey("agent:main:main")).toBe(false);
|
||||
expect(isWebSessionKey("agent:main:subagent:worker")).toBe(false);
|
||||
expect(isWebSessionKey("agent:main:cron:job-1")).toBe(false);
|
||||
expect(isWebSessionKey(undefined)).toBe(false);
|
||||
expect(isWebSessionKey("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -61,6 +61,18 @@ export function getSubagentDepth(sessionKey: string | undefined | null): number
|
||||
return raw.split(":subagent:").length - 1;
|
||||
}
|
||||
|
||||
export function isWebSessionKey(sessionKey: string | undefined | null): boolean {
|
||||
const raw = (sessionKey ?? "").trim();
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
if (raw.toLowerCase().startsWith("web:")) {
|
||||
return true;
|
||||
}
|
||||
const parsed = parseAgentSessionKey(raw);
|
||||
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("web:"));
|
||||
}
|
||||
|
||||
export function isAcpSessionKey(sessionKey: string | undefined | null): boolean {
|
||||
const raw = (sessionKey ?? "").trim();
|
||||
if (!raw) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user