@fureworks/scope 0.1.0 → 0.2.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/README.md +129 -48
- package/dist/cli/config.d.ts +8 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +368 -42
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +15 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/notifications.d.ts +7 -0
- package/dist/cli/notifications.d.ts.map +1 -0
- package/dist/cli/notifications.js +77 -0
- package/dist/cli/notifications.js.map +1 -0
- package/dist/cli/onboard.d.ts.map +1 -1
- package/dist/cli/onboard.js +2 -1
- package/dist/cli/onboard.js.map +1 -1
- package/dist/cli/plan.d.ts +7 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +111 -0
- package/dist/cli/plan.js.map +1 -0
- package/dist/cli/review.d.ts +6 -0
- package/dist/cli/review.d.ts.map +1 -0
- package/dist/cli/review.js +167 -0
- package/dist/cli/review.js.map +1 -0
- package/dist/cli/snooze.d.ts +12 -0
- package/dist/cli/snooze.d.ts.map +1 -0
- package/dist/cli/snooze.js +155 -0
- package/dist/cli/snooze.js.map +1 -0
- package/dist/cli/today.d.ts.map +1 -1
- package/dist/cli/today.js +69 -9
- package/dist/cli/today.js.map +1 -1
- package/dist/cli/tune.d.ts +8 -0
- package/dist/cli/tune.d.ts.map +1 -0
- package/dist/cli/tune.js +62 -0
- package/dist/cli/tune.js.map +1 -0
- package/dist/engine/prioritize.d.ts +10 -2
- package/dist/engine/prioritize.d.ts.map +1 -1
- package/dist/engine/prioritize.js +156 -25
- package/dist/engine/prioritize.js.map +1 -1
- package/dist/index.js +48 -5
- package/dist/index.js.map +1 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +6 -10
- package/dist/notifications/index.js.map +1 -1
- package/dist/sources/activity.d.ts +32 -0
- package/dist/sources/activity.d.ts.map +1 -0
- package/dist/sources/activity.js +101 -0
- package/dist/sources/activity.js.map +1 -0
- package/dist/sources/calendar.d.ts +6 -0
- package/dist/sources/calendar.d.ts.map +1 -1
- package/dist/sources/calendar.js +114 -0
- package/dist/sources/calendar.js.map +1 -1
- package/dist/store/config.d.ts +8 -0
- package/dist/store/config.d.ts.map +1 -1
- package/dist/store/config.js +22 -0
- package/dist/store/config.js.map +1 -1
- package/dist/store/muted.d.ts +17 -0
- package/dist/store/muted.d.ts.map +1 -0
- package/dist/store/muted.js +55 -0
- package/dist/store/muted.js.map +1 -0
- package/dist/store/snapshot.d.ts +12 -0
- package/dist/store/snapshot.d.ts.map +1 -0
- package/dist/store/snapshot.js +41 -0
- package/dist/store/snapshot.js.map +1 -0
- package/package.json +8 -2
- package/src/cli/config.ts +0 -66
- package/src/cli/context.ts +0 -109
- package/src/cli/daemon.ts +0 -217
- package/src/cli/onboard.ts +0 -335
- package/src/cli/status.ts +0 -77
- package/src/cli/switch.ts +0 -93
- package/src/cli/today.ts +0 -114
- package/src/engine/prioritize.ts +0 -257
- package/src/index.ts +0 -58
- package/src/notifications/index.ts +0 -42
- package/src/sources/calendar.ts +0 -170
- package/src/sources/git.ts +0 -168
- package/src/sources/issues.ts +0 -62
- package/src/store/config.ts +0 -104
- package/tsconfig.json +0 -19
package/src/cli/switch.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { loadConfig, getScopeDir } from "../store/config.js";
|
|
5
|
-
import { simpleGit } from "simple-git";
|
|
6
|
-
|
|
7
|
-
interface ProjectContext {
|
|
8
|
-
name: string;
|
|
9
|
-
path: string;
|
|
10
|
-
branch: string;
|
|
11
|
-
lastSwitchedAt: string;
|
|
12
|
-
notes: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function getContextPath(projectName: string): string {
|
|
16
|
-
return join(getScopeDir(), "contexts", `${projectName}.json`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function loadContext(projectName: string): ProjectContext | null {
|
|
20
|
-
const contextPath = getContextPath(projectName);
|
|
21
|
-
if (!existsSync(contextPath)) return null;
|
|
22
|
-
try {
|
|
23
|
-
return JSON.parse(readFileSync(contextPath, "utf-8"));
|
|
24
|
-
} catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function saveContext(context: ProjectContext): void {
|
|
30
|
-
const dir = join(getScopeDir(), "contexts");
|
|
31
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
32
|
-
writeFileSync(getContextPath(context.name), JSON.stringify(context, null, 2));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function switchCommand(project: string): Promise<void> {
|
|
36
|
-
const config = loadConfig();
|
|
37
|
-
|
|
38
|
-
const projectConfig = config.projects[project];
|
|
39
|
-
if (!projectConfig) {
|
|
40
|
-
console.log(
|
|
41
|
-
chalk.yellow(`\n Project "${project}" not found.\n`)
|
|
42
|
-
);
|
|
43
|
-
console.log(chalk.dim(" Available projects:"));
|
|
44
|
-
for (const name of Object.keys(config.projects)) {
|
|
45
|
-
console.log(chalk.dim(` - ${name}`));
|
|
46
|
-
}
|
|
47
|
-
console.log(chalk.dim(`\n Add with: scope config projects\n`));
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Save current context if we can detect one
|
|
52
|
-
// (We save the project we're switching FROM)
|
|
53
|
-
|
|
54
|
-
// Load target context
|
|
55
|
-
const existingContext = loadContext(project);
|
|
56
|
-
|
|
57
|
-
// Get current git branch for the target project
|
|
58
|
-
let branch = "unknown";
|
|
59
|
-
try {
|
|
60
|
-
const git = simpleGit(projectConfig.path);
|
|
61
|
-
const branchInfo = await git.branch();
|
|
62
|
-
branch = branchInfo.current;
|
|
63
|
-
} catch {
|
|
64
|
-
// Not a git repo or error
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Save/update context
|
|
68
|
-
const context: ProjectContext = {
|
|
69
|
-
name: project,
|
|
70
|
-
path: projectConfig.path,
|
|
71
|
-
branch,
|
|
72
|
-
lastSwitchedAt: new Date().toISOString(),
|
|
73
|
-
notes: existingContext?.notes || "",
|
|
74
|
-
};
|
|
75
|
-
saveContext(context);
|
|
76
|
-
|
|
77
|
-
console.log("");
|
|
78
|
-
console.log(chalk.bold(` Switched to: ${project}`));
|
|
79
|
-
console.log(chalk.dim(` ─────────────────────`));
|
|
80
|
-
console.log(` 📁 ${projectConfig.path}`);
|
|
81
|
-
console.log(` 🌿 ${branch}`);
|
|
82
|
-
if (existingContext?.notes) {
|
|
83
|
-
console.log(` 📝 ${existingContext.notes}`);
|
|
84
|
-
}
|
|
85
|
-
if (existingContext?.lastSwitchedAt) {
|
|
86
|
-
const last = new Date(existingContext.lastSwitchedAt);
|
|
87
|
-
const ago = Math.round((Date.now() - last.getTime()) / (1000 * 60 * 60));
|
|
88
|
-
console.log(chalk.dim(` Last here: ${ago}h ago`));
|
|
89
|
-
}
|
|
90
|
-
console.log("");
|
|
91
|
-
console.log(chalk.dim(` cd ${projectConfig.path}`));
|
|
92
|
-
console.log("");
|
|
93
|
-
}
|
package/src/cli/today.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { loadConfig, configExists } from "../store/config.js";
|
|
3
|
-
import { scanAllRepos } from "../sources/git.js";
|
|
4
|
-
import { getCalendarToday } from "../sources/calendar.js";
|
|
5
|
-
import { scanAssignedIssues } from "../sources/issues.js";
|
|
6
|
-
import { prioritize } from "../engine/prioritize.js";
|
|
7
|
-
|
|
8
|
-
interface TodayOptions {
|
|
9
|
-
calendar?: boolean;
|
|
10
|
-
json?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function todayCommand(options: TodayOptions): Promise<void> {
|
|
14
|
-
if (!configExists()) {
|
|
15
|
-
console.log(
|
|
16
|
-
chalk.yellow(
|
|
17
|
-
" Scope isn't set up yet. Run `scope onboard` to get started.\n"
|
|
18
|
-
)
|
|
19
|
-
);
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const config = loadConfig();
|
|
24
|
-
|
|
25
|
-
if (config.repos.length === 0) {
|
|
26
|
-
console.log(
|
|
27
|
-
chalk.yellow(" No repos configured. Run `scope config git` to add some.\n")
|
|
28
|
-
);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Scan git repos
|
|
33
|
-
const gitSignals = await scanAllRepos(config.repos);
|
|
34
|
-
|
|
35
|
-
// Get calendar events
|
|
36
|
-
let calendarEvents: Awaited<ReturnType<typeof getCalendarToday>> = null;
|
|
37
|
-
if (options.calendar !== false && config.calendar.enabled) {
|
|
38
|
-
calendarEvents = await getCalendarToday();
|
|
39
|
-
if (!calendarEvents) {
|
|
40
|
-
console.log(
|
|
41
|
-
chalk.dim(
|
|
42
|
-
" ⚠ Calendar not available. Try: gws auth login\n"
|
|
43
|
-
)
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const events = calendarEvents?.events ?? [];
|
|
49
|
-
const freeBlocks = calendarEvents?.freeBlocks ?? [];
|
|
50
|
-
const issueScan = await scanAssignedIssues();
|
|
51
|
-
if (!issueScan.available) {
|
|
52
|
-
console.log(
|
|
53
|
-
chalk.dim(" ⚠ GitHub issues not available. Install/auth gh to enable issue signals.\n")
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Prioritize
|
|
58
|
-
const result = prioritize(gitSignals, events, freeBlocks, issueScan.issues);
|
|
59
|
-
|
|
60
|
-
// Output
|
|
61
|
-
if (options.json) {
|
|
62
|
-
console.log(JSON.stringify(result, null, 2));
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
console.log("");
|
|
67
|
-
|
|
68
|
-
if (result.now.length === 0 && result.today.length === 0) {
|
|
69
|
-
console.log(chalk.green(" ✓ Nothing urgent. You're clear.\n"));
|
|
70
|
-
if (result.laterCount > 0) {
|
|
71
|
-
console.log(
|
|
72
|
-
chalk.dim(` ${result.laterCount} low-priority items → scope status\n`)
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// NOW section
|
|
79
|
-
if (result.now.length > 0) {
|
|
80
|
-
console.log(chalk.bold(" NOW"));
|
|
81
|
-
console.log(chalk.dim(" ───"));
|
|
82
|
-
for (const item of result.now) {
|
|
83
|
-
console.log(` ${item.emoji} ${chalk.bold(item.label)}`);
|
|
84
|
-
console.log(` ${chalk.dim(item.detail)}`);
|
|
85
|
-
}
|
|
86
|
-
console.log("");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// TODAY section
|
|
90
|
-
if (result.today.length > 0) {
|
|
91
|
-
console.log(chalk.bold(" TODAY"));
|
|
92
|
-
console.log(chalk.dim(" ────"));
|
|
93
|
-
for (const item of result.today) {
|
|
94
|
-
console.log(` ${item.emoji} ${chalk.bold(item.label)}`);
|
|
95
|
-
console.log(` ${chalk.dim(item.detail)}`);
|
|
96
|
-
}
|
|
97
|
-
console.log("");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Suggestions
|
|
101
|
-
if (result.suggestions.length > 0) {
|
|
102
|
-
for (const suggestion of result.suggestions) {
|
|
103
|
-
console.log(` 💡 ${suggestion}`);
|
|
104
|
-
}
|
|
105
|
-
console.log("");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Later count
|
|
109
|
-
if (result.laterCount > 0) {
|
|
110
|
-
console.log(
|
|
111
|
-
chalk.dim(` ${result.laterCount} other items can wait → scope status\n`)
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
}
|
package/src/engine/prioritize.ts
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import { GitSignal, PRInfo } from "../sources/git.js";
|
|
2
|
-
import { CalendarEvent, FreeBlock } from "../sources/calendar.js";
|
|
3
|
-
import { IssueSignal } from "../sources/issues.js";
|
|
4
|
-
|
|
5
|
-
export type Priority = "now" | "today" | "later";
|
|
6
|
-
|
|
7
|
-
export interface ScoredItem {
|
|
8
|
-
priority: Priority;
|
|
9
|
-
score: number;
|
|
10
|
-
emoji: string;
|
|
11
|
-
label: string;
|
|
12
|
-
detail: string;
|
|
13
|
-
source: "git" | "calendar" | "pr" | "issue";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface PrioritizedOutput {
|
|
17
|
-
now: ScoredItem[];
|
|
18
|
-
today: ScoredItem[];
|
|
19
|
-
laterCount: number;
|
|
20
|
-
freeBlocks: FreeBlock[];
|
|
21
|
-
suggestions: string[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function scorePR(pr: PRInfo, repoName: string): ScoredItem {
|
|
25
|
-
let score = 0;
|
|
26
|
-
const details: string[] = [];
|
|
27
|
-
|
|
28
|
-
// Staleness
|
|
29
|
-
if (pr.ageDays > 14) {
|
|
30
|
-
score += 9; // 2+ weeks = critical
|
|
31
|
-
details.push(`open ${Math.round(pr.ageDays)} days`);
|
|
32
|
-
} else if (pr.ageDays > 5) {
|
|
33
|
-
score += 7;
|
|
34
|
-
details.push(`open ${Math.round(pr.ageDays)} days`);
|
|
35
|
-
} else if (pr.ageDays > 2) {
|
|
36
|
-
score += 4;
|
|
37
|
-
details.push(`open ${Math.round(pr.ageDays)} days`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Blocking potential
|
|
41
|
-
if (pr.reviewRequested) {
|
|
42
|
-
score += 8;
|
|
43
|
-
details.push("review requested");
|
|
44
|
-
}
|
|
45
|
-
if (pr.ciStatus === "fail") {
|
|
46
|
-
score += 5;
|
|
47
|
-
details.push("CI failing");
|
|
48
|
-
}
|
|
49
|
-
if (pr.hasConflicts) {
|
|
50
|
-
score += 4;
|
|
51
|
-
details.push("has conflicts");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const priority: Priority = score >= 8 ? "now" : score >= 4 ? "today" : "later";
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
priority,
|
|
58
|
-
score,
|
|
59
|
-
emoji: priority === "now" ? "🔴" : "🟡",
|
|
60
|
-
label: `PR #${pr.number} on ${repoName}`,
|
|
61
|
-
detail: `${pr.title} — ${details.join(", ")}`,
|
|
62
|
-
source: "pr",
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function scoreRepoWork(signal: GitSignal): ScoredItem | null {
|
|
67
|
-
if (signal.uncommittedFiles === 0) return null;
|
|
68
|
-
|
|
69
|
-
let score = 0;
|
|
70
|
-
const details: string[] = [];
|
|
71
|
-
|
|
72
|
-
details.push(
|
|
73
|
-
`${signal.uncommittedFiles} uncommitted file${signal.uncommittedFiles > 1 ? "s" : ""}`
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
// Staleness of uncommitted work
|
|
77
|
-
const days = Math.round(signal.lastCommitAge / 24);
|
|
78
|
-
if (signal.lastCommitAge > 72) {
|
|
79
|
-
score += 9; // 3+ days uncommitted = NOW
|
|
80
|
-
details.push(`last commit ${days}d ago`);
|
|
81
|
-
} else if (signal.lastCommitAge > 24) {
|
|
82
|
-
score += 6;
|
|
83
|
-
details.push(`last commit ${days}d ago`);
|
|
84
|
-
} else if (signal.lastCommitAge > 4) {
|
|
85
|
-
score += 3;
|
|
86
|
-
details.push(`last touched ${Math.round(signal.lastCommitAge)}h ago`);
|
|
87
|
-
} else {
|
|
88
|
-
score += 1;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (score < 2) return null; // Don't surface fresh uncommitted work
|
|
92
|
-
|
|
93
|
-
const priority: Priority = score >= 8 ? "now" : score >= 4 ? "today" : "later";
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
priority,
|
|
97
|
-
score,
|
|
98
|
-
emoji: priority === "now" ? "🔴" : "🟡",
|
|
99
|
-
label: `${signal.repo}`,
|
|
100
|
-
detail: details.join(", "),
|
|
101
|
-
source: "git",
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function scoreCalendarEvent(event: CalendarEvent): ScoredItem | null {
|
|
106
|
-
// Only surface upcoming events (not past ones)
|
|
107
|
-
if (event.minutesUntilStart < -15) return null;
|
|
108
|
-
|
|
109
|
-
let score = 0;
|
|
110
|
-
|
|
111
|
-
if (event.minutesUntilStart <= 60 && event.minutesUntilStart > 0) {
|
|
112
|
-
score += 10;
|
|
113
|
-
} else if (event.minutesUntilStart <= 0 && event.minutesUntilStart > -15) {
|
|
114
|
-
score += 10; // Happening now
|
|
115
|
-
} else {
|
|
116
|
-
score += 5;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const priority: Priority = score >= 8 ? "now" : "today";
|
|
120
|
-
|
|
121
|
-
let timeLabel: string;
|
|
122
|
-
if (event.minutesUntilStart <= 0) {
|
|
123
|
-
timeLabel = "happening now";
|
|
124
|
-
} else if (event.minutesUntilStart < 60) {
|
|
125
|
-
timeLabel = `in ${event.minutesUntilStart} min`;
|
|
126
|
-
} else {
|
|
127
|
-
const hours = Math.floor(event.minutesUntilStart / 60);
|
|
128
|
-
const mins = event.minutesUntilStart % 60;
|
|
129
|
-
timeLabel = `at ${event.startTime.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
|
|
130
|
-
if (hours > 0) timeLabel += ` (${hours}h${mins > 0 ? `${mins}m` : ""} from now)`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
priority,
|
|
135
|
-
score,
|
|
136
|
-
emoji: "🔴",
|
|
137
|
-
label: `Meeting: ${event.title}`,
|
|
138
|
-
detail: timeLabel,
|
|
139
|
-
source: "calendar",
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function scoreIssue(issue: IssueSignal): ScoredItem | null {
|
|
144
|
-
let score = 0;
|
|
145
|
-
const details: string[] = [];
|
|
146
|
-
|
|
147
|
-
if (issue.ageDays > 14) {
|
|
148
|
-
score += 9;
|
|
149
|
-
details.push(`open ${Math.round(issue.ageDays)} days`);
|
|
150
|
-
} else if (issue.ageDays > 7) {
|
|
151
|
-
score += 7;
|
|
152
|
-
details.push(`open ${Math.round(issue.ageDays)} days`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const hasPriorityLabel = issue.labels.some((label) =>
|
|
156
|
-
["urgent", "critical", "bug"].includes(label.toLowerCase())
|
|
157
|
-
);
|
|
158
|
-
if (hasPriorityLabel) {
|
|
159
|
-
score += 3;
|
|
160
|
-
details.push("priority label");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (score === 0) return null;
|
|
164
|
-
|
|
165
|
-
const priority: Priority = score >= 8 ? "now" : score >= 4 ? "today" : "later";
|
|
166
|
-
const labels = issue.labels.length > 0 ? ` [${issue.labels.join(", ")}]` : "";
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
priority,
|
|
170
|
-
score,
|
|
171
|
-
emoji: "📋",
|
|
172
|
-
label: `Issue #${issue.number} on ${issue.repo}`,
|
|
173
|
-
detail: `${issue.title}${labels}${details.length > 0 ? ` — ${details.join(", ")}` : ""}`,
|
|
174
|
-
source: "issue",
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function prioritize(
|
|
179
|
-
gitSignals: GitSignal[],
|
|
180
|
-
calendarEvents: CalendarEvent[],
|
|
181
|
-
freeBlocks: FreeBlock[],
|
|
182
|
-
issues: IssueSignal[]
|
|
183
|
-
): PrioritizedOutput {
|
|
184
|
-
const allItems: ScoredItem[] = [];
|
|
185
|
-
|
|
186
|
-
// Score calendar events
|
|
187
|
-
for (const event of calendarEvents) {
|
|
188
|
-
const scored = scoreCalendarEvent(event);
|
|
189
|
-
if (scored) allItems.push(scored);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Score git repos
|
|
193
|
-
for (const signal of gitSignals) {
|
|
194
|
-
// Score uncommitted work
|
|
195
|
-
const repoItem = scoreRepoWork(signal);
|
|
196
|
-
if (repoItem) allItems.push(repoItem);
|
|
197
|
-
|
|
198
|
-
// Score PRs
|
|
199
|
-
for (const pr of signal.openPRs) {
|
|
200
|
-
allItems.push(scorePR(pr, signal.repo));
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Score issues
|
|
205
|
-
for (const issue of issues) {
|
|
206
|
-
const scored = scoreIssue(issue);
|
|
207
|
-
if (scored) allItems.push(scored);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Sort by score descending
|
|
211
|
-
allItems.sort((a, b) => b.score - a.score);
|
|
212
|
-
|
|
213
|
-
// Cap output: max 3 NOW items, max 5 TODAY items. Rest goes to later.
|
|
214
|
-
const allNow = allItems.filter((i) => i.priority === "now");
|
|
215
|
-
const allToday = allItems.filter((i) => i.priority === "today");
|
|
216
|
-
const allLater = allItems.filter((i) => i.priority === "later");
|
|
217
|
-
|
|
218
|
-
const now = allNow.slice(0, 3);
|
|
219
|
-
const todayOverflow = allNow.slice(3);
|
|
220
|
-
const today = [...todayOverflow, ...allToday].slice(0, 5);
|
|
221
|
-
const laterCount =
|
|
222
|
-
allLater.length +
|
|
223
|
-
Math.max(0, todayOverflow.length + allToday.length - 5);
|
|
224
|
-
|
|
225
|
-
// Generate suggestions
|
|
226
|
-
const suggestions: string[] = [];
|
|
227
|
-
if (freeBlocks.length > 0) {
|
|
228
|
-
const biggestBlock = freeBlocks.reduce((a, b) =>
|
|
229
|
-
a.durationMinutes > b.durationMinutes ? a : b
|
|
230
|
-
);
|
|
231
|
-
const blockStart = biggestBlock.start.toLocaleTimeString([], {
|
|
232
|
-
hour: "2-digit",
|
|
233
|
-
minute: "2-digit",
|
|
234
|
-
});
|
|
235
|
-
const blockEnd = biggestBlock.end.toLocaleTimeString([], {
|
|
236
|
-
hour: "2-digit",
|
|
237
|
-
minute: "2-digit",
|
|
238
|
-
});
|
|
239
|
-
const hours = Math.floor(biggestBlock.durationMinutes / 60);
|
|
240
|
-
const mins = biggestBlock.durationMinutes % 60;
|
|
241
|
-
const durationStr = hours > 0 ? `${hours}h${mins > 0 ? `${mins}m` : ""}` : `${mins}m`;
|
|
242
|
-
|
|
243
|
-
// Find a good item to suggest for this block
|
|
244
|
-
const deepWorkItem = today.find((i) => i.source === "git");
|
|
245
|
-
if (deepWorkItem) {
|
|
246
|
-
suggestions.push(
|
|
247
|
-
`${durationStr} free block (${blockStart}–${blockEnd}). Good for: ${deepWorkItem.label}`
|
|
248
|
-
);
|
|
249
|
-
} else {
|
|
250
|
-
suggestions.push(
|
|
251
|
-
`${durationStr} free block available (${blockStart}–${blockEnd})`
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return { now, today, laterCount, freeBlocks, suggestions };
|
|
257
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from "commander";
|
|
4
|
-
import { todayCommand } from "./cli/today.js";
|
|
5
|
-
import { switchCommand } from "./cli/switch.js";
|
|
6
|
-
import { contextCommand } from "./cli/context.js";
|
|
7
|
-
import { statusCommand } from "./cli/status.js";
|
|
8
|
-
import { onboardCommand } from "./cli/onboard.js";
|
|
9
|
-
import { configCommand } from "./cli/config.js";
|
|
10
|
-
import { daemonCommand } from "./cli/daemon.js";
|
|
11
|
-
|
|
12
|
-
const program = new Command();
|
|
13
|
-
|
|
14
|
-
program
|
|
15
|
-
.name("scope")
|
|
16
|
-
.description("Personal ops CLI — focus on what matters.")
|
|
17
|
-
.version("0.1.0");
|
|
18
|
-
|
|
19
|
-
program
|
|
20
|
-
.command("today")
|
|
21
|
-
.description("What needs your attention right now")
|
|
22
|
-
.option("--no-calendar", "Skip calendar data")
|
|
23
|
-
.option("--json", "Output as JSON")
|
|
24
|
-
.action(todayCommand);
|
|
25
|
-
|
|
26
|
-
program
|
|
27
|
-
.command("onboard")
|
|
28
|
-
.description("Guided first-time setup")
|
|
29
|
-
.action(onboardCommand);
|
|
30
|
-
|
|
31
|
-
program
|
|
32
|
-
.command("switch <project>")
|
|
33
|
-
.description("Switch to a project context")
|
|
34
|
-
.action(switchCommand);
|
|
35
|
-
|
|
36
|
-
program
|
|
37
|
-
.command("context")
|
|
38
|
-
.description("Show current project context")
|
|
39
|
-
.option("--edit", "Open scratchpad in $EDITOR")
|
|
40
|
-
.action(contextCommand);
|
|
41
|
-
|
|
42
|
-
program
|
|
43
|
-
.command("status")
|
|
44
|
-
.description("Overview of all watched projects")
|
|
45
|
-
.option("--json", "Output as JSON")
|
|
46
|
-
.action(statusCommand);
|
|
47
|
-
|
|
48
|
-
program
|
|
49
|
-
.command("config [key] [value]")
|
|
50
|
-
.description("View or edit configuration")
|
|
51
|
-
.action(configCommand);
|
|
52
|
-
|
|
53
|
-
program
|
|
54
|
-
.command("daemon <action>")
|
|
55
|
-
.description("Manage background signal checks (start|stop|status)")
|
|
56
|
-
.action(daemonCommand);
|
|
57
|
-
|
|
58
|
-
program.parse();
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { appendFileSync } from "node:fs";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { ensureScopeDir, getScopeDir } from "../store/config.js";
|
|
5
|
-
|
|
6
|
-
function commandExists(command: string): boolean {
|
|
7
|
-
const check = spawnSync("which", [command], { stdio: "pipe" });
|
|
8
|
-
return check.status === 0;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function fallbackNotify(title: string, body: string): void {
|
|
12
|
-
ensureScopeDir();
|
|
13
|
-
const logPath = join(getScopeDir(), "notifications.log");
|
|
14
|
-
appendFileSync(logPath, `[${new Date().toISOString()}] ${title}: ${body}\n`, "utf-8");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function notify(title: string, body: string): void {
|
|
18
|
-
try {
|
|
19
|
-
if (process.platform === "linux") {
|
|
20
|
-
if (commandExists("notify-send")) {
|
|
21
|
-
const result = spawnSync("notify-send", [title, body], { stdio: "pipe" });
|
|
22
|
-
if (result.status === 0) return;
|
|
23
|
-
}
|
|
24
|
-
fallbackNotify(title, body);
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (process.platform === "darwin") {
|
|
29
|
-
const escapedTitle = title.replace(/"/g, '\\"');
|
|
30
|
-
const escapedBody = body.replace(/"/g, '\\"');
|
|
31
|
-
const script = `display notification "${escapedBody}" with title "${escapedTitle}"`;
|
|
32
|
-
const result = spawnSync("osascript", ["-e", script], { stdio: "pipe" });
|
|
33
|
-
if (result.status === 0) return;
|
|
34
|
-
fallbackNotify(title, body);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
fallbackNotify(title, body);
|
|
39
|
-
} catch {
|
|
40
|
-
fallbackNotify(title, body);
|
|
41
|
-
}
|
|
42
|
-
}
|