@bobfrankston/gcal 0.1.48 → 0.1.50

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/gcal.ts CHANGED
@@ -107,6 +107,66 @@ async function patchEvent(
107
107
  return await res.json() as GoogleEvent;
108
108
  }
109
109
 
110
+ /** Print warnings for events that overlap or fall within 1 hour of [start, end]. */
111
+ async function checkProximity(
112
+ accessToken: string,
113
+ calendarId: string,
114
+ start: Date,
115
+ end: Date,
116
+ excludeBaseId?: string
117
+ ): Promise<void> {
118
+ const HOUR = 60 * 60_000;
119
+ const windowMin = new Date(start.getTime() - HOUR).toISOString();
120
+ const windowMax = new Date(end.getTime() + HOUR).toISOString();
121
+ let nearby: GoogleEvent[];
122
+ try {
123
+ nearby = await listEvents(accessToken, calendarId, 50, windowMin, windowMax);
124
+ } catch {
125
+ return; // Non-fatal — skip warning on fetch failure
126
+ }
127
+
128
+ const sMs = start.getTime();
129
+ const eMs = end.getTime();
130
+ const warnings: string[] = [];
131
+
132
+ for (const e of nearby) {
133
+ if (e.eventType === 'birthday') continue;
134
+ const baseId = (e.id || '').split('_')[0];
135
+ if (excludeBaseId && baseId === excludeBaseId) continue;
136
+ if (!e.start) continue;
137
+
138
+ let evStart: number;
139
+ let evEnd: number;
140
+ if (e.start.dateTime && e.end?.dateTime) {
141
+ evStart = new Date(e.start.dateTime).getTime();
142
+ evEnd = new Date(e.end.dateTime).getTime();
143
+ } else if (e.start.date && e.end?.date) {
144
+ evStart = parseAllDay(e.start.date).getTime();
145
+ evEnd = parseAllDay(e.end.date).getTime();
146
+ } else {
147
+ continue;
148
+ }
149
+
150
+ const summary = e.summary || '(no title)';
151
+ const when = formatDateTime(e.start);
152
+
153
+ if (evStart < eMs && evEnd > sMs) {
154
+ warnings.push(` OVERLAPS: ${when} ${summary}`);
155
+ } else if (evEnd <= sMs && sMs - evEnd <= HOUR) {
156
+ const mins = Math.round((sMs - evEnd) / 60_000);
157
+ warnings.push(` ${String(mins).padStart(2)}m before: ${when} ${summary}`);
158
+ } else if (evStart >= eMs && evStart - eMs <= HOUR) {
159
+ const mins = Math.round((evStart - eMs) / 60_000);
160
+ warnings.push(` ${String(mins).padStart(2)}m after: ${when} ${summary}`);
161
+ }
162
+ }
163
+
164
+ if (warnings.length > 0) {
165
+ console.log(`\nWarning: nearby events:`);
166
+ for (const w of warnings) console.log(w);
167
+ }
168
+ }
169
+
110
170
  async function importIcsFile(
111
171
  filePath: string,
112
172
  accessToken: string,
@@ -195,6 +255,7 @@ Usage: gcal <file.ics> Import ICS file (file association)
195
255
 
196
256
  Commands:
197
257
  list List upcoming events
258
+ show Show full details for an event (-json for JSON)
198
259
  add Add event (explicit, AI, or interactive)
199
260
  del | delete Delete event(s) by ID
200
261
  remind Add reminder(s) to existing event
@@ -226,6 +287,15 @@ const USAGE: Record<string, string> = {
226
287
  gcal list -since "10 days ago"
227
288
  gcal list -since "april 1" -till "may 1"
228
289
  gcal list -since "april 1" -n 50
290
+ `,
291
+ show: `gcal show <id> [-json]
292
+ Show full details for an event (by ID prefix).
293
+ -json Output raw event JSON instead of human-readable text.
294
+ Searches up to 30 days back; widen with -since.
295
+
296
+ Examples:
297
+ gcal show abc12345
298
+ gcal show abc12345 -json
229
299
  `,
230
300
  add: `gcal add <title> <when> [duration] Explicit
231
301
  gcal add "<free text>" AI-parsed single arg
@@ -314,6 +384,7 @@ interface ParsedArgs {
314
384
  birthdays: boolean;
315
385
  clip: boolean;
316
386
  all: boolean;
387
+ json: boolean;
317
388
  reminders: number[];
318
389
  since?: Date;
319
390
  till?: Date;
@@ -333,6 +404,7 @@ function parseArgs(argv: string[]): ParsedArgs {
333
404
  birthdays: false,
334
405
  clip: false,
335
406
  all: false,
407
+ json: false,
336
408
  reminders: [],
337
409
  helpCmd: ''
338
410
  };
@@ -373,6 +445,10 @@ function parseArgs(argv: string[]): ParsedArgs {
373
445
  case '--all':
374
446
  result.all = true;
375
447
  break;
448
+ case '-json':
449
+ case '--json':
450
+ result.json = true;
451
+ break;
376
452
  case '-r':
377
453
  case '-reminder':
378
454
  case '--reminder': {
@@ -682,6 +758,7 @@ async function main(): Promise<void> {
682
758
  };
683
759
 
684
760
  const token = await getAccessToken(user, true);
761
+ await checkProximity(token, parsed.calendar, startTime, endTime);
685
762
  const created = await createEvent(token, event, parsed.calendar);
686
763
  console.log(`\nEvent created: ${created.summary}`);
687
764
  console.log(` When: ${formatDateTime(created.start)} - ${formatDateTime(created.end)}`);
@@ -725,6 +802,7 @@ async function main(): Promise<void> {
725
802
  }
726
803
 
727
804
  const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
805
+ const token = await getAccessToken(user, true);
728
806
  const events: GoogleEvent[] = [];
729
807
  for (const extracted of extractedEvents) {
730
808
  const tz = extracted.timeZone || localTz;
@@ -752,6 +830,8 @@ async function main(): Promise<void> {
752
830
  console.log(` When: ${formatDateTime(event.start)} - ${formatDateTime(event.end)} (${extracted.duration || '1h'})${tz !== localTz ? ` [${tz}]` : ''}`);
753
831
  if (extracted.location) console.log(` Where: ${extracted.location}`);
754
832
  if (extracted.description) console.log(` Note: ${extracted.description}`);
833
+
834
+ await checkProximity(token, parsed.calendar, new Date(startDt), endDate);
755
835
  }
756
836
 
757
837
  if (events.length === 0) {
@@ -776,7 +856,6 @@ async function main(): Promise<void> {
776
856
  break;
777
857
  }
778
858
 
779
- const token = await getAccessToken(user, true);
780
859
  for (const event of events) {
781
860
  const created = await createEvent(token, event, parsed.calendar);
782
861
  console.log(`\nEvent created: ${created.summary}`);
@@ -1004,6 +1083,17 @@ async function main(): Promise<void> {
1004
1083
  newEndDisplay = patch.end!;
1005
1084
  }
1006
1085
 
1086
+ // Proximity check for timed events (skip all-day)
1087
+ if (!origIsAllDay && patch.start?.dateTime && patch.end?.dateTime) {
1088
+ await checkProximity(
1089
+ token,
1090
+ parsed.calendar,
1091
+ new Date(patch.start.dateTime),
1092
+ new Date(patch.end.dateTime),
1093
+ (event.id || '').split('_')[0]
1094
+ );
1095
+ }
1096
+
1007
1097
  const updated = await patchEvent(token, event.id!, patch, parsed.calendar);
1008
1098
  console.log(`Rescheduled: ${updated.summary}`);
1009
1099
  console.log(` From: ${formatDateTime(event.start!)} - ${formatDateTime(event.end!)}`);
@@ -1011,6 +1101,100 @@ async function main(): Promise<void> {
1011
1101
  break;
1012
1102
  }
1013
1103
 
1104
+ case 'show': {
1105
+ if (parsed.args.length < 1) {
1106
+ console.error('Usage: gcal show <id> [-json]');
1107
+ console.error('Use "gcal list" to see event IDs');
1108
+ process.exit(1);
1109
+ }
1110
+
1111
+ const idPrefix = parsed.args[0];
1112
+ const lookback = parsed.since
1113
+ ? parsed.since.toISOString()
1114
+ : new Date(Date.now() - 30 * 86400_000).toISOString();
1115
+ const timeMax = parsed.till ? parsed.till.toISOString() : undefined;
1116
+
1117
+ const token = await getAccessToken(user, false);
1118
+ const events = await listEvents(token, parsed.calendar, 250, lookback, timeMax);
1119
+
1120
+ let matches = events.filter(e => e.id?.startsWith(idPrefix));
1121
+ if (!parsed.birthdays) {
1122
+ matches = matches.filter(e => e.eventType !== 'birthday');
1123
+ }
1124
+ const unique = [...new Map(matches.map(e => [(e.id || '').split('_')[0], e])).values()];
1125
+
1126
+ if (unique.length === 0) {
1127
+ console.error(`${idPrefix}: not found (searched from ${lookback.slice(0, 10)})`);
1128
+ process.exit(1);
1129
+ }
1130
+ if (unique.length > 1) {
1131
+ console.error(`${idPrefix}: ambiguous (${unique.length} matches)`);
1132
+ for (const e of unique) {
1133
+ console.error(` ${e.id?.slice(0, 8)} - ${e.summary}`);
1134
+ }
1135
+ process.exit(1);
1136
+ }
1137
+
1138
+ const event = unique[0];
1139
+ if (parsed.json) {
1140
+ console.log(JSON.stringify(event, null, 2));
1141
+ break;
1142
+ }
1143
+
1144
+ console.log(`\n${event.summary || '(no title)'}`);
1145
+ const duration = (event.start && event.end) ? formatDuration(event.start, event.end) : '';
1146
+ const whenLine = event.start && event.end
1147
+ ? `${formatDateTime(event.start)} - ${formatDateTime(event.end)}${duration ? ` (${duration})` : ''}`
1148
+ : '?';
1149
+ console.log(` When: ${whenLine}`);
1150
+ if (event.start?.timeZone) {
1151
+ console.log(` TZ: ${event.start.timeZone}`);
1152
+ }
1153
+ if (event.location) console.log(` Where: ${event.location}`);
1154
+ if (event.description) {
1155
+ const indented = event.description.split('\n').map((l, i) => i === 0 ? l : ` ${l}`).join('\n');
1156
+ console.log(` Notes: ${indented}`);
1157
+ }
1158
+ if (event.recurrence?.length) {
1159
+ console.log(` Repeat: ${event.recurrence.join('; ')}`);
1160
+ }
1161
+ if (event.recurringEventId) {
1162
+ console.log(` Series ID: ${event.recurringEventId}`);
1163
+ }
1164
+ if (event.attendees?.length) {
1165
+ console.log(` Attendees:`);
1166
+ for (const a of event.attendees) {
1167
+ const name = a.displayName ? `${a.displayName} <${a.email}>` : (a.email || '?');
1168
+ const status = a.responseStatus ? ` [${a.responseStatus}]` : '';
1169
+ const role = a.organizer ? ' (organizer)' : a.optional ? ' (optional)' : '';
1170
+ console.log(` ${name}${status}${role}`);
1171
+ }
1172
+ }
1173
+ if (event.reminders?.overrides?.length) {
1174
+ console.log(` Reminders:`);
1175
+ for (const r of event.reminders.overrides) {
1176
+ const m = r.minutes || 0;
1177
+ const dur = m >= 60 && m % 60 === 0 ? `${m / 60}h` : `${m}m`;
1178
+ console.log(` ${dur} (${r.method || 'popup'})`);
1179
+ }
1180
+ } else if (event.reminders?.useDefault) {
1181
+ console.log(` Reminders: (calendar default)`);
1182
+ }
1183
+ if (event.creator?.email) {
1184
+ console.log(` Creator: ${event.creator.displayName ? `${event.creator.displayName} <${event.creator.email}>` : event.creator.email}`);
1185
+ }
1186
+ if (event.organizer?.email && event.organizer.email !== event.creator?.email) {
1187
+ console.log(` Organizer: ${event.organizer.displayName ? `${event.organizer.displayName} <${event.organizer.email}>` : event.organizer.email}`);
1188
+ }
1189
+ if (event.hangoutLink) console.log(` Meet: ${event.hangoutLink}`);
1190
+ if (event.htmlLink) console.log(` Link: ${event.htmlLink}`);
1191
+ console.log(` Status: ${event.status || 'confirmed'}`);
1192
+ if (event.created) console.log(` Created: ${formatDateTime({ dateTime: event.created })}`);
1193
+ if (event.updated) console.log(` Updated: ${formatDateTime({ dateTime: event.updated })}`);
1194
+ console.log(` ID: ${event.id}`);
1195
+ break;
1196
+ }
1197
+
1014
1198
  default:
1015
1199
  console.error(`Unknown command: ${parsed.command}`);
1016
1200
  showUsage();
@@ -1019,8 +1203,10 @@ async function main(): Promise<void> {
1019
1203
  }
1020
1204
 
1021
1205
  if (import.meta.main) {
1022
- main().catch(e => {
1023
- console.error(`Error: ${e.message}`);
1024
- process.exit(1);
1025
- });
1206
+ main()
1207
+ .then(() => process.exit(0))
1208
+ .catch(e => {
1209
+ console.error(`Error: ${e.message}`);
1210
+ process.exit(1);
1211
+ });
1026
1212
  }
@@ -12,6 +12,12 @@ export interface ExtractedEvent {
12
12
  description?: string;
13
13
  }
14
14
  export declare function extractEventsFromText(text: string): Promise<ExtractedEvent[]>;
15
+ export interface ExtractedTask {
16
+ title: string;
17
+ due?: string; /** YYYY-MM-DD, optional */
18
+ notes?: string;
19
+ }
20
+ export declare function extractTasksFromText(text: string): Promise<ExtractedTask[]>;
15
21
  /** Read clipboard text (cross-platform via clipboardy) */
16
22
  export declare function readClipboard(): string;
17
23
  //# sourceMappingURL=aihelper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"aihelper.d.ts","sourceRoot":"","sources":["aihelper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAkCH,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AA6BD,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAoEnF;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,IAAI,MAAM,CAMtC"}
1
+ {"version":3,"file":"aihelper.d.ts","sourceRoot":"","sources":["aihelper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAkCH,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AA6BD,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAoEnF;AAED,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,CAAO,2BAA2B;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAwBD,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA0DjF;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,IAAI,MAAM,CAMtC"}
package/glib/aihelper.js CHANGED
@@ -121,6 +121,83 @@ export async function extractEventsFromText(text) {
121
121
  return [];
122
122
  }
123
123
  }
124
+ const TASK_EXTRACTION_PROMPT = `Extract to-do tasks from the user's text and return ONLY valid JSON.
125
+
126
+ Today's date is {{TODAY}}.
127
+
128
+ The text may describe one or multiple tasks. Always return a JSON array of task objects.
129
+
130
+ Output format:
131
+ [
132
+ {
133
+ "title": "Task title",
134
+ "due": "YYYY-MM-DD",
135
+ "notes": "optional details"
136
+ }
137
+ ]
138
+
139
+ Rules:
140
+ - title: concise task title (imperative if natural, e.g. "Call plumber")
141
+ - due: date-only (YYYY-MM-DD). Resolve relative dates ("tomorrow", "next Friday") using today's date. Omit if no date is implied.
142
+ - notes: include supporting details the title doesn't already capture. Omit if none.
143
+ - Google Tasks ignores time-of-day, so do not include hours/minutes.
144
+ - Return ONLY the JSON array, no markdown, no explanation`;
145
+ export async function extractTasksFromText(text) {
146
+ const apiKey = process.env.ANTHROPIC_API_KEY;
147
+ if (!apiKey) {
148
+ const home = process.env.HOME || process.env.USERPROFILE || '';
149
+ const appData = process.env.APPDATA || path.join(home, '.config');
150
+ const keysPath = path.join(appData, 'gcards', 'keys.env');
151
+ console.error(`\nANTHROPIC_API_KEY not set.`);
152
+ console.error(`Add it to: ${keysPath}`);
153
+ console.error(`Format: ANTHROPIC_API_KEY=sk-ant-...\n`);
154
+ return [];
155
+ }
156
+ const today = new Date().toISOString().split('T')[0];
157
+ const dayName = new Date().toLocaleDateString('en-US', { weekday: 'long' });
158
+ const systemPrompt = TASK_EXTRACTION_PROMPT
159
+ .replace('{{TODAY}}', `${today} (${dayName})`);
160
+ try {
161
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
162
+ method: 'POST',
163
+ headers: {
164
+ 'Content-Type': 'application/json',
165
+ 'x-api-key': apiKey,
166
+ 'anthropic-version': '2023-06-01'
167
+ },
168
+ body: JSON.stringify({
169
+ model: 'claude-haiku-4-5-20251001',
170
+ max_tokens: 512,
171
+ system: systemPrompt,
172
+ messages: [{ role: 'user', content: text }]
173
+ })
174
+ });
175
+ if (!response.ok) {
176
+ const errorText = await response.text();
177
+ console.error(`Claude API error: ${response.status} ${errorText}`);
178
+ return [];
179
+ }
180
+ const data = await response.json();
181
+ const content = data.content?.[0]?.text;
182
+ if (!content)
183
+ return [];
184
+ const jsonMatch = content.match(/\[[\s\S]*\]/);
185
+ if (!jsonMatch) {
186
+ const objMatch = content.match(/\{[\s\S]*\}/);
187
+ if (!objMatch) {
188
+ console.error('No JSON found in AI response');
189
+ return [];
190
+ }
191
+ return [JSON.parse(objMatch[0])];
192
+ }
193
+ const parsed = JSON.parse(jsonMatch[0]);
194
+ return Array.isArray(parsed) ? parsed : [parsed];
195
+ }
196
+ catch (error) {
197
+ console.error(`Error extracting tasks: ${error}`);
198
+ return [];
199
+ }
200
+ }
124
201
  /** Read clipboard text (cross-platform via clipboardy) */
125
202
  export function readClipboard() {
126
203
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"aihelper.js","sourceRoot":"","sources":["aihelper.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"aihelper.js","sourceRoot":"","sources":["aihelper.ts"],"names":[],"mappings":"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"}
package/glib/aihelper.ts CHANGED
@@ -142,6 +142,94 @@ export async function extractEventsFromText(text: string): Promise<ExtractedEven
142
142
  }
143
143
  }
144
144
 
145
+ export interface ExtractedTask {
146
+ title: string;
147
+ due?: string; /** YYYY-MM-DD, optional */
148
+ notes?: string;
149
+ }
150
+
151
+ const TASK_EXTRACTION_PROMPT = `Extract to-do tasks from the user's text and return ONLY valid JSON.
152
+
153
+ Today's date is {{TODAY}}.
154
+
155
+ The text may describe one or multiple tasks. Always return a JSON array of task objects.
156
+
157
+ Output format:
158
+ [
159
+ {
160
+ "title": "Task title",
161
+ "due": "YYYY-MM-DD",
162
+ "notes": "optional details"
163
+ }
164
+ ]
165
+
166
+ Rules:
167
+ - title: concise task title (imperative if natural, e.g. "Call plumber")
168
+ - due: date-only (YYYY-MM-DD). Resolve relative dates ("tomorrow", "next Friday") using today's date. Omit if no date is implied.
169
+ - notes: include supporting details the title doesn't already capture. Omit if none.
170
+ - Google Tasks ignores time-of-day, so do not include hours/minutes.
171
+ - Return ONLY the JSON array, no markdown, no explanation`;
172
+
173
+ export async function extractTasksFromText(text: string): Promise<ExtractedTask[]> {
174
+ const apiKey = process.env.ANTHROPIC_API_KEY;
175
+ if (!apiKey) {
176
+ const home = process.env.HOME || process.env.USERPROFILE || '';
177
+ const appData = process.env.APPDATA || path.join(home, '.config');
178
+ const keysPath = path.join(appData, 'gcards', 'keys.env');
179
+ console.error(`\nANTHROPIC_API_KEY not set.`);
180
+ console.error(`Add it to: ${keysPath}`);
181
+ console.error(`Format: ANTHROPIC_API_KEY=sk-ant-...\n`);
182
+ return [];
183
+ }
184
+
185
+ const today = new Date().toISOString().split('T')[0];
186
+ const dayName = new Date().toLocaleDateString('en-US', { weekday: 'long' });
187
+ const systemPrompt = TASK_EXTRACTION_PROMPT
188
+ .replace('{{TODAY}}', `${today} (${dayName})`);
189
+
190
+ try {
191
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
192
+ method: 'POST',
193
+ headers: {
194
+ 'Content-Type': 'application/json',
195
+ 'x-api-key': apiKey,
196
+ 'anthropic-version': '2023-06-01'
197
+ },
198
+ body: JSON.stringify({
199
+ model: 'claude-haiku-4-5-20251001',
200
+ max_tokens: 512,
201
+ system: systemPrompt,
202
+ messages: [{ role: 'user', content: text }]
203
+ })
204
+ });
205
+
206
+ if (!response.ok) {
207
+ const errorText = await response.text();
208
+ console.error(`Claude API error: ${response.status} ${errorText}`);
209
+ return [];
210
+ }
211
+
212
+ const data = await response.json() as any;
213
+ const content = data.content?.[0]?.text;
214
+ if (!content) return [];
215
+
216
+ const jsonMatch = content.match(/\[[\s\S]*\]/);
217
+ if (!jsonMatch) {
218
+ const objMatch = content.match(/\{[\s\S]*\}/);
219
+ if (!objMatch) {
220
+ console.error('No JSON found in AI response');
221
+ return [];
222
+ }
223
+ return [JSON.parse(objMatch[0]) as ExtractedTask];
224
+ }
225
+ const parsed = JSON.parse(jsonMatch[0]);
226
+ return Array.isArray(parsed) ? parsed as ExtractedTask[] : [parsed as ExtractedTask];
227
+ } catch (error) {
228
+ console.error(`Error extracting tasks: ${error}`);
229
+ return [];
230
+ }
231
+ }
232
+
145
233
  /** Read clipboard text (cross-platform via clipboardy) */
146
234
  export function readClipboard(): string {
147
235
  try {
package/gtask.js CHANGED
@@ -7,6 +7,7 @@ import { createInterface } from 'readline/promises';
7
7
  import { loadConfig, saveConfig, parseDateTime, formatYMD, normalizeUser } from './glib/gutils.js';
8
8
  import { setupAbortHandler, getAccessToken } from './glib/goauth.js';
9
9
  import { listTaskLists, listTasks, createTask, patchTask, deleteTask, moveTask, clearCompleted, resolveTaskList } from './glib/tasksapi.js';
10
+ import { extractTasksFromText, readClipboard } from './glib/aihelper.js';
10
11
  import pkg from './package.json' with { type: 'json' };
11
12
  const VERSION = pkg.version;
12
13
  const USAGE_SUMMARY = `gtask v${VERSION} - Google Tasks CLI
@@ -16,6 +17,8 @@ Usage: gtask <command> [options]
16
17
 
17
18
  Commands:
18
19
  add <title> [when] Add a task (optional due date)
20
+ add -clip AI-parse task(s) from clipboard
21
+ add "<freeform text>" AI-parse task(s) from a single argument
19
22
  list List open tasks
20
23
  lists List all tasklists
21
24
  done <id> Mark task completed (id prefix)
@@ -32,14 +35,23 @@ Global options:
32
35
  `;
33
36
  const USAGE = {
34
37
  add: `gtask add <title> [when] [-l <list>] [-n <notes>]
35
- Add a task. <when> is an optional due date (date-only; time is ignored
38
+ gtask add -clip [-l <list>]
39
+ gtask add "<freeform text>" [-l <list>]
40
+
41
+ Explicit mode: supply title and optional <when> (date-only; time is ignored
36
42
  by Google Tasks). Wrap multi-word titles in quotes.
37
43
 
44
+ AI mode: with -clip the clipboard is parsed into one or more tasks via
45
+ Claude. A single quoted argument is parsed the same way. With no args
46
+ and no -clip, you'll be prompted to type the description.
47
+
38
48
  Examples:
39
49
  gtask add "Write report"
40
50
  gtask add "Write report" friday
41
51
  gtask add "Pay bills" "april 30" -n "rent + utilities"
42
52
  gtask add "Call plumber" tomorrow -l Errands
53
+ gtask add -clip
54
+ gtask add "call dentist tomorrow, pick up rx friday"
43
55
  `,
44
56
  list: `gtask list [-l <list>] [-a] [-since <date>] [-till <date>]
45
57
  List open tasks in a tasklist.
@@ -108,6 +120,7 @@ function parseArgs(argv) {
108
120
  title: '',
109
121
  when: '',
110
122
  showAll: false,
123
+ clip: false,
111
124
  help: false,
112
125
  helpCmd: ''
113
126
  };
@@ -145,6 +158,10 @@ function parseArgs(argv) {
145
158
  case '--all':
146
159
  result.showAll = true;
147
160
  break;
161
+ case '-clip':
162
+ case '--clip':
163
+ result.clip = true;
164
+ break;
148
165
  case '-h':
149
166
  case '-help':
150
167
  case '--help':
@@ -282,25 +299,78 @@ async function main() {
282
299
  break;
283
300
  }
284
301
  case 'add': {
285
- if (parsed.args.length < 1) {
286
- showUsage('add');
302
+ // Explicit mode: gtask add "title" [when]
303
+ if (parsed.args.length >= 2 && !parsed.clip) {
304
+ const [title, when] = parsed.args;
305
+ const task = { title };
306
+ if (when)
307
+ task.due = buildDue(when);
308
+ if (parsed.notes)
309
+ task.notes = parsed.notes;
310
+ const token = await getAccessToken(user, true);
311
+ const tl = await resolveTaskList(token, parsed.list);
312
+ const created = await createTask(token, task, tl.id);
313
+ console.log(`\nTask created in ${tl.title}: ${created.title}`);
314
+ if (created.due)
315
+ console.log(` Due: ${dueToYMD(created.due)}`);
316
+ if (created.notes)
317
+ console.log(` Notes: ${created.notes}`);
318
+ console.log(` ID: ${(created.id || '').slice(0, 8)}`);
319
+ break;
320
+ }
321
+ // AI mode: freeform text from clipboard, single arg, or prompt
322
+ let inputText;
323
+ if (parsed.clip) {
324
+ console.log('Reading from clipboard...');
325
+ inputText = readClipboard();
326
+ if (!inputText) {
327
+ console.error('Clipboard is empty');
328
+ process.exit(1);
329
+ }
330
+ console.log(`Clipboard: ${inputText.substring(0, 200)}${inputText.length > 200 ? '...' : ''}`);
331
+ }
332
+ else if (parsed.args.length === 1) {
333
+ inputText = parsed.args[0].trim();
334
+ if (!inputText) {
335
+ console.error('Task description is empty');
336
+ process.exit(1);
337
+ }
338
+ }
339
+ else {
340
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
341
+ inputText = (await rl.question('Describe the task(s): ')).trim();
342
+ rl.close();
343
+ if (!inputText) {
344
+ console.error('No input provided');
345
+ process.exit(1);
346
+ }
347
+ }
348
+ console.log('Extracting task details...');
349
+ const extracted = await extractTasksFromText(inputText);
350
+ if (extracted.length === 0) {
351
+ console.error('Failed to extract task details from text');
287
352
  process.exit(1);
288
353
  }
289
- const [title, when] = parsed.args;
290
- const task = { title };
291
- if (when)
292
- task.due = buildDue(when);
293
- if (parsed.notes)
294
- task.notes = parsed.notes;
295
354
  const token = await getAccessToken(user, true);
296
355
  const tl = await resolveTaskList(token, parsed.list);
297
- const created = await createTask(token, task, tl.id);
298
- console.log(`\nTask created in ${tl.title}: ${created.title}`);
299
- if (created.due)
300
- console.log(` Due: ${dueToYMD(created.due)}`);
301
- if (created.notes)
302
- console.log(` Notes: ${created.notes}`);
303
- console.log(` ID: ${(created.id || '').slice(0, 8)}`);
356
+ console.log(`\nCreating ${extracted.length} task(s) in ${tl.title}:\n`);
357
+ for (const e of extracted) {
358
+ if (!e.title) {
359
+ console.error(' AI returned task with no title — skipping');
360
+ continue;
361
+ }
362
+ const task = { title: e.title };
363
+ if (e.due)
364
+ task.due = `${e.due}T00:00:00.000Z`;
365
+ if (e.notes)
366
+ task.notes = e.notes;
367
+ if (parsed.notes && !task.notes)
368
+ task.notes = parsed.notes;
369
+ const created = await createTask(token, task, tl.id);
370
+ console.log(` ${(created.id || '').slice(0, 8)} ${created.title}${created.due ? ` (due ${dueToYMD(created.due)})` : ''}`);
371
+ if (created.notes)
372
+ console.log(` Notes: ${created.notes}`);
373
+ }
304
374
  break;
305
375
  }
306
376
  case 'done': {
@@ -421,7 +491,9 @@ async function main() {
421
491
  }
422
492
  }
423
493
  if (import.meta.main) {
424
- main().catch(e => {
494
+ main()
495
+ .then(() => process.exit(0))
496
+ .catch(e => {
425
497
  console.error(`Error: ${e.message}`);
426
498
  process.exit(1);
427
499
  });