ultimate-brain 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/brain-apply/SKILL.md +22 -12
- package/.agents/skills/brain-catch-up/SKILL.md +66 -0
- package/.agents/skills/brain-connect-notion/SKILL.md +37 -9
- package/.agents/skills/brain-howto/references/guide.md +21 -14
- package/.agents/skills/brain-ingest/SKILL.md +6 -6
- package/.agents/skills/brain-ingest/references/cli-inputs.md +15 -2
- package/.agents/skills/brain-ingest/references/extraction.md +11 -4
- package/.agents/skills/brain-ingest/references/notion.md +5 -5
- package/.agents/skills/brain-ingest/references/sources.md +17 -11
- package/.agents/skills/brain-sync/SKILL.md +8 -7
- package/.agents/skills/brain-sync/references/formats.md +2 -2
- package/dist/cli.js +10 -0
- package/dist/commands/status.js +9 -1
- package/dist/commands/sync.js +3 -3
- package/dist/commands/upgrade.js +33 -0
- package/dist/lib/apply.js +24 -7
- package/dist/lib/brain.js +3 -1
- package/dist/lib/command.js +1 -1
- package/dist/lib/proposals.js +66 -15
- package/dist/lib/sync.js +104 -38
- package/package.json +2 -1
- package/spec/schemas/brain.schema.json +35 -0
- package/spec/schemas/note.schema.json +40 -0
- package/spec/schemas/proposal.schema.json +131 -4
- package/templates/brain/AGENTS.md +4 -2
- package/templates/brain/brain.json +4 -1
package/dist/lib/sync.js
CHANGED
|
@@ -8,11 +8,11 @@ import { assertStagedValid, checkNoteInput, stageNotes } from "./note-new.js";
|
|
|
8
8
|
import { compareCodePoints } from "./notes.js";
|
|
9
9
|
import { loadSchemas } from "./schemas.js";
|
|
10
10
|
import { formatDate, formatTimestamp, normalizeTimestamp } from "./time.js";
|
|
11
|
-
const DATABASES = ["areas", "goals", "key_results", "projects", "people", "orgs", "decisions", "questions", "review"];
|
|
11
|
+
const DATABASES = ["areas", "goals", "key_results", "projects", "people", "orgs", "decisions", "questions", "concepts", "review"];
|
|
12
12
|
const CREATABLE = new Set(["areas", "goals", "projects", "people", "orgs"]);
|
|
13
|
-
const TYPE_FOR_DATABASE = { areas: "area", goals: "goal", projects: "project", people: "person", orgs: "org", decisions: "decision", questions: "question", review: "proposal" };
|
|
14
|
-
const DATABASE_FOR_TYPE = { area: "areas", goal: "goals", project: "projects", person: "people", org: "orgs", decision: "decisions", question: "questions", meeting: "meetings", proposal: "review" };
|
|
15
|
-
const PUBLISHED_TYPES = new Set(["area", "goal", "project", "person", "org", "decision", "question", "meeting", "proposal"]);
|
|
13
|
+
const TYPE_FOR_DATABASE = { areas: "area", goals: "goal", projects: "project", people: "person", orgs: "org", decisions: "decision", questions: "question", concepts: "concept", review: "proposal" };
|
|
14
|
+
const DATABASE_FOR_TYPE = { area: "areas", goal: "goals", project: "projects", person: "people", org: "orgs", decision: "decisions", question: "questions", concept: "concepts", source: "inbox", meeting: "meetings", proposal: "review" };
|
|
15
|
+
const PUBLISHED_TYPES = new Set(["area", "goal", "project", "person", "org", "decision", "question", "concept", "source", "meeting", "proposal"]);
|
|
16
16
|
function property(value, key) {
|
|
17
17
|
return Object.hasOwn(value, key) ? value[key] : undefined;
|
|
18
18
|
}
|
|
@@ -299,7 +299,7 @@ function planDecision(row, proposal, out) {
|
|
|
299
299
|
return;
|
|
300
300
|
}
|
|
301
301
|
const action = String(decision).toLowerCase();
|
|
302
|
-
const
|
|
302
|
+
const note = property(row.properties, "Note");
|
|
303
303
|
if (!["approve", "reject", "edit"].includes(action)) {
|
|
304
304
|
out.ignored.push({ database: row.database, page_id: row.page_id, brain_id: proposal.id, reason: "NOT_CORRECTABLE" });
|
|
305
305
|
return;
|
|
@@ -308,11 +308,15 @@ function planDecision(row, proposal, out) {
|
|
|
308
308
|
out.conflicts.push(conflict(row, "EDIT_NOT_ALLOWED", `Edit is not allowed on a ${String(proposal.attributes.kind)} proposal`, "Decision", decision));
|
|
309
309
|
return;
|
|
310
310
|
}
|
|
311
|
-
if (action === "edit" && (
|
|
312
|
-
out.conflicts.push(conflict(row, "EDIT_WITHOUT_VALUE", "Edit needs
|
|
311
|
+
if (action === "edit" && (note === null || note === undefined || note === "")) {
|
|
312
|
+
out.conflicts.push(conflict(row, "EDIT_WITHOUT_VALUE", "Edit needs a Note", "Note"));
|
|
313
313
|
return;
|
|
314
314
|
}
|
|
315
|
-
|
|
315
|
+
if (action !== "edit" && typeof note === "string" && note !== "") {
|
|
316
|
+
out.conflicts.push(conflict(row, "NOTE_TO_READ", "Review Note needs interpretation by the skill", "Note", note, decision));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
out.decisions.push({ proposal: proposal.id, action: action, ...(action === "edit" ? { value: note, note: note } : {}), decided_at: row.last_edited, decided_by: "notion" });
|
|
316
320
|
}
|
|
317
321
|
function planKeyResult(workspace, asOf, row, out) {
|
|
318
322
|
const goals = relationIds(workspace, property(row.properties, "Goal"), out.conflicts, row, "Goal");
|
|
@@ -348,7 +352,7 @@ function notionFields(database, p, asOf) {
|
|
|
348
352
|
}
|
|
349
353
|
function brainProperties(database) {
|
|
350
354
|
const common = [["Name", "title"], ["Summary", "summary"], ["Lifecycle", "lifecycle"]];
|
|
351
|
-
const extras = { goals: [["Why", "why"], ["Deadline", "facts.deadline"], ["Area", "area"]], projects: [["Key Facts", "facts"], ["Area", "area"], ["Goal", "goal"], ["Client", "client"], ["People", "people"]], people: [["Role", "facts.role"], ["Org", "org"]], decisions: [["Assessment", "assessment"]], questions: [["Kind", "kind"]] };
|
|
355
|
+
const extras = { goals: [["Why", "why"], ["Deadline", "facts.deadline"], ["Area", "area"]], projects: [["Key Facts", "facts"], ["Area", "area"], ["Goal", "goal"], ["Client", "client"], ["People", "people"]], people: [["Role", "facts.role"], ["Org", "org"]], decisions: [["Assessment", "assessment"], ["Project", "projects"], ["Area", "derived_area"]], questions: [["Kind", "kind"], ["Project", "projects"], ["Area", "derived_area"]], concepts: [["Project", "concept_projects"], ["Area", "concept_area"]] };
|
|
352
356
|
return [...common, ...(extras[database] ?? [])];
|
|
353
357
|
}
|
|
354
358
|
function convertedValue(workspace, row, name, value, conflicts) {
|
|
@@ -511,13 +515,13 @@ function summarize(items) { const result = { applied: 0, closed: 0, recorded: 0,
|
|
|
511
515
|
if (typeof item.result === "string" && Object.hasOwn(result, item.result))
|
|
512
516
|
result[item.result] += 1; return result; }
|
|
513
517
|
export function render(workspace, now, ids, held) {
|
|
514
|
-
const selected = ids === undefined ? workspace.notes.filter((note) => PUBLISHED_TYPES.has(note.type)) : ids.map((id) => workspace.get(id)).filter(Boolean);
|
|
518
|
+
const selected = ids === undefined ? workspace.notes.filter((note) => PUBLISHED_TYPES.has(note.type) && !(note.type === "source" && note.attributes.kind === undefined)) : ids.map((id) => workspace.get(id)).filter(Boolean);
|
|
515
519
|
const items = [];
|
|
516
520
|
const errors = [];
|
|
517
521
|
const warnings = [];
|
|
518
522
|
for (const note of selected) {
|
|
519
523
|
const database = DATABASE_FOR_TYPE[note.type];
|
|
520
|
-
if (database === undefined)
|
|
524
|
+
if (database === undefined || (note.type === "source" && note.attributes.kind === undefined))
|
|
521
525
|
continue;
|
|
522
526
|
const databaseId = databaseIdFor(workspace, database);
|
|
523
527
|
if (databaseId === "") {
|
|
@@ -532,33 +536,39 @@ export function render(workspace, now, ids, held) {
|
|
|
532
536
|
errors.push({ id: note.id, reason: "PROJECT_NOT_IN_NOTION" });
|
|
533
537
|
continue;
|
|
534
538
|
}
|
|
539
|
+
const capturePage = note.type === "source" && note.attributes.kind === "capture" && typeof note.attributes.source_key === "string" && note.attributes.source_key.startsWith("notion:") ? note.attributes.source_key.slice("notion:".length) : undefined;
|
|
540
|
+
if (note.type === "source" && note.attributes.kind === "capture" && (capturePage === undefined || (typeof note.attributes.notion === "string" && note.attributes.notion !== capturePage))) {
|
|
541
|
+
errors.push({ id: note.id, reason: "NOTION_ID_MISMATCH" });
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
535
544
|
const rendered = renderedBrainValues(note, workspace);
|
|
536
545
|
const published = isRecord(note.attributes.notion_published) ? note.attributes.notion_published : {};
|
|
537
546
|
const properties = {};
|
|
538
547
|
const pending = [];
|
|
539
548
|
const heldProperties = held?.get(note.id) ?? new Set();
|
|
549
|
+
const isCreate = note.attributes.notion === undefined && capturePage === undefined;
|
|
540
550
|
for (const [name, value] of Object.entries(rendered)) {
|
|
541
|
-
if (name === "@content" || heldProperties.has(name))
|
|
551
|
+
if (name === "@content" || heldProperties.has(name) || (capturePage !== undefined && name === "Name"))
|
|
542
552
|
continue;
|
|
543
553
|
const encoded = notionProperty(name, value, note, workspace, pending);
|
|
544
|
-
if (encoded !== undefined && (
|
|
554
|
+
if (encoded !== undefined && (isCreate || !Object.hasOwn(published, name) || !sameValue(value, published[name])))
|
|
545
555
|
properties[name] = encoded;
|
|
546
556
|
}
|
|
547
557
|
const content = renderedContent(note, workspace);
|
|
548
558
|
if (content.hash !== undefined)
|
|
549
559
|
rendered["@content"] = content.hash;
|
|
550
560
|
const contentChanged = content.hash !== undefined && !isDeepStrictEqual(content.hash, published["@content"]);
|
|
551
|
-
const isCreate = note.attributes.notion === undefined;
|
|
552
561
|
if (!isCreate && Object.keys(properties).length === 0 && !contentChanged)
|
|
553
562
|
continue;
|
|
554
563
|
if (!isCreate && note.type !== "meeting" && note.type !== "proposal")
|
|
555
564
|
properties["Last Synced"] = { type: "date", value: { start: formatTimestamp(now), end: null } };
|
|
556
|
-
items.push({ id: note.id, type: note.type, database, database_id: databaseId, page_id: typeof note.attributes.notion === "string" ? note.attributes.notion : null, action: isCreate ? "create" : "update", properties, body: content.body, sections: content.sections, pending_relations: pending, published: rendered });
|
|
565
|
+
items.push({ id: note.id, type: note.type, database, database_id: databaseId, page_id: typeof note.attributes.notion === "string" ? note.attributes.notion : capturePage ?? null, action: isCreate ? "create" : "update", properties, body: content.body, sections: content.sections, pending_relations: pending, published: rendered });
|
|
557
566
|
}
|
|
558
567
|
items.sort((a, b) => DATABASES.concat(["meetings"]).indexOf(a.database) - DATABASES.concat(["meetings"]).indexOf(b.database) || compareCodePoints(a.id, b.id));
|
|
559
568
|
return { items, errors, warnings };
|
|
560
569
|
}
|
|
561
|
-
function databaseIdFor(workspace, database) { const notion = workspace.config.notion;
|
|
570
|
+
function databaseIdFor(workspace, database) { const notion = workspace.config.notion; if (!isRecord(notion) || !isRecord(notion.databases))
|
|
571
|
+
return ""; return typeof notion.databases[database] === "string" ? notion.databases[database] : database === "inbox" && typeof notion.databases.capture_inbox === "string" ? notion.databases.capture_inbox : ""; }
|
|
562
572
|
function renderedBrainValues(note, workspace) {
|
|
563
573
|
const a = note.attributes;
|
|
564
574
|
const values = {};
|
|
@@ -566,8 +576,8 @@ function renderedBrainValues(note, workspace) {
|
|
|
566
576
|
if (note.type === "proposal") {
|
|
567
577
|
const target = typeof a.target === "string" ? workspace.get(a.target) : undefined;
|
|
568
578
|
const addLink = a.kind === "add_link";
|
|
569
|
-
const after = addLink
|
|
570
|
-
? `${fieldLabel(
|
|
579
|
+
const after = addLink
|
|
580
|
+
? proposalLinks(a).map((link) => `${fieldLabel(String(link.field))}: ${noteTitle(link.value, workspace)}`).join("\n")
|
|
571
581
|
: undefined;
|
|
572
582
|
const reviewText = (value) => truncate(typeof value === "string" ? reviewMarkdown(value, target) : value);
|
|
573
583
|
Object.assign(values, {
|
|
@@ -581,6 +591,8 @@ function renderedBrainValues(note, workspace) {
|
|
|
581
591
|
Object.assign(values, { Decisions: relationPages(a.decisions, workspace), Questions: relationPages(a.questions, workspace), Attendees: relationPages(a.attendees, workspace), Project: relationPages(a.projects, workspace), Area: relationPages(a.area === undefined ? [] : [a.area], workspace), "Brain Status": "Ingested", "Ingested At": dateOutput(a.created), "Brain ID": note.id });
|
|
582
592
|
return values;
|
|
583
593
|
}
|
|
594
|
+
if (note.type === "source")
|
|
595
|
+
return sourceValues(note, workspace);
|
|
584
596
|
common();
|
|
585
597
|
if (note.type === "goal")
|
|
586
598
|
Object.assign(values, { Why: a.why ?? null, Deadline: dateOutput(isRecord(a.facts) ? a.facts.deadline : undefined), Area: relationPages(a.area === undefined ? [] : [a.area], workspace) });
|
|
@@ -589,20 +601,38 @@ function renderedBrainValues(note, workspace) {
|
|
|
589
601
|
if (note.type === "person")
|
|
590
602
|
Object.assign(values, { Role: isRecord(a.facts) ? a.facts.role ?? null : null, Org: relationPages(a.org === undefined ? [] : [a.org], workspace) });
|
|
591
603
|
if (note.type === "decision")
|
|
592
|
-
Object.assign(values, { Date: dateOutput(a.date), Assessment: select(a.assessment), Project: relationPages(a.projects, workspace), "Decided In": relationPages(a.decided_in === undefined ? [] : [a.decided_in], workspace), Supersedes: relationPages((Array.isArray(a.relations) ? a.relations.filter(isRecord).filter((x) => x.type === "supersedes").map((x) => x.target) : []), workspace) });
|
|
604
|
+
Object.assign(values, { Date: dateOutput(a.date), Assessment: select(a.assessment), Area: relationPages(projectAreas(a.projects, workspace), workspace), Project: relationPages(a.projects, workspace), "Decided In": relationPages(a.decided_in === undefined ? [] : [a.decided_in], workspace), Supersedes: relationPages((Array.isArray(a.relations) ? a.relations.filter(isRecord).filter((x) => x.type === "supersedes").map((x) => x.target) : []), workspace) });
|
|
593
605
|
if (note.type === "question")
|
|
594
|
-
Object.assign(values, { Kind: select(a.kind), Project: relationPages(a.projects, workspace), "Raised In": relationPages(a.raised_in === undefined ? [] : [a.raised_in], workspace) });
|
|
606
|
+
Object.assign(values, { Kind: select(a.kind), Area: relationPages(projectAreas(a.projects, workspace), workspace), Project: relationPages(a.projects, workspace), "Raised In": relationPages(a.raised_in === undefined ? [] : [a.raised_in], workspace) });
|
|
607
|
+
if (note.type === "concept")
|
|
608
|
+
Object.assign(values, { Project: relationPages(a.projects, workspace), Area: relationPages(a.area === undefined ? [] : [a.area], workspace) });
|
|
595
609
|
return values;
|
|
596
610
|
}
|
|
611
|
+
/** Area on decisions and questions is a derived Notion relation (decision 0034). */
|
|
612
|
+
function projectAreas(projects, workspace) {
|
|
613
|
+
const areas = (Array.isArray(projects) ? projects : []).flatMap((project) => {
|
|
614
|
+
const area = workspace.get(String(project))?.attributes.area;
|
|
615
|
+
return typeof area === "string" ? [area] : [];
|
|
616
|
+
});
|
|
617
|
+
return [...new Set(areas)].sort(compareCodePoints);
|
|
618
|
+
}
|
|
619
|
+
function sourceValues(note, workspace) {
|
|
620
|
+
const produced = workspace.notes.filter((candidate) => ["decision", "question", "concept", "person"].includes(candidate.type) && Array.isArray(candidate.attributes.sources) && candidate.attributes.sources.includes(note.id));
|
|
621
|
+
const ids = (type) => produced.filter((candidate) => candidate.type === type).map((candidate) => candidate.id).sort(compareCodePoints);
|
|
622
|
+
const projects = [...new Set(produced.flatMap((candidate) => Array.isArray(candidate.attributes.projects) ? candidate.attributes.projects.filter((id) => typeof id === "string") : []))].sort(compareCodePoints);
|
|
623
|
+
const areas = [...new Set([...produced.map((candidate) => candidate.attributes.area).filter((id) => typeof id === "string"), ...projects.flatMap((id) => { const area = workspace.get(id)?.attributes.area; return typeof area === "string" ? [area] : []; })])].sort(compareCodePoints);
|
|
624
|
+
return { Name: note.attributes.title, Kind: select(note.attributes.kind), Summary: note.attributes.summary ?? null, Decisions: relationPages(ids("decision"), workspace), Questions: relationPages(ids("question"), workspace), Concepts: relationPages(ids("concept"), workspace), People: relationPages(ids("person"), workspace), Project: relationPages(projects, workspace), Area: relationPages(areas, workspace), "Brain Status": "Processed", "Brain ID": note.id };
|
|
625
|
+
}
|
|
597
626
|
function notionProperty(name, value, note, workspace, pending) {
|
|
598
|
-
const relation = ["Area", "Goal", "Client", "People", "Org", "Project", "Decided In", "Raised In", "Supersedes", "Decisions", "Questions", "Attendees"].includes(name);
|
|
627
|
+
const relation = ["Area", "Goal", "Client", "People", "Org", "Project", "Decided In", "Raised In", "Supersedes", "Decisions", "Questions", "Concepts", "Attendees"].includes(name);
|
|
599
628
|
if (relation) {
|
|
600
629
|
const sourceField = { Area: "area", Goal: "goal", Client: "client", People: "people", Org: "org", Project: "projects", "Decided In": "decided_in", "Raised In": "raised_in", Decisions: "decisions", Questions: "questions", Attendees: "attendees" };
|
|
601
|
-
const raw = name === "Supersedes" ? (Array.isArray(note.attributes.relations) ? note.attributes.relations.filter(isRecord).filter((item) => item.type === "supersedes").map((item) => item.target) : []) : note.attributes[sourceField[name] ?? ""];
|
|
630
|
+
const raw = name === "Area" && (note.type === "decision" || note.type === "question") ? projectAreas(note.attributes.projects, workspace) : name === "Supersedes" ? (Array.isArray(note.attributes.relations) ? note.attributes.relations.filter(isRecord).filter((item) => item.type === "supersedes").map((item) => item.target) : []) : note.type === "source" ? sourceRelationIds(note, name, workspace) : note.attributes[sourceField[name] ?? ""];
|
|
602
631
|
const missing = (Array.isArray(raw) ? raw : raw === undefined ? [] : [raw]).map(String).filter((id) => typeof workspace.get(id)?.attributes.notion !== "string");
|
|
603
632
|
if (missing.length > 0)
|
|
604
633
|
pending.push({ property: name, ids: missing });
|
|
605
|
-
|
|
634
|
+
const mode = (note.type === "meeting" || note.type === "source") ? (["Project", "Area"].includes(name) ? "if_empty" : note.type === "meeting" ? "add" : undefined) : undefined;
|
|
635
|
+
return { type: "relation", value: Array.isArray(value) ? value : [], ...(mode === undefined ? {} : { mode }) };
|
|
606
636
|
}
|
|
607
637
|
if (["Lifecycle", "Assessment", "Kind", "State", "Brain Status"].includes(name))
|
|
608
638
|
return { type: "select", value };
|
|
@@ -610,7 +640,29 @@ function notionProperty(name, value, note, workspace, pending) {
|
|
|
610
640
|
return { type: "date", value };
|
|
611
641
|
return { type: name === "Name" ? "title" : "rich_text", value };
|
|
612
642
|
}
|
|
613
|
-
function
|
|
643
|
+
function sourceRelationIds(note, name, workspace) {
|
|
644
|
+
const produced = workspace.notes.filter((candidate) => ["decision", "question", "concept", "person"].includes(candidate.type) && Array.isArray(candidate.attributes.sources) && candidate.attributes.sources.includes(note.id));
|
|
645
|
+
if (name === "Decisions")
|
|
646
|
+
return produced.filter((candidate) => candidate.type === "decision").map((candidate) => candidate.id);
|
|
647
|
+
if (name === "Questions")
|
|
648
|
+
return produced.filter((candidate) => candidate.type === "question").map((candidate) => candidate.id);
|
|
649
|
+
if (name === "Concepts")
|
|
650
|
+
return produced.filter((candidate) => candidate.type === "concept").map((candidate) => candidate.id);
|
|
651
|
+
if (name === "People")
|
|
652
|
+
return produced.filter((candidate) => candidate.type === "person").map((candidate) => candidate.id);
|
|
653
|
+
const projects = [...new Set(produced.flatMap((candidate) => Array.isArray(candidate.attributes.projects) ? candidate.attributes.projects.filter((id) => typeof id === "string") : []))];
|
|
654
|
+
if (name === "Project")
|
|
655
|
+
return projects;
|
|
656
|
+
if (name === "Area")
|
|
657
|
+
return [...new Set([...produced.map((candidate) => candidate.attributes.area).filter((id) => typeof id === "string"), ...projects.flatMap((id) => { const area = workspace.get(id)?.attributes.area; return typeof area === "string" ? [area] : []; })])];
|
|
658
|
+
return [];
|
|
659
|
+
}
|
|
660
|
+
function proposalLinks(attributes) {
|
|
661
|
+
if (Array.isArray(attributes.links))
|
|
662
|
+
return attributes.links.filter(isRecord).map((link) => ({ field: link.field, value: link.value }));
|
|
663
|
+
return attributes.field === undefined ? [] : [{ field: attributes.field, value: attributes.after }];
|
|
664
|
+
}
|
|
665
|
+
function renderedContent(note, workspace) { if (note.type === "decision" || note.type === "question" || note.type === "concept") {
|
|
614
666
|
const body = notionMarkdown(note.body, note, workspace);
|
|
615
667
|
return { body, sections: null, hash: hash(body) };
|
|
616
668
|
} if (note.type === "project") {
|
|
@@ -667,17 +719,22 @@ function proposalAfter(attributes, workspace) {
|
|
|
667
719
|
return noteTitle(attributes.merge_into, workspace);
|
|
668
720
|
return attributes.after ?? null;
|
|
669
721
|
}
|
|
670
|
-
export function renderTasks(workspace,
|
|
722
|
+
export function renderTasks(workspace, targetId, input) {
|
|
671
723
|
if (!isRecord(input) || !Array.isArray(input.items))
|
|
672
724
|
throw new BrainError("INPUT_INVALID", "tasks input needs an items list");
|
|
673
725
|
const config = tasksConfig(workspace);
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
726
|
+
const target = workspace.get(targetId);
|
|
727
|
+
const sourceMode = target?.type === "source";
|
|
728
|
+
if (target === undefined)
|
|
729
|
+
throw new BrainError("UNKNOWN_ID", `No meeting or source has id ${targetId}`);
|
|
730
|
+
if (target.type !== "meeting" && !sourceMode)
|
|
731
|
+
throw new BrainError("INPUT_INVALID", `${targetId} is not a meeting or source`);
|
|
732
|
+
if (sourceMode && target.attributes.kind === undefined)
|
|
733
|
+
throw new BrainError("INPUT_INVALID", "minutes sources use their meeting for tasks");
|
|
677
734
|
const tasks = input.items.map((item, index) => {
|
|
678
735
|
if (!isRecord(item) || typeof item.title !== "string" || item.title === "")
|
|
679
736
|
throw new BrainError("INPUT_INVALID", "each task needs a title");
|
|
680
|
-
const projects = taskProjects(workspace,
|
|
737
|
+
const projects = taskProjects(workspace, sourceMode ? undefined : target, item.projects, index);
|
|
681
738
|
const properties = {};
|
|
682
739
|
const pending_relations = [];
|
|
683
740
|
const put = (key, type, value) => { const name = config.properties[key]; if (typeof name === "string" && name !== "")
|
|
@@ -686,7 +743,7 @@ export function renderTasks(workspace, meetingId, input) {
|
|
|
686
743
|
put("status", "select", config.new_task_status);
|
|
687
744
|
if (item.due !== undefined)
|
|
688
745
|
put("due", "date", dateOutput(item.due));
|
|
689
|
-
if (item.owner !== undefined)
|
|
746
|
+
if (!sourceMode && item.owner !== undefined)
|
|
690
747
|
put("owner", "person_name", item.owner);
|
|
691
748
|
const projectProperty = config.properties.project;
|
|
692
749
|
if (typeof projectProperty === "string" && projectProperty !== "") {
|
|
@@ -695,19 +752,28 @@ export function renderTasks(workspace, meetingId, input) {
|
|
|
695
752
|
pending_relations.push({ property: projectProperty, ids: unpublished });
|
|
696
753
|
put("project", "relation", publishedPages(projects, workspace));
|
|
697
754
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
755
|
+
const areas = sourceMode ? [...new Set(projects.flatMap((id) => { const area = workspace.get(id)?.attributes.area; return typeof area === "string" ? [area] : []; }))] : target.attributes.area === undefined ? [] : [target.attributes.area];
|
|
756
|
+
put("area", "relation", publishedPages(areas, workspace));
|
|
757
|
+
if (sourceMode) {
|
|
758
|
+
if (typeof config.properties.source === "string" && config.properties.source !== "") {
|
|
759
|
+
if (typeof target.attributes.notion !== "string")
|
|
760
|
+
pending_relations.push({ property: config.properties.source, ids: [target.id] });
|
|
761
|
+
else
|
|
762
|
+
put("source", "relation", [target.attributes.notion]);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
else if (config.properties.meeting !== "")
|
|
766
|
+
put("meeting", "relation", [target.attributes.notion]);
|
|
701
767
|
else
|
|
702
|
-
put("meeting_url", "url",
|
|
703
|
-
put("brain_id", "rich_text",
|
|
768
|
+
put("meeting_url", "url", target.attributes.notion === undefined ? undefined : `https://www.notion.so/${String(target.attributes.notion).replaceAll("-", "")}`);
|
|
769
|
+
put("brain_id", "rich_text", target.id);
|
|
704
770
|
return { database_id: config.database, properties, pending_relations };
|
|
705
771
|
});
|
|
706
772
|
return { items: tasks };
|
|
707
773
|
}
|
|
708
774
|
function taskProjects(workspace, meeting, value, itemIndex) {
|
|
709
775
|
if (value === undefined || (Array.isArray(value) && value.length === 0))
|
|
710
|
-
return Array.isArray(meeting
|
|
776
|
+
return Array.isArray(meeting?.attributes.projects) ? meeting.attributes.projects.filter((id) => typeof id === "string") : [];
|
|
711
777
|
if (!Array.isArray(value))
|
|
712
778
|
throw new BrainError("INPUT_INVALID", "task projects must be an array", [{ pointer: `/items/${itemIndex}/projects`, message: "must be an array of project ids" }]);
|
|
713
779
|
const problems = [];
|
|
@@ -757,9 +823,9 @@ export function record(workspace, input) {
|
|
|
757
823
|
}
|
|
758
824
|
if (isRecord(entry.published))
|
|
759
825
|
doc.attributes.notion_published = { ...(isRecord(doc.attributes.notion_published) ? doc.attributes.notion_published : {}), ...entry.published };
|
|
760
|
-
if (note.type !== "
|
|
826
|
+
if (note.type !== "view" && note.type !== "proposal")
|
|
761
827
|
doc.attributes.last_synced = syncedAt;
|
|
762
|
-
if (note.type === "meeting" && Array.isArray(entry.tasks))
|
|
828
|
+
if ((note.type === "meeting" || note.type === "source") && Array.isArray(entry.tasks))
|
|
763
829
|
doc.attributes.tasks = [...new Set([...(Array.isArray(doc.attributes.tasks) ? doc.attributes.tasks : []), ...entry.tasks.filter((id) => typeof id === "string")])];
|
|
764
830
|
workspace.save(note.path, doc);
|
|
765
831
|
const errors = workspace.newErrors();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-brain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "CLI for an AI-assisted Markdown and Git second brain",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc --project tsconfig.build.json",
|
|
31
31
|
"prepack": "npm run build",
|
|
32
|
+
"skills": "node --experimental-strip-types scripts/build-skills.ts && node --experimental-strip-types --test tests/skills.test.ts",
|
|
32
33
|
"test:package": "node scripts/test-package.mjs",
|
|
33
34
|
"test:scripts": "node --test tests/release-scripts.test.mjs",
|
|
34
35
|
"test": "node --experimental-strip-types --test tests/*.test.ts tests/*.test.mjs",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"name": {
|
|
26
26
|
"type": "string",
|
|
27
27
|
"minLength": 1
|
|
28
|
+
},
|
|
29
|
+
"person": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"pattern": "^per-[0-9abcdefghjkmnpqrstvwxyz]{8}$"
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
},
|
|
@@ -40,6 +44,26 @@
|
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
},
|
|
47
|
+
"settings": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"additionalProperties": false,
|
|
50
|
+
"properties": {
|
|
51
|
+
"publish": {
|
|
52
|
+
"enum": [
|
|
53
|
+
"ask",
|
|
54
|
+
"auto"
|
|
55
|
+
],
|
|
56
|
+
"default": "ask"
|
|
57
|
+
},
|
|
58
|
+
"push": {
|
|
59
|
+
"enum": [
|
|
60
|
+
"auto",
|
|
61
|
+
"remind"
|
|
62
|
+
],
|
|
63
|
+
"default": "auto"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
43
67
|
"notion": {
|
|
44
68
|
"type": "object",
|
|
45
69
|
"required": [
|
|
@@ -83,6 +107,14 @@
|
|
|
83
107
|
"$ref": "#/$defs/dbId"
|
|
84
108
|
},
|
|
85
109
|
"capture_inbox": {
|
|
110
|
+
"$ref": "#/$defs/dbId",
|
|
111
|
+
"deprecated": true,
|
|
112
|
+
"description": "Old name of inbox; read as inbox when inbox is absent. brain upgrade renames it."
|
|
113
|
+
},
|
|
114
|
+
"inbox": {
|
|
115
|
+
"$ref": "#/$defs/dbId"
|
|
116
|
+
},
|
|
117
|
+
"concepts": {
|
|
86
118
|
"$ref": "#/$defs/dbId"
|
|
87
119
|
}
|
|
88
120
|
}
|
|
@@ -130,6 +162,9 @@
|
|
|
130
162
|
"meeting_url": {
|
|
131
163
|
"type": "string"
|
|
132
164
|
},
|
|
165
|
+
"source": {
|
|
166
|
+
"type": "string"
|
|
167
|
+
},
|
|
133
168
|
"brain_id": {
|
|
134
169
|
"type": "string"
|
|
135
170
|
}
|
|
@@ -833,6 +833,27 @@
|
|
|
833
833
|
"converter": {
|
|
834
834
|
"type": "string",
|
|
835
835
|
"minLength": 1
|
|
836
|
+
},
|
|
837
|
+
"summary": {
|
|
838
|
+
"type": "string",
|
|
839
|
+
"minLength": 1,
|
|
840
|
+
"maxLength": 400
|
|
841
|
+
},
|
|
842
|
+
"notion": {
|
|
843
|
+
"$ref": "#/$defs/notionPageId"
|
|
844
|
+
},
|
|
845
|
+
"notion_published": {
|
|
846
|
+
"description": "Notion property name → value last published to the Inbox (spec/cli.md §5.14).",
|
|
847
|
+
"type": "object"
|
|
848
|
+
},
|
|
849
|
+
"last_synced": {
|
|
850
|
+
"$ref": "#/$defs/timestamp"
|
|
851
|
+
},
|
|
852
|
+
"tasks": {
|
|
853
|
+
"type": "array",
|
|
854
|
+
"items": {
|
|
855
|
+
"$ref": "#/$defs/notionPageId"
|
|
856
|
+
}
|
|
836
857
|
}
|
|
837
858
|
},
|
|
838
859
|
"not": {
|
|
@@ -896,6 +917,25 @@
|
|
|
896
917
|
}
|
|
897
918
|
}
|
|
898
919
|
}
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
"if": {
|
|
923
|
+
"required": [
|
|
924
|
+
"kind"
|
|
925
|
+
],
|
|
926
|
+
"properties": {
|
|
927
|
+
"kind": {
|
|
928
|
+
"const": "capture"
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
"then": {
|
|
933
|
+
"properties": {
|
|
934
|
+
"source_key": {
|
|
935
|
+
"pattern": "^notion:"
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
899
939
|
}
|
|
900
940
|
]
|
|
901
941
|
},
|
|
@@ -104,6 +104,32 @@
|
|
|
104
104
|
"value": {
|
|
105
105
|
"$ref": "note.schema.json#/$defs/factValue"
|
|
106
106
|
},
|
|
107
|
+
"note": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"minLength": 1
|
|
110
|
+
},
|
|
111
|
+
"exclude": {
|
|
112
|
+
"type": "array",
|
|
113
|
+
"minItems": 1,
|
|
114
|
+
"uniqueItems": true,
|
|
115
|
+
"items": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"required": [
|
|
118
|
+
"field",
|
|
119
|
+
"value"
|
|
120
|
+
],
|
|
121
|
+
"additionalProperties": false,
|
|
122
|
+
"properties": {
|
|
123
|
+
"field": {
|
|
124
|
+
"type": "string"
|
|
125
|
+
},
|
|
126
|
+
"value": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"minLength": 1
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
107
133
|
"decided_at": {
|
|
108
134
|
"$ref": "note.schema.json#/$defs/timestamp"
|
|
109
135
|
},
|
|
@@ -170,6 +196,35 @@
|
|
|
170
196
|
"notion_published": {
|
|
171
197
|
"description": "Review property name → value last published (spec/cli.md §5.14).",
|
|
172
198
|
"type": "object"
|
|
199
|
+
},
|
|
200
|
+
"links": {
|
|
201
|
+
"type": "array",
|
|
202
|
+
"minItems": 1,
|
|
203
|
+
"uniqueItems": true,
|
|
204
|
+
"items": {
|
|
205
|
+
"type": "object",
|
|
206
|
+
"required": [
|
|
207
|
+
"field",
|
|
208
|
+
"value"
|
|
209
|
+
],
|
|
210
|
+
"additionalProperties": false,
|
|
211
|
+
"properties": {
|
|
212
|
+
"field": {
|
|
213
|
+
"enum": [
|
|
214
|
+
"aliases",
|
|
215
|
+
"tags",
|
|
216
|
+
"projects",
|
|
217
|
+
"people",
|
|
218
|
+
"attendees",
|
|
219
|
+
"deciders"
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
"value": {
|
|
223
|
+
"type": "string",
|
|
224
|
+
"minLength": 1
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
173
228
|
}
|
|
174
229
|
},
|
|
175
230
|
"additionalProperties": false,
|
|
@@ -237,9 +292,7 @@
|
|
|
237
292
|
},
|
|
238
293
|
"then": {
|
|
239
294
|
"required": [
|
|
240
|
-
"target"
|
|
241
|
-
"field",
|
|
242
|
-
"after"
|
|
295
|
+
"target"
|
|
243
296
|
],
|
|
244
297
|
"properties": {
|
|
245
298
|
"field": {
|
|
@@ -275,7 +328,39 @@
|
|
|
275
328
|
]
|
|
276
329
|
}
|
|
277
330
|
]
|
|
278
|
-
}
|
|
331
|
+
},
|
|
332
|
+
"oneOf": [
|
|
333
|
+
{
|
|
334
|
+
"required": [
|
|
335
|
+
"links"
|
|
336
|
+
],
|
|
337
|
+
"not": {
|
|
338
|
+
"anyOf": [
|
|
339
|
+
{
|
|
340
|
+
"required": [
|
|
341
|
+
"field"
|
|
342
|
+
]
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"required": [
|
|
346
|
+
"after"
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"required": [
|
|
354
|
+
"field",
|
|
355
|
+
"after"
|
|
356
|
+
],
|
|
357
|
+
"not": {
|
|
358
|
+
"required": [
|
|
359
|
+
"links"
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
]
|
|
279
364
|
}
|
|
280
365
|
},
|
|
281
366
|
{
|
|
@@ -554,6 +639,48 @@
|
|
|
554
639
|
}
|
|
555
640
|
}
|
|
556
641
|
}
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
"if": {
|
|
645
|
+
"required": [
|
|
646
|
+
"links"
|
|
647
|
+
]
|
|
648
|
+
},
|
|
649
|
+
"then": {
|
|
650
|
+
"properties": {
|
|
651
|
+
"kind": {
|
|
652
|
+
"const": "add_link"
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
"if": {
|
|
659
|
+
"required": [
|
|
660
|
+
"decision"
|
|
661
|
+
],
|
|
662
|
+
"properties": {
|
|
663
|
+
"decision": {
|
|
664
|
+
"required": [
|
|
665
|
+
"exclude"
|
|
666
|
+
]
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
"then": {
|
|
671
|
+
"properties": {
|
|
672
|
+
"kind": {
|
|
673
|
+
"const": "add_link"
|
|
674
|
+
},
|
|
675
|
+
"decision": {
|
|
676
|
+
"properties": {
|
|
677
|
+
"action": {
|
|
678
|
+
"const": "approve"
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
557
684
|
}
|
|
558
685
|
]
|
|
559
686
|
}
|
|
@@ -13,7 +13,8 @@ Each operation has a skill; follow it end to end:
|
|
|
13
13
|
| The user wants to… | Skill |
|
|
14
14
|
|---|---|
|
|
15
15
|
| connect the brain to Notion (after building the space) | `brain-connect-notion` |
|
|
16
|
-
|
|
|
16
|
+
| catch up: do everything that's waiting (decided reviews, reviewed meetings, the Inbox, the drop folder) | `brain-catch-up` |
|
|
17
|
+
| ingest one input: reviewed minutes, a document, a web page, a pasted email or note, the Inbox, the drop folder | `brain-ingest` |
|
|
17
18
|
| apply decisions from the Notion Review queue | `brain-apply` |
|
|
18
19
|
| pull Notion edits and new rows, republish notes | `brain-sync` |
|
|
19
20
|
| answer a question from the brain | `brain-query` |
|
|
@@ -28,7 +29,8 @@ Each operation has a skill; follow it end to end:
|
|
|
28
29
|
- **A new decision or question for a meeting that already exists**: a `new_item` proposal, because the meeting note must change too.
|
|
29
30
|
- **Generated files** (`index.md`, `log/`, the `@generated` blocks of `brief.md` and `decision-log.md`) and **source snapshots** in `sources/` are written by the CLI only. To change what a view shows, change the canonical note and regenerate.
|
|
30
31
|
- **After every write:** `brain index`, `brain views`, `brain lint`. Commit when lint shows no errors you introduced, e.g. `ingest: Acme sync 2026-12-03 (4 new, 3 proposals)`. A run that publishes to Notion commits again after `brain sync record`, so the stored page ids are in Git too.
|
|
31
|
-
- **
|
|
32
|
+
- **Publishing** follows `settings.publish` in `brain.json` (see `brain status`): `ask` (the default) asks once per run before writing to Notion; `auto` publishes without asking. After a run the user was happy with and that asked, offer once to switch: `brain config set settings.publish '"auto"'`.
|
|
33
|
+
- **After the last commit,** per `settings.push`: `auto` (the default) runs `git push`; if it fails (offline, no remote), say so in the report's last line and carry on, the work is committed. `remind` leaves it to the user, and the report's last line says how many commits are not pushed.
|
|
32
34
|
|
|
33
35
|
## Product files and findings
|
|
34
36
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"owner": { "name": "Owner" },
|
|
4
4
|
"budgets": { "ingest_max_notes": 8 },
|
|
5
|
+
"settings": { "publish": "ask", "push": "auto" },
|
|
5
6
|
"notion": {
|
|
6
7
|
"databases": {
|
|
7
8
|
"areas": "",
|
|
@@ -14,7 +15,8 @@
|
|
|
14
15
|
"decisions": "",
|
|
15
16
|
"questions": "",
|
|
16
17
|
"review": "",
|
|
17
|
-
"
|
|
18
|
+
"inbox": "",
|
|
19
|
+
"concepts": ""
|
|
18
20
|
},
|
|
19
21
|
"tasks": {
|
|
20
22
|
"database": "",
|
|
@@ -27,6 +29,7 @@
|
|
|
27
29
|
"owner": "",
|
|
28
30
|
"meeting": "Meeting",
|
|
29
31
|
"meeting_url": "",
|
|
32
|
+
"source": "Source",
|
|
30
33
|
"brain_id": "Brain ID"
|
|
31
34
|
},
|
|
32
35
|
"new_task_status": "To Do"
|