@bobfrankston/gcards 0.1.11 → 0.1.13

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 CHANGED
@@ -242,11 +242,36 @@ After export, run `gcards push -u <target>` to upload.
242
242
 
243
243
  ## Setup
244
244
 
245
+ ### Google API Setup
246
+
245
247
  1. Create Google Cloud project
246
248
  2. Enable People API
247
249
  3. Create OAuth2 credentials (Desktop app type)
248
250
  4. Download `credentials.json` to the gcards directory
249
251
 
252
+ ### AI Error Explanations (Optional)
253
+
254
+ When API errors occur, gcards can use AI to explain what went wrong and suggest fixes.
255
+
256
+ 1. Create `keys.env` in your gcards app directory:
257
+ - **Windows**: `%APPDATA%\gcards\keys.env`
258
+ - **Linux/Mac**: `~/.config/gcards/keys.env`
259
+
260
+ 2. Add your API key(s):
261
+ ```
262
+ ANTHROPIC_API_KEY=sk-ant-api03-...
263
+ ```
264
+ or
265
+ ```
266
+ OPENAI_API_KEY=sk-...
267
+ ```
268
+
269
+ **Get API keys:**
270
+ - Anthropic: https://console.anthropic.com/settings/keys
271
+ - OpenAI: https://platform.openai.com/api-keys
272
+
273
+ The tool tries Anthropic first, then falls back to OpenAI if available.
274
+
250
275
  ## License
251
276
 
252
277
  MIT
package/gcards.ts CHANGED
@@ -14,7 +14,7 @@ import type { GooglePerson, GoogleConnectionsResponse } from './glib/types.ts';
14
14
  import { GCARDS_GUID_KEY, extractGuids } from './glib/gctypes.ts';
15
15
  import type { ContactIndex, IndexEntry, DeletedEntry, DeleteQueue, DeleteQueueEntry, PushStatus, PendingChange, UserPaths } from './glib/gctypes.ts';
16
16
  import { DATA_DIR, CREDENTIALS_FILE, loadConfig, saveConfig, getUserPaths, ensureUserDir, loadIndex, normalizeUser, getAllUsers, resolveUser, FileLogger } from './glib/gutils.ts';
17
- import { explainApiError, isAIAvailable } from './glib/aihelper.ts';
17
+ import { explainApiError, isAIAvailable, showAISetupHint } from './glib/aihelper.ts';
18
18
 
19
19
  const PEOPLE_API_BASE = 'https://people.googleapis.com/v1';
20
20
  const CONTACTS_SCOPE_READ = 'https://www.googleapis.com/auth/contacts.readonly';
@@ -77,6 +77,7 @@ function cleanupEscapeHandler(): void {
77
77
  if (process.stdin.isTTY) {
78
78
  process.stdin.setRawMode(false);
79
79
  process.stdin.pause();
80
+ process.stdin.removeAllListeners('data');
80
81
  }
81
82
  }
82
83
 
@@ -124,11 +125,24 @@ async function refreshAccessToken(): Promise<string> {
124
125
  return currentAccessToken;
125
126
  }
126
127
 
128
+ /** Get sort key from contact: fileAs > displayNameLastFirst > displayName */
129
+ function getSortKey(person: GooglePerson): string {
130
+ const fileAs = person.fileAses?.[0]?.value;
131
+ if (fileAs) return fileAs;
132
+ const name = person.names?.[0];
133
+ if (name?.displayNameLastFirst) return name.displayNameLastFirst;
134
+ return name?.displayName || '';
135
+ }
136
+
127
137
  function saveIndex(paths: UserPaths, index: ContactIndex): void {
128
- // Sort contacts by displayName for convenience
138
+ // Sort contacts by sortKey (fileAs > displayNameLastFirst > displayName)
129
139
  const sortedContacts: Record<string, IndexEntry> = {};
130
140
  const entries = Object.entries(index.contacts);
131
- entries.sort((a, b) => a[1].displayName.localeCompare(b[1].displayName));
141
+ entries.sort((a, b) => {
142
+ const keyA = a[1].sortKey || a[1].displayName || '';
143
+ const keyB = b[1].sortKey || b[1].displayName || '';
144
+ return keyA.localeCompare(keyB);
145
+ });
132
146
  for (const [key, value] of entries) {
133
147
  sortedContacts[key] = value;
134
148
  }
@@ -452,10 +466,12 @@ async function syncContacts(user: string, options: { full: boolean; verbose: boo
452
466
  // Preserve _delete flag from existing entry
453
467
  const existingDelete = index.contacts[person.resourceName]?._delete;
454
468
 
469
+ const sortKey = getSortKey(person);
455
470
  index.contacts[person.resourceName] = {
456
471
  resourceName: person.resourceName,
457
472
  displayName,
458
473
  updatedAt: new Date().toISOString(),
474
+ sortKey,
459
475
  ...(hasPhoto && { hasPhoto }),
460
476
  ...(starred && { starred }),
461
477
  ...(guids.length > 0 && { guids }),
@@ -912,10 +928,12 @@ async function pushContacts(user: string, options: { yes: boolean; verbose: bool
912
928
  const starred = isStarred(created);
913
929
 
914
930
  // Add to index
931
+ const sortKey = getSortKey(created);
915
932
  index.contacts[created.resourceName] = {
916
933
  resourceName: created.resourceName,
917
934
  displayName: change.displayName,
918
935
  updatedAt: new Date().toISOString(),
936
+ sortKey,
919
937
  ...(hasPhoto && { hasPhoto }),
920
938
  ...(starred && { starred }),
921
939
  ...(guids.length > 0 && { guids })
@@ -1036,23 +1054,27 @@ async function pushContacts(user: string, options: { yes: boolean; verbose: bool
1036
1054
  errorCount++;
1037
1055
 
1038
1056
  // Get AI explanation if available and error is from API
1039
- if (error instanceof ApiError && error.payload && isAIAvailable()) {
1040
- try {
1041
- const explanation = await explainApiError(error.message, error.payload);
1042
- if (explanation) {
1043
- console.log(`\n AI Analysis:`);
1044
- if (explanation.explanation) {
1045
- console.log(` Problem: ${explanation.explanation}`);
1046
- }
1047
- if (explanation.suggestedFix) {
1048
- console.log(` Fix: ${explanation.suggestedFix}`);
1049
- }
1050
- if (explanation.correctedJson) {
1051
- console.log(` Corrected JSON:\n${explanation.correctedJson.split('\n').map(l => ' ' + l).join('\n')}`);
1057
+ if (error instanceof ApiError && error.payload) {
1058
+ if (isAIAvailable()) {
1059
+ try {
1060
+ const explanation = await explainApiError(error.message, error.payload);
1061
+ if (explanation) {
1062
+ console.log(`\n AI Analysis:`);
1063
+ if (explanation.explanation) {
1064
+ console.log(` Problem: ${explanation.explanation}`);
1065
+ }
1066
+ if (explanation.suggestedFix) {
1067
+ console.log(` Fix: ${explanation.suggestedFix}`);
1068
+ }
1069
+ if (explanation.correctedJson) {
1070
+ console.log(` Corrected JSON:\n${explanation.correctedJson.split('\n').map(l => ' ' + l).join('\n')}`);
1071
+ }
1072
+ problems.push(` AI: ${explanation.explanation || ''} Fix: ${explanation.suggestedFix || ''}`);
1052
1073
  }
1053
- problems.push(` AI: ${explanation.explanation || ''} Fix: ${explanation.suggestedFix || ''}`);
1054
- }
1055
- } catch { /* ignore AI errors */ }
1074
+ } catch { /* ignore AI errors */ }
1075
+ } else {
1076
+ showAISetupHint();
1077
+ }
1056
1078
  }
1057
1079
  }
1058
1080
  }
package/gfix.ts CHANGED
@@ -1898,12 +1898,12 @@ async function main(): Promise<void> {
1898
1898
  console.error('Usage: gfix export -u <source> -to <target> <pattern>');
1899
1899
  process.exit(1);
1900
1900
  }
1901
- const resolvedSource = resolveUser(user);
1901
+ const resolvedSource = resolveUser(user, true);
1902
1902
  await runExport(resolvedSource, targetUser, positionalArgs[0]);
1903
1903
  return;
1904
1904
  }
1905
1905
 
1906
- const resolvedUser = resolveUser(user);
1906
+ const resolvedUser = resolveUser(user, true);
1907
1907
 
1908
1908
  if (processLimit > 0) {
1909
1909
  console.log(`[DEBUG] Processing limited to first ${processLimit} contacts\n`);
package/glib/aihelper.ts CHANGED
@@ -194,3 +194,22 @@ export async function explainApiError(errorMessage: string, payload: unknown): P
194
194
  export function isAIAvailable(): boolean {
195
195
  return !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
196
196
  }
197
+
198
+ /**
199
+ * Get the expected keys.env path for the current platform
200
+ */
201
+ export function getKeysEnvPath(): string {
202
+ const home = process.env.HOME || process.env.USERPROFILE || '';
203
+ const appData = process.env.APPDATA || path.join(home, '.config');
204
+ return path.join(appData, 'gcards', 'keys.env');
205
+ }
206
+
207
+ /**
208
+ * Show instructions for setting up AI keys (call when AI could have helped but wasn't available)
209
+ */
210
+ export function showAISetupHint(): void {
211
+ const keysPath = getKeysEnvPath();
212
+ console.log(`\n Tip: Enable AI error explanations by creating ${keysPath}`);
213
+ console.log(` Add: ANTHROPIC_API_KEY=sk-ant-... or OPENAI_API_KEY=sk-...`);
214
+ console.log(` See README.md for details.`);
215
+ }
package/glib/gctypes.ts CHANGED
@@ -29,6 +29,7 @@ export interface IndexEntry {
29
29
  resourceName: string;
30
30
  displayName: string;
31
31
  updatedAt: string;
32
+ sortKey?: string; /** Sort key: fileAs, displayNameLastFirst, or displayName */
32
33
  hasPhoto?: boolean; /** Has non-default photo */
33
34
  starred?: boolean; /** In starred/favorites group */
34
35
  _delete?: string; /** Deletion reason. *prefix means skipped (e.g., *photo, *starred) */
package/glib/gutils.ts CHANGED
@@ -125,9 +125,23 @@ export function resolveUser(cliUser: string, setAsDefault = false): string {
125
125
  return config.lastUser;
126
126
  }
127
127
 
128
- // 3. No user - require --user on first use
129
- console.error('No user specified. Use --user <name> on first run (e.g., your Gmail username).');
130
- console.error('This will be remembered for future runs.');
128
+ // 3. Auto-detect if only one user exists
129
+ const allUsers = getAllUsers();
130
+ if (allUsers.length === 1) {
131
+ const onlyUser = allUsers[0];
132
+ console.log(`Auto-selected only user: ${onlyUser}`);
133
+ config.lastUser = onlyUser;
134
+ saveConfig(config);
135
+ return onlyUser;
136
+ }
137
+
138
+ // 4. No user - require --user on first use
139
+ if (allUsers.length > 1) {
140
+ console.error(`Multiple users exist: ${allUsers.join(', ')}`);
141
+ console.error('Use -u <name> to select one.');
142
+ } else {
143
+ console.error('No user specified. Use -u <email> on first run (e.g., your Gmail address).');
144
+ }
131
145
  process.exit(1);
132
146
  }
133
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/gcards",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Google Contacts cleanup and management tool",
5
5
  "type": "module",
6
6
  "main": "gcards.ts",