mindcontext-cli 0.1.0

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 ADDED
@@ -0,0 +1,978 @@
1
+ // src/commands/init.ts
2
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
3
+ import { createInterface } from "readline";
4
+
5
+ // src/lib/config.ts
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
+ import { homedir } from "os";
8
+ import { join } from "path";
9
+
10
+ // src/lib/machine-id.ts
11
+ import { createHash } from "crypto";
12
+ import { hostname, userInfo } from "os";
13
+ import { execSync } from "child_process";
14
+ function getMachineId() {
15
+ const host = hostname();
16
+ const user = userInfo().username;
17
+ let gitEmail = "";
18
+ try {
19
+ gitEmail = execSync("git config user.email", { encoding: "utf8" }).trim();
20
+ } catch {
21
+ }
22
+ const hash = createHash("sha256").update(`${host}-${user}-${gitEmail}`).digest("hex").substring(0, 8);
23
+ const name = `${user}-${host}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
24
+ return { name, id: hash };
25
+ }
26
+ function getTimestamp() {
27
+ const now = /* @__PURE__ */ new Date();
28
+ return now.toISOString().replace(/:/g, "-").replace(/\.\d{3}Z$/, "");
29
+ }
30
+ function generateUpdateFilename() {
31
+ const { name, id } = getMachineId();
32
+ const timestamp = getTimestamp();
33
+ return `${timestamp}_${name}_${id}.json`;
34
+ }
35
+
36
+ // src/lib/config.ts
37
+ var MINDCONTEXT_DIR = join(homedir(), ".mindcontext");
38
+ var CONFIG_FILE = join(MINDCONTEXT_DIR, "config.json");
39
+ var REPO_DIR = join(MINDCONTEXT_DIR, "repo");
40
+ var PENDING_FILE = join(MINDCONTEXT_DIR, "pending.json");
41
+ function getMindcontextDir() {
42
+ return MINDCONTEXT_DIR;
43
+ }
44
+ function getRepoDir() {
45
+ return REPO_DIR;
46
+ }
47
+ function isInitialized() {
48
+ return existsSync(CONFIG_FILE) && existsSync(REPO_DIR);
49
+ }
50
+ function createDefaultConfig() {
51
+ const machine = getMachineId();
52
+ return {
53
+ version: "1.0",
54
+ dashboard_repo: "",
55
+ dashboard_url: "",
56
+ projects: {},
57
+ machine
58
+ };
59
+ }
60
+ function readConfig() {
61
+ if (!existsSync(CONFIG_FILE)) {
62
+ return null;
63
+ }
64
+ try {
65
+ const content = readFileSync(CONFIG_FILE, "utf8");
66
+ return JSON.parse(content);
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ function writeConfig(config) {
72
+ if (!existsSync(MINDCONTEXT_DIR)) {
73
+ mkdirSync(MINDCONTEXT_DIR, { recursive: true });
74
+ }
75
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
76
+ }
77
+ function readPending() {
78
+ if (!existsSync(PENDING_FILE)) {
79
+ return [];
80
+ }
81
+ try {
82
+ const content = readFileSync(PENDING_FILE, "utf8");
83
+ return JSON.parse(content);
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
88
+ function writePending(pending) {
89
+ writeFileSync(PENDING_FILE, JSON.stringify(pending, null, 2));
90
+ }
91
+ function addPending(message) {
92
+ const pending = readPending();
93
+ pending.push({
94
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
95
+ message
96
+ });
97
+ writePending(pending);
98
+ }
99
+ function clearPending() {
100
+ if (existsSync(PENDING_FILE)) {
101
+ writeFileSync(PENDING_FILE, "[]");
102
+ }
103
+ }
104
+ function getProjectDir(projectName) {
105
+ return join(REPO_DIR, "projects", projectName, "updates");
106
+ }
107
+ function ensureProjectDir(projectName) {
108
+ const dir = getProjectDir(projectName);
109
+ if (!existsSync(dir)) {
110
+ mkdirSync(dir, { recursive: true });
111
+ }
112
+ }
113
+
114
+ // src/lib/git-ops.ts
115
+ import { execSync as execSync2 } from "child_process";
116
+ import { existsSync as existsSync2 } from "fs";
117
+ var execOptions = {
118
+ encoding: "utf8",
119
+ stdio: ["pipe", "pipe", "pipe"]
120
+ };
121
+ function cloneRepo(repoUrl, targetDir) {
122
+ execSync2(`git clone "${repoUrl}" "${targetDir}"`, execOptions);
123
+ }
124
+ function pull() {
125
+ const repoDir = getRepoDir();
126
+ if (!existsSync2(repoDir)) {
127
+ return { success: false, message: "Repository not initialized" };
128
+ }
129
+ try {
130
+ const output = execSync2("git pull --rebase origin main", {
131
+ ...execOptions,
132
+ cwd: repoDir
133
+ });
134
+ return { success: true, message: output.trim() };
135
+ } catch (error) {
136
+ const message = error instanceof Error ? error.message : "Pull failed";
137
+ return { success: false, message };
138
+ }
139
+ }
140
+ function stageAll() {
141
+ const repoDir = getRepoDir();
142
+ execSync2("git add -A", { ...execOptions, cwd: repoDir });
143
+ }
144
+ function commit(message) {
145
+ const repoDir = getRepoDir();
146
+ try {
147
+ const status = execSync2("git status --porcelain", {
148
+ ...execOptions,
149
+ cwd: repoDir
150
+ });
151
+ if (!status.trim()) {
152
+ return { success: true, message: "Nothing to commit" };
153
+ }
154
+ stageAll();
155
+ execSync2(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
156
+ ...execOptions,
157
+ cwd: repoDir
158
+ });
159
+ return { success: true, message: "Committed" };
160
+ } catch (error) {
161
+ const msg = error instanceof Error ? error.message : "Commit failed";
162
+ return { success: false, message: msg };
163
+ }
164
+ }
165
+ function push() {
166
+ const repoDir = getRepoDir();
167
+ try {
168
+ execSync2("git push origin main", {
169
+ ...execOptions,
170
+ cwd: repoDir,
171
+ timeout: 3e4
172
+ });
173
+ return { success: true, message: "Pushed to remote" };
174
+ } catch (error) {
175
+ const message = error instanceof Error ? error.message : "Push failed";
176
+ return { success: false, message };
177
+ }
178
+ }
179
+ function sync(commitMessage) {
180
+ const pending = readPending();
181
+ if (pending.length > 0) {
182
+ const pushResult2 = push();
183
+ if (pushResult2.success) {
184
+ clearPending();
185
+ }
186
+ }
187
+ const commitResult = commit(commitMessage);
188
+ if (!commitResult.success) {
189
+ return {
190
+ committed: false,
191
+ pushed: false,
192
+ message: commitResult.message
193
+ };
194
+ }
195
+ if (commitResult.message === "Nothing to commit") {
196
+ return {
197
+ committed: false,
198
+ pushed: false,
199
+ message: "No changes to sync"
200
+ };
201
+ }
202
+ const pushResult = push();
203
+ if (!pushResult.success) {
204
+ addPending(commitMessage);
205
+ return {
206
+ committed: true,
207
+ pushed: false,
208
+ message: "Committed locally. Push pending (offline)"
209
+ };
210
+ }
211
+ return {
212
+ committed: true,
213
+ pushed: true,
214
+ message: "Synced successfully"
215
+ };
216
+ }
217
+ function getRecentCommits(projectPath, limit = 5) {
218
+ try {
219
+ const output = execSync2(
220
+ `git log --oneline -${limit} --format="%s"`,
221
+ { ...execOptions, cwd: projectPath }
222
+ );
223
+ return output.trim().split("\n").filter((line) => line.length > 0);
224
+ } catch {
225
+ return [];
226
+ }
227
+ }
228
+
229
+ // src/commands/init.ts
230
+ async function prompt(question) {
231
+ const rl = createInterface({
232
+ input: process.stdin,
233
+ output: process.stdout
234
+ });
235
+ return new Promise((resolve5) => {
236
+ rl.question(question, (answer) => {
237
+ rl.close();
238
+ resolve5(answer.trim());
239
+ });
240
+ });
241
+ }
242
+ async function init(options = {}) {
243
+ const mindcontextDir = getMindcontextDir();
244
+ const repoDir = getRepoDir();
245
+ if (isInitialized()) {
246
+ const config2 = readConfig();
247
+ if (!options.quiet) {
248
+ console.log("MindContext is already initialized.");
249
+ console.log(` Directory: ${mindcontextDir}`);
250
+ console.log(` Dashboard: ${config2?.dashboard_url || "Not configured"}`);
251
+ console.log(` Projects: ${Object.keys(config2?.projects || {}).length}`);
252
+ console.log("");
253
+ console.log('Run "mc reset" to start fresh.');
254
+ }
255
+ return;
256
+ }
257
+ if (!options.quiet) {
258
+ console.log("Initializing MindContext...\n");
259
+ }
260
+ if (!existsSync3(mindcontextDir)) {
261
+ mkdirSync2(mindcontextDir, { recursive: true });
262
+ }
263
+ let dashboardRepo = "";
264
+ if (!options.quiet) {
265
+ console.log("Enter your dashboard repository URL.");
266
+ console.log("(Create one from https://github.com/tmsjngx0/mindcontext-template)\n");
267
+ dashboardRepo = await prompt("Dashboard repo URL (git@github.com:user/repo.git): ");
268
+ }
269
+ if (!dashboardRepo) {
270
+ if (!options.quiet) {
271
+ console.log("\nNo dashboard URL provided. You can configure it later with:");
272
+ console.log(" mc config --dashboard-repo <url>");
273
+ }
274
+ const config2 = createDefaultConfig();
275
+ writeConfig(config2);
276
+ if (!options.quiet) {
277
+ console.log(`
278
+ Created: ${mindcontextDir}/config.json`);
279
+ }
280
+ return;
281
+ }
282
+ if (!options.quiet) {
283
+ console.log(`
284
+ Cloning dashboard repository...`);
285
+ }
286
+ try {
287
+ cloneRepo(dashboardRepo, repoDir);
288
+ } catch (error) {
289
+ console.error("Failed to clone repository:", error instanceof Error ? error.message : error);
290
+ process.exit(1);
291
+ }
292
+ let dashboardUrl = "";
293
+ const match = dashboardRepo.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
294
+ if (match) {
295
+ dashboardUrl = `https://${match[1]}.github.io/${match[2]}`;
296
+ }
297
+ const config = createDefaultConfig();
298
+ config.dashboard_repo = dashboardRepo;
299
+ config.dashboard_url = dashboardUrl;
300
+ writeConfig(config);
301
+ if (!options.quiet) {
302
+ console.log("\n\u2713 MindContext initialized!");
303
+ console.log(` Directory: ${mindcontextDir}`);
304
+ console.log(` Dashboard: ${dashboardUrl}`);
305
+ console.log("");
306
+ console.log("Next steps:");
307
+ console.log(" cd <your-project>");
308
+ console.log(" mc connect");
309
+ }
310
+ }
311
+
312
+ // src/commands/connect.ts
313
+ import { basename as basename2, resolve, join as join3 } from "path";
314
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
315
+
316
+ // src/parsers/openspec.ts
317
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2 } from "fs";
318
+ import { join as join2, basename } from "path";
319
+ function hasOpenSpec(projectPath) {
320
+ const openspecDir = join2(projectPath, "openspec");
321
+ return existsSync4(openspecDir) && existsSync4(join2(openspecDir, "changes"));
322
+ }
323
+ function parseTasksFile(filepath) {
324
+ if (!existsSync4(filepath)) {
325
+ return { total: 0, complete: 0 };
326
+ }
327
+ const content = readFileSync2(filepath, "utf8");
328
+ const lines = content.split("\n");
329
+ let total = 0;
330
+ let complete = 0;
331
+ for (const line of lines) {
332
+ if (/^\s*-\s*\[\s*\]\s/.test(line)) {
333
+ total++;
334
+ } else if (/^\s*-\s*\[x\]\s/i.test(line)) {
335
+ total++;
336
+ complete++;
337
+ }
338
+ }
339
+ return { total, complete };
340
+ }
341
+ function determineStatus(tasksTotal, tasksComplete) {
342
+ if (tasksTotal === 0) {
343
+ return "proposed";
344
+ }
345
+ if (tasksComplete === 0) {
346
+ return "proposed";
347
+ }
348
+ if (tasksComplete === tasksTotal) {
349
+ return "done";
350
+ }
351
+ return "in_progress";
352
+ }
353
+ function parseChange(changePath) {
354
+ const id = basename(changePath);
355
+ if (id === "archive") {
356
+ return null;
357
+ }
358
+ const tasksFile = join2(changePath, "tasks.md");
359
+ const { total, complete } = parseTasksFile(tasksFile);
360
+ return {
361
+ id,
362
+ tasksTotal: total,
363
+ tasksComplete: complete,
364
+ status: determineStatus(total, complete)
365
+ };
366
+ }
367
+ function parseOpenSpec(projectPath) {
368
+ if (!hasOpenSpec(projectPath)) {
369
+ return { found: false, changes: [], activeChange: null };
370
+ }
371
+ const changesDir = join2(projectPath, "openspec", "changes");
372
+ const entries = readdirSync(changesDir, { withFileTypes: true });
373
+ const changes = [];
374
+ for (const entry of entries) {
375
+ if (entry.isDirectory()) {
376
+ const change = parseChange(join2(changesDir, entry.name));
377
+ if (change) {
378
+ changes.push(change);
379
+ }
380
+ }
381
+ }
382
+ let activeChange = null;
383
+ for (const change of changes) {
384
+ if (change.status === "in_progress") {
385
+ activeChange = change;
386
+ break;
387
+ }
388
+ }
389
+ if (!activeChange) {
390
+ for (const change of changes) {
391
+ if (change.status === "review") {
392
+ activeChange = change;
393
+ break;
394
+ }
395
+ }
396
+ }
397
+ if (!activeChange) {
398
+ for (const change of changes) {
399
+ if (change.status === "proposed" && change.tasksTotal > 0) {
400
+ activeChange = change;
401
+ break;
402
+ }
403
+ }
404
+ }
405
+ return { found: true, changes, activeChange };
406
+ }
407
+ function getOpenSpecProgress(projectPath) {
408
+ const result = parseOpenSpec(projectPath);
409
+ if (!result.found || !result.activeChange) {
410
+ return {
411
+ source: "manual",
412
+ tasks_done: 0,
413
+ tasks_total: 0
414
+ };
415
+ }
416
+ return {
417
+ source: "openspec",
418
+ change: result.activeChange.id,
419
+ tasks_done: result.activeChange.tasksComplete,
420
+ tasks_total: result.activeChange.tasksTotal
421
+ };
422
+ }
423
+
424
+ // src/lib/templates.ts
425
+ var COMMAND_TEMPLATES = {
426
+ "prime.md": `---
427
+ description: Load context and show what to work on
428
+ ---
429
+
430
+ # Prime Context
431
+
432
+ Load project context at the start of your session.
433
+
434
+ ## Step 1: Get Current Context
435
+
436
+ \`\`\`bash
437
+ mc context --json
438
+ \`\`\`
439
+
440
+ ## Step 2: Load Last Session Notes
441
+
442
+ From the context output, identify:
443
+ - **Last session summary** - What was accomplished
444
+ - **Next tasks** - What was planned for this session
445
+ - **Current change** - The OpenSpec change being worked on
446
+ - **Progress** - Tasks completed vs total
447
+
448
+ ## Step 3: Show Context Summary
449
+
450
+ Display to the user:
451
+
452
+ \`\`\`
453
+ SESSION CONTEXT LOADED
454
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
455
+
456
+ Project: [project name]
457
+ Change: [current openspec change]
458
+ Progress: [tasks_done]/[tasks_total] ([percentage]%)
459
+
460
+ Last Session:
461
+ - [notes from last update]
462
+
463
+ Planned for This Session:
464
+ - [next tasks from last update]
465
+ \`\`\`
466
+
467
+ ## Step 4: Suggest Next Action
468
+
469
+ Based on context, suggest what to work on next.
470
+ `,
471
+ "update.md": `---
472
+ description: Save session context and sync progress
473
+ ---
474
+
475
+ # Update Context
476
+
477
+ Save your session progress with auto-generated context.
478
+
479
+ ## Step 1: Generate Session Summary
480
+
481
+ From your conversation context, summarize what was accomplished:
482
+ - Recent code changes and commits
483
+ - Tasks completed
484
+ - Features implemented or bugs fixed
485
+
486
+ Create 3-5 concise bullet points.
487
+
488
+ ## Step 2: Generate Next Tasks
489
+
490
+ From OpenSpec and conversation context, identify what should be done next:
491
+ - Remaining tasks from current OpenSpec change
492
+ - Blockers or pending items mentioned
493
+
494
+ Create 2-4 actionable next task items.
495
+
496
+ ## Step 3: Run mc sync
497
+
498
+ \`\`\`bash
499
+ mc sync --notes "First accomplishment
500
+ Second accomplishment" --next "Next task 1
501
+ Next task 2"
502
+ \`\`\`
503
+
504
+ Use multiline strings - each line becomes one item.
505
+
506
+ ## Step 4: Show Confirmation
507
+
508
+ \`\`\`
509
+ PROGRESS SYNCED
510
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
511
+
512
+ Session Summary:
513
+ - [generated notes]
514
+
515
+ Next Session:
516
+ - [generated next tasks]
517
+ \`\`\`
518
+ `,
519
+ "progress.md": `---
520
+ description: Show progress
521
+ ---
522
+
523
+ Run \`mc progress\` to see current progress in the terminal.
524
+
525
+ Options:
526
+ - \`--web\` - Open the web dashboard in your browser
527
+
528
+ This aggregates all update files to show:
529
+ - Current change and task completion
530
+ - Recent activity across machines
531
+ - Team member status (if collaborative)
532
+ `,
533
+ "context.md": `---
534
+ description: Output current context
535
+ ---
536
+
537
+ Run \`mc context\` to output the current project context.
538
+
539
+ Options:
540
+ - \`--json\` - Output as JSON (for integration with other tools)
541
+
542
+ This shows:
543
+ - Project connection status
544
+ - Current OpenSpec change and progress
545
+ - Last update details
546
+ - Team activity summary
547
+ `
548
+ };
549
+ var HOOK_TEMPLATES = {
550
+ "session-end.js": `const { execSync } = require('child_process');
551
+
552
+ module.exports = async function() {
553
+ try {
554
+ execSync('mc sync --quiet', {
555
+ stdio: 'inherit',
556
+ timeout: 10000
557
+ });
558
+ } catch (e) {
559
+ // Sync failed silently - don't block session end
560
+ }
561
+ };
562
+ `
563
+ };
564
+
565
+ // src/commands/connect.ts
566
+ async function connect(options = {}) {
567
+ if (!isInitialized()) {
568
+ console.error('MindContext is not initialized. Run "mc init" first.');
569
+ process.exit(1);
570
+ }
571
+ const config = readConfig();
572
+ if (!config) {
573
+ console.error("Failed to read config.");
574
+ process.exit(1);
575
+ }
576
+ const projectPath = resolve(process.cwd());
577
+ const projectName = options.name || basename2(projectPath);
578
+ if (config.projects[projectName]) {
579
+ if (!options.quiet) {
580
+ console.log(`Project "${projectName}" is already connected.`);
581
+ console.log(` Path: ${config.projects[projectName].path}`);
582
+ console.log(` Category: ${config.projects[projectName].category}`);
583
+ console.log(` OpenSpec: ${config.projects[projectName].openspec ? "Yes" : "No"}`);
584
+ }
585
+ return;
586
+ }
587
+ const openspec = hasOpenSpec(projectPath);
588
+ config.projects[projectName] = {
589
+ path: projectPath,
590
+ openspec,
591
+ category: options.category || "default"
592
+ };
593
+ writeConfig(config);
594
+ ensureProjectDir(projectName);
595
+ const claudeDir = join3(projectPath, ".claude");
596
+ const commandsDir = join3(claudeDir, "commands", "mc");
597
+ if (!existsSync5(commandsDir)) {
598
+ mkdirSync3(commandsDir, { recursive: true });
599
+ for (const [filename, content] of Object.entries(COMMAND_TEMPLATES)) {
600
+ writeFileSync2(join3(commandsDir, filename), content);
601
+ }
602
+ if (!options.quiet) {
603
+ console.log(`\u2713 Created .claude/commands/mc/`);
604
+ }
605
+ }
606
+ if (options.withHooks) {
607
+ const hooksDir = join3(claudeDir, "hooks");
608
+ if (!existsSync5(hooksDir)) {
609
+ mkdirSync3(hooksDir, { recursive: true });
610
+ for (const [filename, content] of Object.entries(HOOK_TEMPLATES)) {
611
+ writeFileSync2(join3(hooksDir, filename), content);
612
+ }
613
+ if (!options.quiet) {
614
+ console.log(`\u2713 Created .claude/hooks/`);
615
+ }
616
+ }
617
+ }
618
+ if (!options.quiet) {
619
+ console.log(`\u2713 Connected "${projectName}"`);
620
+ console.log(` Path: ${projectPath}`);
621
+ console.log(` Category: ${options.category || "default"}`);
622
+ console.log(` OpenSpec: ${openspec ? "Detected" : "Not found"}`);
623
+ console.log("");
624
+ console.log('Next: Run "mc sync" to create your first update.');
625
+ }
626
+ }
627
+
628
+ // src/commands/sync.ts
629
+ import { basename as basename3, resolve as resolve2 } from "path";
630
+
631
+ // src/lib/update-file.ts
632
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
633
+ import { join as join4 } from "path";
634
+ function createUpdateFile(projectName, progress2, context2, recentCommits) {
635
+ const { name, id } = getMachineId();
636
+ const filename = generateUpdateFilename();
637
+ const filepath = join4(getProjectDir(projectName), filename);
638
+ const update = {
639
+ version: "1.0",
640
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
641
+ machine: name,
642
+ machine_id: id,
643
+ project: projectName,
644
+ progress: progress2,
645
+ context: context2,
646
+ recent_commits: recentCommits
647
+ };
648
+ ensureProjectDir(projectName);
649
+ writeFileSync3(filepath, JSON.stringify(update, null, 2));
650
+ return filepath;
651
+ }
652
+ function readUpdateFiles(projectName) {
653
+ const dir = getProjectDir(projectName);
654
+ if (!existsSync6(dir)) {
655
+ return [];
656
+ }
657
+ const files = readdirSync2(dir).filter((f) => f.endsWith(".json"));
658
+ const updates = [];
659
+ for (const file of files) {
660
+ try {
661
+ const content = readFileSync3(join4(dir, file), "utf8");
662
+ updates.push(JSON.parse(content));
663
+ } catch {
664
+ }
665
+ }
666
+ return updates.sort(
667
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
668
+ );
669
+ }
670
+ function getLatestUpdate(projectName) {
671
+ const updates = readUpdateFiles(projectName);
672
+ return updates[0] || null;
673
+ }
674
+ function getLatestByMachine(projectName) {
675
+ const updates = readUpdateFiles(projectName);
676
+ const byMachine = /* @__PURE__ */ new Map();
677
+ for (const update of updates) {
678
+ if (!byMachine.has(update.machine_id)) {
679
+ byMachine.set(update.machine_id, update);
680
+ }
681
+ }
682
+ return byMachine;
683
+ }
684
+ function getRecentUpdates(projectName, limit = 10) {
685
+ const updates = readUpdateFiles(projectName);
686
+ return updates.slice(0, limit);
687
+ }
688
+
689
+ // src/commands/sync.ts
690
+ async function sync2(options = {}) {
691
+ if (!isInitialized()) {
692
+ if (!options.quiet) {
693
+ console.error('MindContext is not initialized. Run "mc init" first.');
694
+ }
695
+ process.exit(1);
696
+ }
697
+ const config = readConfig();
698
+ if (!config) {
699
+ if (!options.quiet) {
700
+ console.error("Failed to read config.");
701
+ }
702
+ process.exit(1);
703
+ }
704
+ const projectPath = resolve2(process.cwd());
705
+ const projectName = basename3(projectPath);
706
+ const project = config.projects[projectName];
707
+ if (!project) {
708
+ if (!options.quiet) {
709
+ console.error(`Project "${projectName}" is not connected.`);
710
+ console.error('Run "mc connect" first.');
711
+ }
712
+ process.exit(1);
713
+ }
714
+ const progress2 = getOpenSpecProgress(projectPath);
715
+ const context2 = {
716
+ current_task: progress2.change ? `Working on ${progress2.change}` : void 0,
717
+ status: options.status || (progress2.tasks_done > 0 ? "in_progress" : "idle"),
718
+ notes: options.notes || [],
719
+ next: options.next || []
720
+ };
721
+ if (!options.quiet) {
722
+ console.log(`Syncing "${projectName}"...`);
723
+ if (progress2.source === "openspec") {
724
+ console.log(` Change: ${progress2.change}`);
725
+ console.log(` Progress: ${progress2.tasks_done}/${progress2.tasks_total}`);
726
+ }
727
+ }
728
+ const recentCommits = getRecentCommits(projectPath, 5);
729
+ if (options.dryRun) {
730
+ if (!options.quiet) {
731
+ console.log("\n[Dry run] Would create update file:");
732
+ console.log(JSON.stringify({ progress: progress2, context: context2, recent_commits: recentCommits }, null, 2));
733
+ }
734
+ return;
735
+ }
736
+ const filepath = createUpdateFile(projectName, progress2, context2, recentCommits);
737
+ if (!options.quiet) {
738
+ console.log(` Created: ${filepath}`);
739
+ }
740
+ const commitMessage = `chore(progress): sync ${config.machine.name}`;
741
+ const result = sync(commitMessage);
742
+ if (!options.quiet) {
743
+ if (result.committed && result.pushed) {
744
+ console.log("\u2713 Synced successfully");
745
+ } else if (result.committed) {
746
+ console.log("\u2713 Committed locally (push pending)");
747
+ console.log(' Run "mc push" when online to push changes.');
748
+ } else {
749
+ console.log(` ${result.message}`);
750
+ }
751
+ }
752
+ }
753
+
754
+ // src/commands/pull.ts
755
+ async function pull2(options = {}) {
756
+ if (!isInitialized()) {
757
+ if (!options.quiet) {
758
+ console.error('MindContext is not initialized. Run "mc init" first.');
759
+ }
760
+ process.exit(1);
761
+ }
762
+ if (!options.quiet) {
763
+ console.log("Pulling latest updates...");
764
+ }
765
+ const result = pull();
766
+ if (!options.quiet) {
767
+ if (result.success) {
768
+ console.log("\u2713 Updated");
769
+ if (result.message && result.message !== "Already up to date.") {
770
+ console.log(` ${result.message}`);
771
+ }
772
+ } else {
773
+ console.error("\u2717 Failed to pull");
774
+ console.error(` ${result.message}`);
775
+ }
776
+ }
777
+ }
778
+
779
+ // src/commands/context.ts
780
+ import { basename as basename4, resolve as resolve3 } from "path";
781
+ async function context(options = {}) {
782
+ const projectPath = resolve3(process.cwd());
783
+ const projectName = basename4(projectPath);
784
+ const initialized = isInitialized();
785
+ const config = initialized ? readConfig() : null;
786
+ const project = config?.projects[projectName];
787
+ const progress2 = getOpenSpecProgress(projectPath);
788
+ const percentage = progress2.tasks_total > 0 ? Math.round(progress2.tasks_done / progress2.tasks_total * 100) : 0;
789
+ const output = {
790
+ project: projectName,
791
+ connected: !!project,
792
+ progress: {
793
+ ...progress2,
794
+ percentage
795
+ },
796
+ team: []
797
+ };
798
+ if (project) {
799
+ const latestUpdate = getLatestUpdate(projectName);
800
+ if (latestUpdate) {
801
+ output.lastUpdate = {
802
+ timestamp: latestUpdate.timestamp,
803
+ machine: latestUpdate.machine,
804
+ status: latestUpdate.context.status,
805
+ notes: latestUpdate.context.notes,
806
+ next: latestUpdate.context.next
807
+ };
808
+ }
809
+ const byMachine = getLatestByMachine(projectName);
810
+ for (const [_machineId, update] of byMachine) {
811
+ output.team.push({
812
+ machine: update.machine,
813
+ timestamp: update.timestamp,
814
+ status: update.context.status
815
+ });
816
+ }
817
+ }
818
+ if (options.json) {
819
+ console.log(JSON.stringify(output, null, 2));
820
+ return;
821
+ }
822
+ if (!options.quiet) {
823
+ console.log(`Project: ${projectName}`);
824
+ console.log(`Connected: ${output.connected ? "Yes" : "No"}`);
825
+ console.log("");
826
+ if (progress2.source === "openspec" && progress2.change) {
827
+ console.log(`Change: ${progress2.change}`);
828
+ console.log(`Progress: ${progress2.tasks_done}/${progress2.tasks_total} (${percentage}%)`);
829
+ } else {
830
+ console.log("Progress: No active change");
831
+ }
832
+ if (output.lastUpdate) {
833
+ console.log("");
834
+ console.log(`Last Update: ${output.lastUpdate.timestamp}`);
835
+ console.log(` Machine: ${output.lastUpdate.machine}`);
836
+ console.log(` Status: ${output.lastUpdate.status}`);
837
+ if (output.lastUpdate.notes.length > 0) {
838
+ console.log(` Notes: ${output.lastUpdate.notes.join(", ")}`);
839
+ }
840
+ if (output.lastUpdate.next.length > 0) {
841
+ console.log(` Next: ${output.lastUpdate.next.join(", ")}`);
842
+ }
843
+ }
844
+ if (output.team.length > 1) {
845
+ console.log("");
846
+ console.log("Team Activity:");
847
+ for (const member of output.team) {
848
+ console.log(` ${member.machine}: ${member.status} (${member.timestamp})`);
849
+ }
850
+ }
851
+ }
852
+ }
853
+
854
+ // src/commands/progress.ts
855
+ import { execSync as execSync3 } from "child_process";
856
+ import { basename as basename5, resolve as resolve4 } from "path";
857
+ async function progress(options = {}) {
858
+ if (!isInitialized()) {
859
+ if (!options.quiet) {
860
+ console.error('MindContext is not initialized. Run "mc init" first.');
861
+ }
862
+ process.exit(1);
863
+ }
864
+ const config = readConfig();
865
+ if (!config) {
866
+ if (!options.quiet) {
867
+ console.error("Failed to read config.");
868
+ }
869
+ process.exit(1);
870
+ }
871
+ if (options.web) {
872
+ if (!config.dashboard_url) {
873
+ if (!options.quiet) {
874
+ console.error("No dashboard URL configured.");
875
+ console.error("Configure with: mc config --dashboard-url <url>");
876
+ }
877
+ process.exit(1);
878
+ }
879
+ if (!options.quiet) {
880
+ console.log(`Opening: ${config.dashboard_url}`);
881
+ }
882
+ try {
883
+ const platform = process.platform;
884
+ if (platform === "darwin") {
885
+ execSync3(`open "${config.dashboard_url}"`);
886
+ } else if (platform === "win32") {
887
+ execSync3(`start "${config.dashboard_url}"`);
888
+ } else {
889
+ execSync3(`xdg-open "${config.dashboard_url}"`);
890
+ }
891
+ } catch {
892
+ if (!options.quiet) {
893
+ console.log(`Please open: ${config.dashboard_url}`);
894
+ }
895
+ }
896
+ return;
897
+ }
898
+ const projectPath = resolve4(process.cwd());
899
+ const projectName = basename5(projectPath);
900
+ const project = config.projects[projectName];
901
+ if (!project) {
902
+ console.log("MindContext Progress\n");
903
+ console.log("Projects:");
904
+ for (const [name, proj] of Object.entries(config.projects)) {
905
+ const openspecProgress2 = getOpenSpecProgress(proj.path);
906
+ const percentage2 = openspecProgress2.tasks_total > 0 ? Math.round(openspecProgress2.tasks_done / openspecProgress2.tasks_total * 100) : 0;
907
+ const bar = generateProgressBar(percentage2);
908
+ console.log(` ${name}`);
909
+ console.log(` ${bar} ${percentage2}%`);
910
+ if (openspecProgress2.change) {
911
+ console.log(` Current: ${openspecProgress2.change}`);
912
+ }
913
+ }
914
+ if (Object.keys(config.projects).length === 0) {
915
+ console.log(" (No projects connected)");
916
+ console.log("");
917
+ console.log('Run "mc connect" in a project directory to connect it.');
918
+ }
919
+ if (config.dashboard_url) {
920
+ console.log("");
921
+ console.log(`Dashboard: ${config.dashboard_url}`);
922
+ console.log('Run "mc progress --web" to open in browser.');
923
+ }
924
+ return;
925
+ }
926
+ const openspecProgress = getOpenSpecProgress(projectPath);
927
+ const percentage = openspecProgress.tasks_total > 0 ? Math.round(openspecProgress.tasks_done / openspecProgress.tasks_total * 100) : 0;
928
+ console.log(`${projectName}
929
+ `);
930
+ if (openspecProgress.source === "openspec" && openspecProgress.change) {
931
+ console.log(`Change: ${openspecProgress.change}`);
932
+ console.log(`Progress: ${openspecProgress.tasks_done}/${openspecProgress.tasks_total}`);
933
+ console.log(generateProgressBar(percentage, 30) + ` ${percentage}%`);
934
+ } else {
935
+ console.log("No active OpenSpec change.");
936
+ }
937
+ const recentUpdates = getRecentUpdates(projectName, 5);
938
+ if (recentUpdates.length > 0) {
939
+ console.log("\nRecent Activity:");
940
+ for (const update of recentUpdates) {
941
+ const date = new Date(update.timestamp).toLocaleDateString();
942
+ const time = new Date(update.timestamp).toLocaleTimeString();
943
+ console.log(` ${date} ${time} - ${update.machine}`);
944
+ if (update.context.notes.length > 0) {
945
+ console.log(` Notes: ${update.context.notes.join(", ")}`);
946
+ }
947
+ }
948
+ }
949
+ }
950
+ function generateProgressBar(percentage, width = 20) {
951
+ const filled = Math.round(percentage / 100 * width);
952
+ const empty = width - filled;
953
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty);
954
+ }
955
+ export {
956
+ connect,
957
+ context,
958
+ createUpdateFile,
959
+ generateUpdateFilename,
960
+ getLatestByMachine,
961
+ getLatestUpdate,
962
+ getMachineId,
963
+ getMindcontextDir,
964
+ getOpenSpecProgress,
965
+ getRepoDir,
966
+ getTimestamp,
967
+ hasOpenSpec,
968
+ init,
969
+ isInitialized,
970
+ parseOpenSpec,
971
+ progress,
972
+ pull2 as pull,
973
+ readConfig,
974
+ readUpdateFiles,
975
+ sync2 as sync,
976
+ writeConfig
977
+ };
978
+ //# sourceMappingURL=index.js.map