@bobfrankston/gcards 0.1.34 → 0.1.36

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.
@@ -23,7 +23,18 @@
23
23
  "WebFetch(domain:lh3.googleusercontent.com)",
24
24
  "Bash(curl:*)",
25
25
  "Bash(bun build:*)",
26
- "Bash(xargs -I {} sh -c 'echo \"\"=== {} ===\"\" && head -20 {}')"
26
+ "Bash(xargs -I {} sh -c 'echo \"\"=== {} ===\"\" && head -20 {}')",
27
+ "Bash(del \"y:\\\\dev\\\\utils\\\\cardx\\\\gcards\\\\data\\\\bobfrankston\\\\token.json\" \"y:\\\\dev\\\\utils\\\\cardx\\\\gcards\\\\data\\\\bobfrankston\\\\token-write.json\")",
28
+ "Bash(echo:*)",
29
+ "Bash(npm whoami:*)",
30
+ "Bash(npm run build:*)",
31
+ "Bash(npm publish:*)",
32
+ "Bash(npm version:*)",
33
+ "Bash(npm link)",
34
+ "Bash(npm link:*)",
35
+ "Bash(cmd /c \"dir /AL\")",
36
+ "Bash(node gcards.ts:*)",
37
+ "Bash(npm run release:*)"
27
38
  ],
28
39
  "deny": [],
29
40
  "ask": []
package/README.md CHANGED
@@ -261,12 +261,31 @@ After export, run `gcards push -u <target>` to upload.
261
261
 
262
262
  ## Setup
263
263
 
264
- ### Google API Setup
265
-
266
- 1. Create Google Cloud project
267
- 2. Enable People API
268
- 3. Create OAuth2 credentials (Desktop app type)
269
- 4. Download `credentials.json` to the gcards directory
264
+ ### Google API Credentials (Required)
265
+
266
+ Each user must create their own Google Cloud credentials. This is a one-time setup:
267
+
268
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
269
+ 2. Create a new project (e.g., "gcards")
270
+ 3. Enable the **People API**:
271
+ - Go to APIs & Services > Library
272
+ - Search for "People API" and enable it
273
+ 4. Configure OAuth consent screen:
274
+ - Go to APIs & Services > OAuth consent screen
275
+ - Choose "External" user type
276
+ - Fill in app name and email fields
277
+ - Add scope: `https://www.googleapis.com/auth/contacts`
278
+ 5. Create OAuth credentials:
279
+ - Go to APIs & Services > Credentials
280
+ - Click "+ CREATE CREDENTIALS" > "OAuth client ID"
281
+ - Choose "Web application"
282
+ - Add redirect URI: `http://localhost:9326/oauth2callback`
283
+ - Click Create and download the JSON file
284
+ 6. Save as `credentials.json` in the gcards install directory:
285
+ - Find install location: `npm list -g @bobfrankston/gcards`
286
+ - Or run gcards once to see the expected path in the error message
287
+
288
+ For detailed instructions, see: [SETUP-GOOGLE-OAUTH.md](https://github.com/BobFrankston/oauthsupport/blob/master/SETUP-GOOGLE-OAUTH.md)
270
289
 
271
290
  ### AI Error Explanations (Optional)
272
291
 
package/gcards.ts CHANGED
@@ -10,7 +10,7 @@ import path from 'path';
10
10
  import crypto from 'crypto';
11
11
  import { parse as parseJsonc } from 'jsonc-parser';
12
12
  import { parseArgs, showUsage, showHelp } from './glib/parsecli.ts';
13
- import { authenticateOAuth } from '../../../projects/oauth/oauthsupport/index.ts';
13
+ import { authenticateOAuth } from '@bobfrankston/oauthsupport';
14
14
  import type { GooglePerson, GoogleConnectionsResponse } from './glib/types.ts';
15
15
  import { GCARDS_GUID_KEY, extractGuids } from './glib/gctypes.ts';
16
16
  import type { ContactIndex, IndexEntry, DeletedEntry, DeleteQueue, DeleteQueueEntry, PushStatus, PendingChange, UserPaths } from './glib/gctypes.ts';
@@ -87,7 +87,15 @@ function cleanupEscapeHandler(): void {
87
87
 
88
88
  async function getAccessToken(user: string, writeAccess = false, forceRefresh = false): Promise<string> {
89
89
  if (!fs.existsSync(CREDENTIALS_FILE)) {
90
- throw new Error(`Credentials file not found: ${CREDENTIALS_FILE}`);
90
+ console.error(`\nCredentials file not found: ${CREDENTIALS_FILE}\n`);
91
+ console.error(`To use gcards, you need to create your own Google Cloud credentials:`);
92
+ console.error(` 1. Go to https://console.cloud.google.com/`);
93
+ console.error(` 2. Create a project and enable the People API`);
94
+ console.error(` 3. Create OAuth 2.0 credentials (Web application type)`);
95
+ console.error(` 4. Add redirect URI: http://localhost:9326/oauth2callback`);
96
+ console.error(` 5. Download credentials.json to: ${CREDENTIALS_FILE}\n`);
97
+ console.error(`See: https://github.com/BobFrankston/oauthsupport/blob/master/SETUP-GOOGLE-OAUTH.md`);
98
+ process.exit(1);
91
99
  }
92
100
 
93
101
  const paths = getUserPaths(user);
@@ -108,9 +116,10 @@ async function getAccessToken(user: string, writeAccess = false, forceRefresh =
108
116
  tokenDirectory: paths.userDir,
109
117
  tokenFileName,
110
118
  credentialsKey: 'web',
111
- includeOfflineAccess: true,
112
- prompt: 'select_account', // Always show account picker for new auth
113
- signal: abortController?.signal // Pass abort signal for immediate cancellation
119
+ signal: abortController?.signal
120
+ // includeOfflineAccess defaults to true - auto-requests refresh token
121
+ // prompt auto-detects when consent is needed for refresh token
122
+ // maxTokenLifetimeHours defaults to unlimited (uses refresh token indefinitely)
114
123
  });
115
124
 
116
125
  if (!token) {
@@ -419,7 +428,7 @@ async function fetchContactsWithRetry(
419
428
  const BASE_DELAY = 5000; /** 5 seconds base delay */
420
429
 
421
430
  const params = new URLSearchParams({
422
- personFields: 'names,emailAddresses,phoneNumbers,addresses,organizations,biographies,urls,birthdays,photos,memberships,metadata,fileAses',
431
+ personFields: 'names,emailAddresses,phoneNumbers,addresses,organizations,biographies,urls,birthdays,photos,memberships,metadata,fileAses,userDefined',
423
432
  pageSize: '100' /** Smaller page size to avoid quota issues */
424
433
  });
425
434
 
@@ -923,7 +932,7 @@ async function confirm(message: string): Promise<boolean> {
923
932
  }
924
933
 
925
934
  async function updateContactOnGoogle(person: GooglePerson, retryCount = 0, tokenRefreshed = false): Promise<void> {
926
- const updateMask = 'names,emailAddresses,phoneNumbers,addresses,organizations,biographies,urls,birthdays,fileAses';
935
+ const updateMask = 'names,emailAddresses,phoneNumbers,addresses,organizations,biographies,urls,birthdays,fileAses,userDefined';
927
936
 
928
937
  const response = await fetch(`${PEOPLE_API_BASE}/${person.resourceName}:updateContact?updatePersonFields=${updateMask}`, {
929
938
  method: 'PATCH',
package/glib/gmerge.ts CHANGED
@@ -149,15 +149,36 @@ function unionMemberships(target: GoogleMembership[], source: GoogleMembership[]
149
149
  }
150
150
 
151
151
  function unionUserDefined(target: GoogleUserDefined[], source: GoogleUserDefined[]): GoogleUserDefined[] {
152
- const seen = new Set<string>();
153
- const result: GoogleUserDefined[] = [];
152
+ const byKey = new Map<string, GoogleUserDefined[]>();
153
+
154
+ // Collect all values for each key
154
155
  for (const u of [...target, ...source]) {
155
- const key = `${u.key}:${u.value}`;
156
- if (!seen.has(key)) {
157
- seen.add(key);
158
- result.push(u);
156
+ if (!u.key) continue;
157
+ const existing = byKey.get(u.key) || [];
158
+ // Only add if this exact key:value pair isn't already present
159
+ if (!existing.some(e => e.value === u.value)) {
160
+ existing.push(u);
161
+ }
162
+ byKey.set(u.key, existing);
163
+ }
164
+
165
+ const result: GoogleUserDefined[] = [];
166
+
167
+ // For each key, handle based on what makes sense
168
+ for (const [key, values] of byKey.entries()) {
169
+ if (values.length === 1) {
170
+ // Single value - just add it
171
+ result.push(values[0]);
172
+ } else {
173
+ // Multiple values for same key
174
+ // For most keys, prefer target (first occurrence)
175
+ // But preserve all unique values by keeping them separate
176
+ for (const val of values) {
177
+ result.push(val);
178
+ }
159
179
  }
160
180
  }
181
+
161
182
  return result;
162
183
  }
163
184
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/gcards",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Google Contacts cleanup and management tool",
5
5
  "type": "module",
6
6
  "main": "gcards.ts",
@@ -33,7 +33,8 @@
33
33
  "tsx": "^4.21.0"
34
34
  },
35
35
  "dependencies": {
36
- "@bobfrankston/miscassists": "file:../../../projects/NodeJS/miscassists",
36
+ "@bobfrankston/miscassists": "^1.0.36",
37
+ "@bobfrankston/oauthsupport": "^1.0.1",
37
38
  "jsonc-parser": "^3.3.1"
38
39
  }
39
40
  }