open-coleslaw 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +137 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3810,6 +3810,126 @@ connect();
|
|
|
3810
3810
|
|
|
3811
3811
|
// src/dashboard/state-bridge.ts
|
|
3812
3812
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
3813
|
+
|
|
3814
|
+
// src/dashboard/minutes-hydrator.ts
|
|
3815
|
+
import { readdir, readFile } from "fs/promises";
|
|
3816
|
+
import { join as join5 } from "path";
|
|
3817
|
+
var MINUTES_DIR = "docs/open-coleslaw";
|
|
3818
|
+
async function hydratePastMeetings(projectPath, limit = 20) {
|
|
3819
|
+
const dir = join5(projectPath, MINUTES_DIR);
|
|
3820
|
+
let files;
|
|
3821
|
+
try {
|
|
3822
|
+
files = await readdir(dir);
|
|
3823
|
+
} catch {
|
|
3824
|
+
return [];
|
|
3825
|
+
}
|
|
3826
|
+
const minutesFiles = files.filter((f) => f.endsWith(".md")).filter((f) => f !== "INDEX.md").filter((f) => !f.startsWith(".")).sort().reverse();
|
|
3827
|
+
const threads = [];
|
|
3828
|
+
for (const filename of minutesFiles) {
|
|
3829
|
+
if (threads.length >= limit) break;
|
|
3830
|
+
try {
|
|
3831
|
+
const content = await readFile(join5(dir, filename), "utf-8");
|
|
3832
|
+
const parsed = parseMinutesMarkdown(filename, content);
|
|
3833
|
+
if (parsed) threads.push(parsed);
|
|
3834
|
+
} catch (err) {
|
|
3835
|
+
logger.warn(
|
|
3836
|
+
`minutes-hydrator: failed to parse ${filename}: ${err instanceof Error ? err.message : String(err)}`
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
return threads;
|
|
3841
|
+
}
|
|
3842
|
+
function parseMinutesMarkdown(filename, md) {
|
|
3843
|
+
const topic = grabField(md, "Topic") || firstH1(md) || filename.replace(/\.md$/, "");
|
|
3844
|
+
const meetingId = grabField(md, "MeetingId") || grabField(md, "meetingId") || `hydrated:${filename}`;
|
|
3845
|
+
const date = grabField(md, "Date") || inferDateFromFilename(filename) || "";
|
|
3846
|
+
const type = inferTypeFromFilename(filename);
|
|
3847
|
+
const participants = parseParticipants(md);
|
|
3848
|
+
const decisions = extractBulletedSection(md, "Decisions");
|
|
3849
|
+
const actionItems = extractBulletedSection(md, "Action Items");
|
|
3850
|
+
const agenda = extractNumberedSection(md, "Agenda");
|
|
3851
|
+
const startedAt = date ? Date.parse(date) || Date.now() : Date.now();
|
|
3852
|
+
return {
|
|
3853
|
+
meetingId,
|
|
3854
|
+
meetingType: type,
|
|
3855
|
+
topic,
|
|
3856
|
+
agenda,
|
|
3857
|
+
participants,
|
|
3858
|
+
status: "completed",
|
|
3859
|
+
phase: "minutes-generation",
|
|
3860
|
+
comments: [],
|
|
3861
|
+
mvps: [],
|
|
3862
|
+
decisions,
|
|
3863
|
+
actionItems,
|
|
3864
|
+
startedAt,
|
|
3865
|
+
completedAt: startedAt
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
function grabField(md, name) {
|
|
3869
|
+
const re = new RegExp(
|
|
3870
|
+
`^\\s*-?\\s*\\*{0,2}\\s*${escapeRegex(name)}\\s*\\*{0,2}\\s*:\\s*\\*{0,2}\\s*(.+?)\\s*\\*{0,2}\\s*$`,
|
|
3871
|
+
"im"
|
|
3872
|
+
);
|
|
3873
|
+
const m = md.match(re);
|
|
3874
|
+
if (!m) return null;
|
|
3875
|
+
return m[1].replace(/^\**/, "").replace(/\**$/, "").trim();
|
|
3876
|
+
}
|
|
3877
|
+
function firstH1(md) {
|
|
3878
|
+
const m = md.match(/^#\s+(.+)$/m);
|
|
3879
|
+
if (!m) return null;
|
|
3880
|
+
return m[1].replace(/^Meeting Minutes\s*[—-]\s*/i, "").trim();
|
|
3881
|
+
}
|
|
3882
|
+
function parseParticipants(md) {
|
|
3883
|
+
const field = grabField(md, "Participants");
|
|
3884
|
+
if (!field) return [];
|
|
3885
|
+
return field.replace(/\*+/g, "").split(/[,、]/).map((s) => s.trim()).filter(Boolean);
|
|
3886
|
+
}
|
|
3887
|
+
function extractBulletedSection(md, heading) {
|
|
3888
|
+
const lines = md.split("\n");
|
|
3889
|
+
const out = [];
|
|
3890
|
+
let inSection = false;
|
|
3891
|
+
for (const line of lines) {
|
|
3892
|
+
const h = line.match(/^#{2,3}\s+(.+)$/);
|
|
3893
|
+
if (h) {
|
|
3894
|
+
inSection = h[1].trim().toLowerCase().startsWith(heading.toLowerCase());
|
|
3895
|
+
continue;
|
|
3896
|
+
}
|
|
3897
|
+
if (!inSection) continue;
|
|
3898
|
+
const m = line.match(/^\s*[-*]\s+(?:\[[ xX]\]\s+)?(.*\S)\s*$/);
|
|
3899
|
+
if (m) out.push(m[1]);
|
|
3900
|
+
}
|
|
3901
|
+
return out;
|
|
3902
|
+
}
|
|
3903
|
+
function extractNumberedSection(md, heading) {
|
|
3904
|
+
const lines = md.split("\n");
|
|
3905
|
+
const out = [];
|
|
3906
|
+
let inSection = false;
|
|
3907
|
+
for (const line of lines) {
|
|
3908
|
+
const h = line.match(/^#{2,3}\s+(.+)$/);
|
|
3909
|
+
if (h) {
|
|
3910
|
+
inSection = h[1].trim().toLowerCase().startsWith(heading.toLowerCase());
|
|
3911
|
+
continue;
|
|
3912
|
+
}
|
|
3913
|
+
if (!inSection) continue;
|
|
3914
|
+
const m = line.match(/^\s*\d+\.\s+(.+\S)\s*$/);
|
|
3915
|
+
if (m) out.push(m[1]);
|
|
3916
|
+
}
|
|
3917
|
+
return out;
|
|
3918
|
+
}
|
|
3919
|
+
function inferTypeFromFilename(filename) {
|
|
3920
|
+
if (filename.includes("_kickoff_")) return "kickoff";
|
|
3921
|
+
if (filename.includes("_verify-retry_") || filename.includes("-verify-retry")) return "verify-retry";
|
|
3922
|
+
return "design";
|
|
3923
|
+
}
|
|
3924
|
+
function inferDateFromFilename(filename) {
|
|
3925
|
+
const m = filename.match(/^(\d{4}-\d{2}-\d{2})_/);
|
|
3926
|
+
return m ? m[1] : null;
|
|
3927
|
+
}
|
|
3928
|
+
function escapeRegex(s) {
|
|
3929
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
// src/dashboard/state-bridge.ts
|
|
3813
3933
|
var MAX_PAST_MEETINGS = 20;
|
|
3814
3934
|
var EVENT_DEBOUNCE_MS = 100;
|
|
3815
3935
|
var StateBridge = class extends EventEmitter2 {
|
|
@@ -3852,6 +3972,20 @@ var StateBridge = class extends EventEmitter2 {
|
|
|
3852
3972
|
this.projects.set(info.projectPath, state);
|
|
3853
3973
|
this.sessionToProject.set(info.sessionId, info.projectPath);
|
|
3854
3974
|
logger.info(`Project registered: ${displayName} (${info.projectPath})`);
|
|
3975
|
+
hydratePastMeetings(info.projectPath).then((past) => {
|
|
3976
|
+
if (past.length === 0) return;
|
|
3977
|
+
const current = this.projects.get(info.projectPath);
|
|
3978
|
+
if (!current) return;
|
|
3979
|
+
current.pastMeetings = past;
|
|
3980
|
+
logger.info(
|
|
3981
|
+
`Hydrated ${past.length} past meeting(s) for ${displayName}`
|
|
3982
|
+
);
|
|
3983
|
+
this.emit("broadcast", JSON.stringify(this.getSnapshot()));
|
|
3984
|
+
}).catch((err) => {
|
|
3985
|
+
logger.warn(
|
|
3986
|
+
`Failed to hydrate past meetings for ${displayName}: ${err instanceof Error ? err.message : String(err)}`
|
|
3987
|
+
);
|
|
3988
|
+
});
|
|
3855
3989
|
this.emit(
|
|
3856
3990
|
"broadcast",
|
|
3857
3991
|
JSON.stringify({
|
|
@@ -4084,13 +4218,13 @@ var DashboardClient = class {
|
|
|
4084
4218
|
|
|
4085
4219
|
// src/dashboard/comment-queue.ts
|
|
4086
4220
|
import { appendFile, mkdir } from "fs/promises";
|
|
4087
|
-
import { join as
|
|
4221
|
+
import { join as join6 } from "path";
|
|
4088
4222
|
var DIR = "docs/open-coleslaw";
|
|
4089
4223
|
var PENDING = ".pending-comments.jsonl";
|
|
4090
4224
|
async function enqueueComment(projectPath, entry) {
|
|
4091
|
-
const dir =
|
|
4225
|
+
const dir = join6(projectPath, DIR);
|
|
4092
4226
|
await mkdir(dir, { recursive: true });
|
|
4093
|
-
const file =
|
|
4227
|
+
const file = join6(dir, PENDING);
|
|
4094
4228
|
const line = JSON.stringify(entry) + "\n";
|
|
4095
4229
|
await appendFile(file, line, "utf8");
|
|
4096
4230
|
}
|