gworkspace 0.2.1

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.
@@ -0,0 +1,80 @@
1
+ import { google } from 'googleapis';
2
+ import { optNumber, optString } from '../lib/args.js';
3
+ import { fail, output } from '../lib/io.js';
4
+ import type { AnyRecord, Options } from '../lib/types.js';
5
+
6
+ function formatGmailDate(d: Date): string {
7
+ const year = d.getFullYear();
8
+ const month = `${d.getMonth() + 1}`.padStart(2, '0');
9
+ const day = `${d.getDate()}`.padStart(2, '0');
10
+ return `${year}/${month}/${day}`;
11
+ }
12
+
13
+ function getTodayGmailQuery(): string {
14
+ const start = new Date();
15
+ start.setHours(0, 0, 0, 0);
16
+ const end = new Date(start);
17
+ end.setDate(end.getDate() + 1);
18
+ return `after:${formatGmailDate(start)} before:${formatGmailDate(end)}`;
19
+ }
20
+
21
+ export async function commandGmailSearch(auth: any, options: Options) {
22
+ const gmail = google.gmail({ version: 'v1', auth });
23
+ const max = Math.max(1, Math.min(optNumber(options, 'max', 20), 100));
24
+ const baseQuery = optString(options, 'query') || '';
25
+ const useToday = options.today === true;
26
+ const query = useToday ? `${baseQuery} ${getTodayGmailQuery()}`.trim() : baseQuery;
27
+
28
+ const res = await gmail.users.messages.list({
29
+ userId: 'me',
30
+ q: query,
31
+ maxResults: max,
32
+ pageToken: optString(options, 'pageToken'),
33
+ });
34
+
35
+ const messages = res.data.messages || [];
36
+ output({
37
+ ok: true,
38
+ action: 'gmail.search',
39
+ today: useToday,
40
+ query,
41
+ count: messages.length,
42
+ nextPageToken: res.data.nextPageToken || null,
43
+ messages: messages.map((m: AnyRecord) => ({ id: m.id, threadId: m.threadId })),
44
+ });
45
+ }
46
+
47
+ export async function commandGmailGet(auth: any, options: Options) {
48
+ const id = optString(options, 'id');
49
+ if (!id) fail('Missing required --id for gmail get.');
50
+
51
+ const gmail = google.gmail({ version: 'v1', auth });
52
+ const res = await gmail.users.messages.get({
53
+ userId: 'me',
54
+ id,
55
+ format: 'metadata',
56
+ metadataHeaders: ['From', 'To', 'Subject', 'Date'],
57
+ });
58
+
59
+ const headers = res.data.payload?.headers || [];
60
+ const map = headers.reduce<Record<string, string>>((acc, h: AnyRecord) => {
61
+ if (h.name && h.value) acc[h.name.toLowerCase()] = h.value;
62
+ return acc;
63
+ }, {});
64
+
65
+ output({
66
+ ok: true,
67
+ action: 'gmail.get',
68
+ message: {
69
+ id: res.data.id,
70
+ threadId: res.data.threadId,
71
+ labelIds: res.data.labelIds || [],
72
+ snippet: res.data.snippet || '',
73
+ subject: map.subject || '',
74
+ from: map.from || '',
75
+ to: map.to || '',
76
+ date: map.date || '',
77
+ internalDate: res.data.internalDate || null,
78
+ },
79
+ });
80
+ }
@@ -0,0 +1,34 @@
1
+ import { output } from '../lib/io.js';
2
+
3
+ export function commandTimeNow() {
4
+ const now = new Date();
5
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
6
+ output({
7
+ ok: true,
8
+ action: 'time.now',
9
+ utc: now.toISOString(),
10
+ localDate: now.toLocaleDateString('en-CA', { timeZone }),
11
+ localTime: now.toLocaleTimeString('en-GB', { hour12: false, timeZone }),
12
+ timeZone,
13
+ });
14
+ }
15
+
16
+ export function commandTimeDate() {
17
+ const now = new Date();
18
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
19
+ output({
20
+ ok: true,
21
+ action: 'time.date',
22
+ utc: now.toISOString().slice(0, 10),
23
+ local: now.toLocaleDateString('en-CA', { timeZone }),
24
+ timeZone,
25
+ });
26
+ }
27
+
28
+ export function commandTimeZone() {
29
+ output({
30
+ ok: true,
31
+ action: 'time.zone',
32
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
33
+ });
34
+ }
package/src/config.ts ADDED
@@ -0,0 +1,32 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import type { Options } from './lib/types.js';
5
+ import { optString } from './lib/args.js';
6
+
7
+ export const SCOPES = [
8
+ 'https://www.googleapis.com/auth/calendar.readonly',
9
+ 'https://www.googleapis.com/auth/gmail.readonly',
10
+ 'https://www.googleapis.com/auth/drive.readonly',
11
+ 'https://www.googleapis.com/auth/chat.spaces.readonly',
12
+ 'https://www.googleapis.com/auth/chat.messages.readonly',
13
+ ];
14
+
15
+ export const CONFIG_DIR =
16
+ process.env.GWORKSPACE_CONFIG_DIR ||
17
+ path.join(os.homedir(), '.config', 'gworkspace');
18
+
19
+ export const TOKEN_PATH = path.join(CONFIG_DIR, 'token.json');
20
+
21
+ export const CREDENTIALS_PATH =
22
+ process.env.GOOGLE_OAUTH_CREDENTIALS ||
23
+ path.join(CONFIG_DIR, 'credentials.json');
24
+
25
+ export function ensureConfigDir() {
26
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
27
+ }
28
+
29
+ export function resolveCredentialsPath(options: Options): string {
30
+ const fromOption = optString(options, 'credentials');
31
+ return fromOption ? path.resolve(fromOption) : CREDENTIALS_PATH;
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from './cli.js';
4
+ import { fail } from './lib/io.js';
5
+
6
+ runCli(process.argv.slice(2)).catch((error: unknown) => {
7
+ const msg = error instanceof Error ? error.message : String(error);
8
+ fail('Command failed.', msg);
9
+ });
@@ -0,0 +1,73 @@
1
+ import type { Options, Primitive } from './types.js';
2
+
3
+ export function parseArgs(argv: string[]) {
4
+ const positional: string[] = [];
5
+ const options: Options = {};
6
+
7
+ const setOpt = (key: string, value: Primitive) => {
8
+ const current = options[key];
9
+ if (current === undefined) {
10
+ options[key] = value;
11
+ return;
12
+ }
13
+ if (Array.isArray(current)) {
14
+ options[key] = [...current, value];
15
+ return;
16
+ }
17
+ options[key] = [current, value];
18
+ };
19
+
20
+ for (let i = 0; i < argv.length; i += 1) {
21
+ const token = argv[i];
22
+ if (!token) continue;
23
+
24
+ if (token === '--') {
25
+ positional.push(...argv.slice(i + 1));
26
+ break;
27
+ }
28
+
29
+ if (token === '-h') {
30
+ setOpt('help', true);
31
+ setOpt('h', true);
32
+ continue;
33
+ }
34
+
35
+ if (!token.startsWith('--')) {
36
+ positional.push(token);
37
+ continue;
38
+ }
39
+
40
+ const eq = token.indexOf('=');
41
+ if (eq > 2) {
42
+ setOpt(token.slice(2, eq), token.slice(eq + 1));
43
+ continue;
44
+ }
45
+
46
+ const key = token.slice(2);
47
+ const next = argv[i + 1];
48
+ if (!next || next.startsWith('--')) {
49
+ setOpt(key, true);
50
+ continue;
51
+ }
52
+
53
+ setOpt(key, next);
54
+ i += 1;
55
+ }
56
+
57
+ return { positional, options };
58
+ }
59
+
60
+ export function optString(options: Options, key: string): string | undefined {
61
+ const value = options[key];
62
+ if (value === undefined || typeof value === 'boolean') return undefined;
63
+ if (Array.isArray(value)) return String(value[value.length - 1]);
64
+ return String(value);
65
+ }
66
+
67
+ export function optNumber(options: Options, key: string, fallback: number): number {
68
+ const raw = optString(options, key);
69
+ if (!raw) return fallback;
70
+ const parsed = Number.parseInt(raw, 10);
71
+ if (Number.isNaN(parsed)) return fallback;
72
+ return parsed;
73
+ }
package/src/lib/io.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { JsonObject } from './types.js';
2
+
3
+ export function output(payload: JsonObject, code = 0): never {
4
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
5
+ process.exit(code);
6
+ }
7
+
8
+ export function fail(message: string, details?: unknown): never {
9
+ output(
10
+ {
11
+ ok: false,
12
+ error: message,
13
+ details: details ?? null,
14
+ },
15
+ 1,
16
+ );
17
+ }
@@ -0,0 +1,5 @@
1
+ export type Primitive = string | boolean;
2
+ export type OptValue = Primitive | Primitive[];
3
+ export type Options = Record<string, OptValue>;
4
+ export type JsonObject = Record<string, unknown>;
5
+ export type AnyRecord = Record<string, any>;
package/src/usage.ts ADDED
@@ -0,0 +1,104 @@
1
+ import { CREDENTIALS_PATH, TOKEN_PATH } from './config.js';
2
+
3
+ type Domain = {
4
+ summary: string;
5
+ commands: string[];
6
+ };
7
+
8
+ const HELP: Record<string, Domain> = {
9
+ auth: {
10
+ summary: 'Authenticate gw with Google Workspace',
11
+ commands: [
12
+ 'gw auth login [--credentials path/to/credentials.json] [--no-open]',
13
+ 'gw auth status',
14
+ 'gw auth logout',
15
+ ],
16
+ },
17
+ calendar: {
18
+ summary: 'Read calendar events',
19
+ commands: [
20
+ 'gw calendar list --from <ISO> --to <ISO> [--calendarId primary] [--max 20]',
21
+ 'gw calendar list today [--calendarId primary] [--max 20]',
22
+ 'gw calendar_getEvents --timeMin <ISO> --timeMax <ISO> [--calendarId primary]',
23
+ ],
24
+ },
25
+ gmail: {
26
+ summary: 'Search and read Gmail messages',
27
+ commands: [
28
+ 'gw gmail search --query "newer_than:7d" [--max 20]',
29
+ 'gw gmail list today [--max 20]',
30
+ 'gw gmail get --id <messageId>',
31
+ 'gw gmail_search --query "..." --max 20',
32
+ ],
33
+ },
34
+ drive: {
35
+ summary: 'Search and inspect Drive files',
36
+ commands: [
37
+ 'gw drive search --query "trashed = false" [--max 20]',
38
+ 'gw drive recent [--max 20]',
39
+ 'gw drive get --id <fileId>',
40
+ 'gw drive_search --query "..." --max 20',
41
+ ],
42
+ },
43
+ chat: {
44
+ summary: 'Read Google Chat spaces and messages',
45
+ commands: [
46
+ 'gw chat spaces [--max 20] [--filter "spaceType = SPACE"]',
47
+ 'gw chat messages --space spaces/<spaceId> [--max 20]',
48
+ 'gw chat list today --space spaces/<spaceId> [--max 20]',
49
+ ],
50
+ },
51
+ time: {
52
+ summary: 'Print local time context',
53
+ commands: ['gw time now', 'gw time date', 'gw time zone'],
54
+ },
55
+ };
56
+
57
+ function printText(text: string): never {
58
+ process.stdout.write(`${text}\n`);
59
+ process.exit(0);
60
+ }
61
+
62
+ export function usage(topic?: string): never {
63
+ const normalized = topic?.trim().toLowerCase();
64
+ const selected = normalized ? HELP[normalized] : undefined;
65
+ if (selected) {
66
+ const topicName = normalized as string;
67
+ const commands = selected.commands.map((line) => ` ${line}`).join('\n');
68
+ printText(
69
+ [
70
+ `${topicName.toUpperCase()} COMMANDS`,
71
+ commands,
72
+ '',
73
+ 'LEARN MORE',
74
+ ` Use \`gw ${topicName} --help\` for command usage.`,
75
+ ].join('\n'),
76
+ );
77
+ }
78
+
79
+ const core = Object.entries(HELP)
80
+ .map(([name, def]) => ` ${name.padEnd(14)}${def.summary}`)
81
+ .join('\n');
82
+
83
+ printText(
84
+ [
85
+ 'Work seamlessly with Google Workspace from the command line.',
86
+ '',
87
+ 'USAGE',
88
+ ' gw <command> <subcommand> [flags]',
89
+ '',
90
+ 'CORE COMMANDS',
91
+ core,
92
+ '',
93
+ 'FLAGS',
94
+ ' --help Show help for command',
95
+ '',
96
+ 'CONFIG FILES',
97
+ ` credentials: ${CREDENTIALS_PATH}`,
98
+ ` token: ${TOKEN_PATH}`,
99
+ '',
100
+ 'LEARN MORE',
101
+ ' Use `gw <command> --help` for more information about a command.',
102
+ ].join('\n'),
103
+ );
104
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "types": ["node"],
9
+ "rootDir": "src",
10
+ "outDir": "dist"
11
+ },
12
+ "include": ["src/**/*"]
13
+ }