@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.
- package/.claude/settings.local.json +12 -1
- package/README.md +25 -6
- package/gcards.ts +16 -7
- package/glib/gmerge.ts +27 -6
- package/package.json +3 -2
|
@@ -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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
153
|
-
|
|
152
|
+
const byKey = new Map<string, GoogleUserDefined[]>();
|
|
153
|
+
|
|
154
|
+
// Collect all values for each key
|
|
154
155
|
for (const u of [...target, ...source]) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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.
|
|
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": "
|
|
36
|
+
"@bobfrankston/miscassists": "^1.0.36",
|
|
37
|
+
"@bobfrankston/oauthsupport": "^1.0.1",
|
|
37
38
|
"jsonc-parser": "^3.3.1"
|
|
38
39
|
}
|
|
39
40
|
}
|