@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.
Files changed (80) hide show
  1. package/README.md +129 -48
  2. package/dist/cli/config.d.ts +8 -1
  3. package/dist/cli/config.d.ts.map +1 -1
  4. package/dist/cli/config.js +368 -42
  5. package/dist/cli/config.js.map +1 -1
  6. package/dist/cli/init.d.ts +2 -0
  7. package/dist/cli/init.d.ts.map +1 -0
  8. package/dist/cli/init.js +15 -0
  9. package/dist/cli/init.js.map +1 -0
  10. package/dist/cli/notifications.d.ts +7 -0
  11. package/dist/cli/notifications.d.ts.map +1 -0
  12. package/dist/cli/notifications.js +77 -0
  13. package/dist/cli/notifications.js.map +1 -0
  14. package/dist/cli/onboard.d.ts.map +1 -1
  15. package/dist/cli/onboard.js +2 -1
  16. package/dist/cli/onboard.js.map +1 -1
  17. package/dist/cli/plan.d.ts +7 -0
  18. package/dist/cli/plan.d.ts.map +1 -0
  19. package/dist/cli/plan.js +111 -0
  20. package/dist/cli/plan.js.map +1 -0
  21. package/dist/cli/review.d.ts +6 -0
  22. package/dist/cli/review.d.ts.map +1 -0
  23. package/dist/cli/review.js +167 -0
  24. package/dist/cli/review.js.map +1 -0
  25. package/dist/cli/snooze.d.ts +12 -0
  26. package/dist/cli/snooze.d.ts.map +1 -0
  27. package/dist/cli/snooze.js +155 -0
  28. package/dist/cli/snooze.js.map +1 -0
  29. package/dist/cli/today.d.ts.map +1 -1
  30. package/dist/cli/today.js +69 -9
  31. package/dist/cli/today.js.map +1 -1
  32. package/dist/cli/tune.d.ts +8 -0
  33. package/dist/cli/tune.d.ts.map +1 -0
  34. package/dist/cli/tune.js +62 -0
  35. package/dist/cli/tune.js.map +1 -0
  36. package/dist/engine/prioritize.d.ts +10 -2
  37. package/dist/engine/prioritize.d.ts.map +1 -1
  38. package/dist/engine/prioritize.js +156 -25
  39. package/dist/engine/prioritize.js.map +1 -1
  40. package/dist/index.js +48 -5
  41. package/dist/index.js.map +1 -1
  42. package/dist/notifications/index.d.ts.map +1 -1
  43. package/dist/notifications/index.js +6 -10
  44. package/dist/notifications/index.js.map +1 -1
  45. package/dist/sources/activity.d.ts +32 -0
  46. package/dist/sources/activity.d.ts.map +1 -0
  47. package/dist/sources/activity.js +101 -0
  48. package/dist/sources/activity.js.map +1 -0
  49. package/dist/sources/calendar.d.ts +6 -0
  50. package/dist/sources/calendar.d.ts.map +1 -1
  51. package/dist/sources/calendar.js +114 -0
  52. package/dist/sources/calendar.js.map +1 -1
  53. package/dist/store/config.d.ts +8 -0
  54. package/dist/store/config.d.ts.map +1 -1
  55. package/dist/store/config.js +22 -0
  56. package/dist/store/config.js.map +1 -1
  57. package/dist/store/muted.d.ts +17 -0
  58. package/dist/store/muted.d.ts.map +1 -0
  59. package/dist/store/muted.js +55 -0
  60. package/dist/store/muted.js.map +1 -0
  61. package/dist/store/snapshot.d.ts +12 -0
  62. package/dist/store/snapshot.d.ts.map +1 -0
  63. package/dist/store/snapshot.js +41 -0
  64. package/dist/store/snapshot.js.map +1 -0
  65. package/package.json +8 -2
  66. package/src/cli/config.ts +0 -66
  67. package/src/cli/context.ts +0 -109
  68. package/src/cli/daemon.ts +0 -217
  69. package/src/cli/onboard.ts +0 -335
  70. package/src/cli/status.ts +0 -77
  71. package/src/cli/switch.ts +0 -93
  72. package/src/cli/today.ts +0 -114
  73. package/src/engine/prioritize.ts +0 -257
  74. package/src/index.ts +0 -58
  75. package/src/notifications/index.ts +0 -42
  76. package/src/sources/calendar.ts +0 -170
  77. package/src/sources/git.ts +0 -168
  78. package/src/sources/issues.ts +0 -62
  79. package/src/store/config.ts +0 -104
  80. package/tsconfig.json +0 -19
@@ -1,170 +0,0 @@
1
- export interface CalendarEvent {
2
- title: string;
3
- startTime: Date;
4
- endTime: Date;
5
- minutesUntilStart: number;
6
- }
7
-
8
- export interface CalendarSignal {
9
- events: CalendarEvent[];
10
- freeBlocks: FreeBlock[];
11
- }
12
-
13
- export interface FreeBlock {
14
- start: Date;
15
- end: Date;
16
- durationMinutes: number;
17
- }
18
-
19
- export async function getCalendarToday(): Promise<CalendarSignal | null> {
20
- try {
21
- const { execSync } = await import("node:child_process");
22
-
23
- // Check if gws is installed
24
- try {
25
- execSync("which gws", { stdio: "pipe" });
26
- } catch {
27
- return null;
28
- }
29
-
30
- // Get today's events via gws calendar +agenda --today
31
- const result = execSync(
32
- `gws calendar +agenda --today --format json 2>/dev/null`,
33
- {
34
- encoding: "utf-8",
35
- timeout: 15000,
36
- stdio: ["pipe", "pipe", "pipe"],
37
- }
38
- );
39
-
40
- let items: Array<{
41
- summary?: string;
42
- title?: string;
43
- start?: string | { dateTime?: string; date?: string };
44
- end?: string | { dateTime?: string; date?: string };
45
- startTime?: string;
46
- endTime?: string;
47
- }>;
48
-
49
- try {
50
- const data = JSON.parse(result);
51
- // gws may return array directly or nested in a field
52
- items = Array.isArray(data) ? data : data.items || data.events || [];
53
- } catch {
54
- // Try parsing as NDJSON (one JSON object per line)
55
- items = result
56
- .trim()
57
- .split("\n")
58
- .filter((line) => line.trim())
59
- .flatMap((line) => {
60
- try {
61
- const parsed = JSON.parse(line);
62
- // Could be a wrapper with items array or a single event
63
- if (Array.isArray(parsed)) return parsed;
64
- if (parsed.items) return parsed.items;
65
- if (parsed.events) return parsed.events;
66
- return [parsed];
67
- } catch {
68
- return [];
69
- }
70
- });
71
- }
72
-
73
- const now = Date.now();
74
-
75
- const events: CalendarEvent[] = items
76
- .map((item) => {
77
- // Handle various gws output formats
78
- const startStr =
79
- typeof item.start === "string"
80
- ? item.start
81
- : item.start?.dateTime || item.startTime;
82
- const endStr =
83
- typeof item.end === "string"
84
- ? item.end
85
- : item.end?.dateTime || item.endTime;
86
- const title = item.summary || item.title || "Untitled event";
87
-
88
- if (!startStr) return null;
89
-
90
- const startTime = new Date(startStr);
91
- const endTime = endStr ? new Date(endStr) : new Date(startTime.getTime() + 60 * 60 * 1000);
92
-
93
- if (isNaN(startTime.getTime())) return null;
94
-
95
- return {
96
- title,
97
- startTime,
98
- endTime,
99
- minutesUntilStart: Math.round(
100
- (startTime.getTime() - now) / (1000 * 60)
101
- ),
102
- };
103
- })
104
- .filter((e): e is CalendarEvent => e !== null)
105
- .sort((a, b) => a.startTime.getTime() - b.startTime.getTime());
106
-
107
- // Calculate free blocks (between events, min 30 min)
108
- const freeBlocks = calculateFreeBlocks(events, new Date());
109
-
110
- return { events, freeBlocks };
111
- } catch {
112
- return null;
113
- }
114
- }
115
-
116
- function calculateFreeBlocks(
117
- events: CalendarEvent[],
118
- today: Date
119
- ): FreeBlock[] {
120
- const blocks: FreeBlock[] = [];
121
- const now = new Date();
122
-
123
- // Working hours: 9am to 6pm
124
- const workStart = new Date(today);
125
- workStart.setHours(9, 0, 0, 0);
126
- const workEnd = new Date(today);
127
- workEnd.setHours(18, 0, 0, 0);
128
-
129
- // Start from now or work start, whichever is later
130
- let cursor = new Date(Math.max(now.getTime(), workStart.getTime()));
131
-
132
- const futureEvents = events.filter(
133
- (e) => e.endTime.getTime() > cursor.getTime()
134
- );
135
-
136
- for (const event of futureEvents) {
137
- if (event.startTime.getTime() > cursor.getTime()) {
138
- const gapMinutes = Math.round(
139
- (event.startTime.getTime() - cursor.getTime()) / (1000 * 60)
140
- );
141
- if (gapMinutes >= 30) {
142
- blocks.push({
143
- start: new Date(cursor),
144
- end: new Date(event.startTime),
145
- durationMinutes: gapMinutes,
146
- });
147
- }
148
- }
149
- // Move cursor past this event
150
- if (event.endTime.getTime() > cursor.getTime()) {
151
- cursor = new Date(event.endTime);
152
- }
153
- }
154
-
155
- // Check for free time after last event until end of work
156
- if (cursor.getTime() < workEnd.getTime()) {
157
- const gapMinutes = Math.round(
158
- (workEnd.getTime() - cursor.getTime()) / (1000 * 60)
159
- );
160
- if (gapMinutes >= 30) {
161
- blocks.push({
162
- start: new Date(cursor),
163
- end: new Date(workEnd),
164
- durationMinutes: gapMinutes,
165
- });
166
- }
167
- }
168
-
169
- return blocks;
170
- }
@@ -1,168 +0,0 @@
1
- import { simpleGit, SimpleGit } from "simple-git";
2
- import { existsSync } from "node:fs";
3
-
4
- export interface GitSignal {
5
- repo: string;
6
- branch: string;
7
- uncommittedFiles: number;
8
- lastCommitAge: number; // hours since last commit
9
- staleBranches: string[]; // branches untouched > 3 days
10
- openPRs: PRInfo[];
11
- }
12
-
13
- export interface PRInfo {
14
- number: number;
15
- title: string;
16
- url: string;
17
- ageDays: number;
18
- reviewRequested: boolean;
19
- ciStatus: "pass" | "fail" | "pending" | "unknown";
20
- hasConflicts: boolean;
21
- }
22
-
23
- export async function scanRepo(repoPath: string): Promise<GitSignal | null> {
24
- if (!existsSync(repoPath)) {
25
- return null;
26
- }
27
-
28
- const git: SimpleGit = simpleGit(repoPath);
29
-
30
- try {
31
- const isRepo = await git.checkIsRepo();
32
- if (!isRepo) return null;
33
-
34
- // Get current branch
35
- const branchInfo = await git.branch();
36
- const branch = branchInfo.current;
37
-
38
- // Get uncommitted changes
39
- const status = await git.status();
40
- const uncommittedFiles =
41
- status.modified.length +
42
- status.not_added.length +
43
- status.created.length +
44
- status.deleted.length;
45
-
46
- // Get last commit time
47
- let lastCommitAge = 0;
48
- try {
49
- const log = await git.log({ maxCount: 1 });
50
- if (log.latest?.date) {
51
- const lastCommitDate = new Date(log.latest.date);
52
- lastCommitAge =
53
- (Date.now() - lastCommitDate.getTime()) / (1000 * 60 * 60);
54
- }
55
- } catch {
56
- // No commits yet
57
- }
58
-
59
- // Get stale branches (untouched > 3 days)
60
- const staleBranches: string[] = [];
61
- try {
62
- const branches = await git.branch(["-a", "--sort=-committerdate"]);
63
- const threeDaysAgo = Date.now() - 3 * 24 * 60 * 60 * 1000;
64
-
65
- for (const branchName of branches.all) {
66
- if (branchName === branch) continue;
67
- if (branchName.startsWith("remotes/")) continue;
68
-
69
- try {
70
- const branchLog = await git.log({
71
- maxCount: 1,
72
- from: branchName,
73
- });
74
- if (branchLog.latest?.date) {
75
- const branchDate = new Date(branchLog.latest.date);
76
- if (branchDate.getTime() < threeDaysAgo) {
77
- staleBranches.push(branchName);
78
- }
79
- }
80
- } catch {
81
- // Skip branches we can't read
82
- }
83
- }
84
- } catch {
85
- // Branch listing failed
86
- }
87
-
88
- // Get open PRs via gh CLI
89
- const openPRs = await getOpenPRs(repoPath);
90
-
91
- const repoName = repoPath.split("/").pop() || repoPath;
92
-
93
- return {
94
- repo: repoName,
95
- branch,
96
- uncommittedFiles,
97
- lastCommitAge,
98
- staleBranches: staleBranches.slice(0, 5), // Cap at 5
99
- openPRs,
100
- };
101
- } catch {
102
- return null;
103
- }
104
- }
105
-
106
- async function getOpenPRs(repoPath: string): Promise<PRInfo[]> {
107
- try {
108
- const { execSync } = await import("node:child_process");
109
- const result = execSync(
110
- `gh pr list --json number,title,url,createdAt,reviewRequests,statusCheckRollup,mergeable --limit 10`,
111
- {
112
- cwd: repoPath,
113
- encoding: "utf-8",
114
- timeout: 10000,
115
- stdio: ["pipe", "pipe", "pipe"],
116
- }
117
- );
118
-
119
- const prs = JSON.parse(result) as Array<{
120
- number: number;
121
- title: string;
122
- url: string;
123
- createdAt: string;
124
- reviewRequests: Array<{ login?: string }>;
125
- statusCheckRollup: Array<{ conclusion: string }> | null;
126
- mergeable: string;
127
- }>;
128
-
129
- return prs.map((pr) => {
130
- const ageDays =
131
- (Date.now() - new Date(pr.createdAt).getTime()) /
132
- (1000 * 60 * 60 * 24);
133
-
134
- let ciStatus: PRInfo["ciStatus"] = "unknown";
135
- if (pr.statusCheckRollup && pr.statusCheckRollup.length > 0) {
136
- const hasFailure = pr.statusCheckRollup.some(
137
- (c) => c.conclusion === "FAILURE"
138
- );
139
- const allSuccess = pr.statusCheckRollup.every(
140
- (c) => c.conclusion === "SUCCESS"
141
- );
142
- if (hasFailure) ciStatus = "fail";
143
- else if (allSuccess) ciStatus = "pass";
144
- else ciStatus = "pending";
145
- }
146
-
147
- return {
148
- number: pr.number,
149
- title: pr.title,
150
- url: pr.url,
151
- ageDays: Math.round(ageDays * 10) / 10,
152
- reviewRequested: pr.reviewRequests.length > 0,
153
- ciStatus,
154
- hasConflicts: pr.mergeable === "CONFLICTING",
155
- };
156
- });
157
- } catch {
158
- // gh CLI not available or not in a repo with remote
159
- return [];
160
- }
161
- }
162
-
163
- export async function scanAllRepos(
164
- repoPaths: string[]
165
- ): Promise<GitSignal[]> {
166
- const results = await Promise.all(repoPaths.map(scanRepo));
167
- return results.filter((r): r is GitSignal => r !== null);
168
- }
@@ -1,62 +0,0 @@
1
- export interface IssueSignal {
2
- number: number;
3
- title: string;
4
- url: string;
5
- repo: string;
6
- ageDays: number;
7
- labels: string[];
8
- }
9
-
10
- export interface IssueScanResult {
11
- available: boolean;
12
- issues: IssueSignal[];
13
- }
14
-
15
- export async function scanAssignedIssues(): Promise<IssueScanResult> {
16
- try {
17
- const { execSync } = await import("node:child_process");
18
-
19
- try {
20
- execSync("which gh", { stdio: "pipe" });
21
- } catch {
22
- return { available: false, issues: [] };
23
- }
24
-
25
- const result = execSync(
26
- "gh issue list --assignee @me --state open --json number,title,url,createdAt,labels,repository --limit 20",
27
- {
28
- encoding: "utf-8",
29
- timeout: 10000,
30
- stdio: ["pipe", "pipe", "pipe"],
31
- }
32
- );
33
-
34
- const parsed = JSON.parse(result) as Array<{
35
- number: number;
36
- title: string;
37
- url: string;
38
- createdAt: string;
39
- labels?: Array<{ name?: string }>;
40
- repository?: { nameWithOwner?: string; name?: string };
41
- }>;
42
-
43
- const issues: IssueSignal[] = parsed.map((issue) => {
44
- const ageDays =
45
- (Date.now() - new Date(issue.createdAt).getTime()) /
46
- (1000 * 60 * 60 * 24);
47
-
48
- return {
49
- number: issue.number,
50
- title: issue.title,
51
- url: issue.url,
52
- repo: issue.repository?.nameWithOwner || issue.repository?.name || "unknown",
53
- ageDays: Math.round(ageDays * 10) / 10,
54
- labels: (issue.labels ?? []).map((label) => label.name || "").filter(Boolean),
55
- };
56
- });
57
-
58
- return { available: true, issues };
59
- } catch {
60
- return { available: true, issues: [] };
61
- }
62
- }
@@ -1,104 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { parse as parseToml } from "toml";
5
-
6
- export interface ScopeConfig {
7
- repos: string[];
8
- projects: Record<
9
- string,
10
- {
11
- path: string;
12
- repos?: string[];
13
- description?: string;
14
- }
15
- >;
16
- calendar: {
17
- enabled: boolean;
18
- backend: "gws";
19
- };
20
- daemon: {
21
- enabled: boolean;
22
- intervalMinutes: number;
23
- };
24
- }
25
-
26
- const SCOPE_DIR = join(homedir(), ".scope");
27
- const CONFIG_PATH = join(SCOPE_DIR, "config.toml");
28
-
29
- export function getScopeDir(): string {
30
- return SCOPE_DIR;
31
- }
32
-
33
- export function ensureScopeDir(): void {
34
- if (!existsSync(SCOPE_DIR)) {
35
- mkdirSync(SCOPE_DIR, { recursive: true });
36
- }
37
- const contextsDir = join(SCOPE_DIR, "contexts");
38
- if (!existsSync(contextsDir)) {
39
- mkdirSync(contextsDir, { recursive: true });
40
- }
41
- }
42
-
43
- export function configExists(): boolean {
44
- return existsSync(CONFIG_PATH);
45
- }
46
-
47
- export function loadConfig(): ScopeConfig {
48
- if (!configExists()) {
49
- return {
50
- repos: [],
51
- projects: {},
52
- calendar: { enabled: false, backend: "gws" },
53
- daemon: { enabled: false, intervalMinutes: 15 },
54
- };
55
- }
56
-
57
- const raw = readFileSync(CONFIG_PATH, "utf-8");
58
- const parsed = parseToml(raw) as Partial<ScopeConfig>;
59
-
60
- return {
61
- repos: parsed.repos ?? [],
62
- projects: parsed.projects ?? {},
63
- calendar: {
64
- enabled: parsed.calendar?.enabled ?? false,
65
- backend: parsed.calendar?.backend ?? "gws",
66
- },
67
- daemon: {
68
- enabled: parsed.daemon?.enabled ?? false,
69
- intervalMinutes: parsed.daemon?.intervalMinutes ?? 15,
70
- },
71
- };
72
- }
73
-
74
- export function saveConfig(config: ScopeConfig): void {
75
- ensureScopeDir();
76
-
77
- const lines: string[] = [];
78
- lines.push("# Scope configuration");
79
- lines.push("");
80
- lines.push(`repos = [${config.repos.map((r) => `"${r}"`).join(", ")}]`);
81
- lines.push("");
82
- lines.push("[calendar]");
83
- lines.push(`enabled = ${config.calendar.enabled}`);
84
- lines.push(`backend = "${config.calendar.backend}"`);
85
- lines.push("");
86
- lines.push("[daemon]");
87
- lines.push(`enabled = ${config.daemon.enabled}`);
88
- lines.push(`intervalMinutes = ${config.daemon.intervalMinutes}`);
89
- lines.push("");
90
-
91
- for (const [name, project] of Object.entries(config.projects)) {
92
- lines.push(`[projects.${name}]`);
93
- lines.push(`path = "${project.path}"`);
94
- if (project.repos && project.repos.length > 0) {
95
- lines.push(`repos = [${project.repos.map((r) => `"${r}"`).join(", ")}]`);
96
- }
97
- if (project.description) {
98
- lines.push(`description = "${project.description}"`);
99
- }
100
- lines.push("");
101
- }
102
-
103
- writeFileSync(CONFIG_PATH, lines.join("\n"), "utf-8");
104
- }
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "Node16",
5
- "moduleResolution": "Node16",
6
- "outDir": "dist",
7
- "rootDir": "src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "resolveJsonModule": true,
13
- "declaration": true,
14
- "declarationMap": true,
15
- "sourceMap": true
16
- },
17
- "include": ["src/**/*"],
18
- "exclude": ["node_modules", "dist"]
19
- }