crewly 1.5.21 → 1.6.0
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/config/roles/orchestrator/prompt.md +182 -25
- package/config/skills/agent/core/cancel-followup/SKILL.md +38 -0
- package/config/skills/agent/core/cancel-followup/execute.sh +111 -0
- package/config/skills/agent/core/cancel-followup/execute.test.sh +42 -0
- package/config/skills/agent/core/list-my-followups/SKILL.md +36 -0
- package/config/skills/agent/core/list-my-followups/execute.sh +93 -0
- package/config/skills/agent/core/list-my-followups/execute.test.sh +41 -0
- package/config/skills/agent/core/schedule-followup/SKILL.md +53 -0
- package/config/skills/agent/core/schedule-followup/execute.sh +195 -0
- package/config/skills/agent/core/schedule-followup/execute.test.sh +48 -0
- package/config/skills/agent/core/watch-for-event/SKILL.md +60 -0
- package/config/skills/agent/core/watch-for-event/execute.sh +177 -0
- package/config/skills/agent/core/watch-for-event/execute.test.sh +43 -0
- package/config/skills/orchestrator/credential-manager/SKILL.md +218 -0
- package/config/skills/orchestrator/credential-manager/execute.sh +166 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.controller.d.ts +80 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.controller.js +365 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.routes.d.ts +26 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.routes.js +40 -0
- package/dist/backend/backend/src/controllers/credentials/credentials.routes.js.map +1 -0
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +23 -14
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
- package/dist/backend/backend/src/scripts/backfill-mission-priority.d.ts +3 -1
- package/dist/backend/backend/src/scripts/backfill-mission-priority.d.ts.map +1 -1
- package/dist/backend/backend/src/scripts/backfill-mission-priority.js +16 -4
- package/dist/backend/backend/src/scripts/backfill-mission-priority.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js +22 -2
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
- package/dist/backend/backend/src/services/credential/credential-store.service.d.ts +161 -0
- package/dist/backend/backend/src/services/credential/credential-store.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/credential/credential-store.service.js +298 -0
- package/dist/backend/backend/src/services/credential/credential-store.service.js.map +1 -0
- package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts +117 -0
- package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -0
- package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +293 -0
- package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -0
- package/dist/backend/backend/src/services/project/task.service.d.ts +18 -2
- package/dist/backend/backend/src/services/project/task.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project/task.service.js +69 -53
- package/dist/backend/backend/src/services/project/task.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/contract-matcher.d.ts +20 -0
- package/dist/backend/backend/src/services/v3/contract-matcher.d.ts.map +1 -0
- package/dist/backend/backend/src/services/v3/contract-matcher.js +33 -0
- package/dist/backend/backend/src/services/v3/contract-matcher.js.map +1 -0
- package/dist/backend/backend/src/services/v3/escalation.service.d.ts +20 -1
- package/dist/backend/backend/src/services/v3/escalation.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation.service.js +97 -28
- package/dist/backend/backend/src/services/v3/escalation.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/service-contract-gate.service.d.ts +6 -4
- package/dist/backend/backend/src/services/v3/service-contract-gate.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/service-contract-gate.service.js +18 -28
- package/dist/backend/backend/src/services/v3/service-contract-gate.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js +14 -9
- package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts +34 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.js +115 -5
- package/dist/backend/backend/src/services/v3/trigger-engine.service.js.map +1 -1
- package/dist/backend/backend/src/types/credential.types.d.ts +185 -0
- package/dist/backend/backend/src/types/credential.types.d.ts.map +1 -0
- package/dist/backend/backend/src/types/credential.types.js +76 -0
- package/dist/backend/backend/src/types/credential.types.js.map +1 -0
- package/dist/backend/backend/src/utils/encryption.utils.d.ts +57 -0
- package/dist/backend/backend/src/utils/encryption.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/encryption.utils.js +162 -0
- package/dist/backend/backend/src/utils/encryption.utils.js.map +1 -0
- package/dist/cli/backend/src/services/credential/credential-store.service.d.ts +161 -0
- package/dist/cli/backend/src/services/credential/credential-store.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/credential/credential-store.service.js +298 -0
- package/dist/cli/backend/src/services/credential/credential-store.service.js.map +1 -0
- package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts +117 -0
- package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -0
- package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +293 -0
- package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -0
- package/dist/cli/backend/src/services/settings/settings.service.d.ts +168 -0
- package/dist/cli/backend/src/services/settings/settings.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/settings/settings.service.js +312 -0
- package/dist/cli/backend/src/services/settings/settings.service.js.map +1 -0
- package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts +159 -0
- package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill/skill-executor.service.js +626 -0
- package/dist/cli/backend/src/services/skill/skill-executor.service.js.map +1 -0
- package/dist/cli/backend/src/services/skill/skill.service.d.ts +273 -0
- package/dist/cli/backend/src/services/skill/skill.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/skill/skill.service.js +655 -0
- package/dist/cli/backend/src/services/skill/skill.service.js.map +1 -0
- package/dist/cli/backend/src/types/credential.types.d.ts +185 -0
- package/dist/cli/backend/src/types/credential.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/credential.types.js +76 -0
- package/dist/cli/backend/src/types/credential.types.js.map +1 -0
- package/dist/cli/backend/src/utils/encryption.utils.d.ts +57 -0
- package/dist/cli/backend/src/utils/encryption.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/encryption.utils.js +162 -0
- package/dist/cli/backend/src/utils/encryption.utils.js.map +1 -0
- package/dist/cli/backend/src/utils/skill-md-parser.d.ts +38 -0
- package/dist/cli/backend/src/utils/skill-md-parser.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/skill-md-parser.js +47 -0
- package/dist/cli/backend/src/utils/skill-md-parser.js.map +1 -0
- package/frontend/dist/assets/{index-dc92ab64.css → index-6aaa0630.css} +1 -1
- package/frontend/dist/assets/{index-76d76633.js → index-9e6d97d1.js} +334 -328
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/config/experts/empathetic-resolver/expert.json +0 -11
- package/config/experts/empathetic-resolver.md +0 -32
- package/config/experts/pragmatic-architect/expert.json +0 -11
- package/config/experts/pragmatic-architect.md +0 -32
- package/config/experts/viral-alchemist/expert.json +0 -11
- package/config/experts/viral-alchemist.md +0 -32
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Workspace Credential Helper
|
|
3
|
+
*
|
|
4
|
+
* Piggybacks on the Google Workspace extension for Gemini CLI
|
|
5
|
+
* (https://github.com/gemini-cli-extensions/workspace) for Google OAuth
|
|
6
|
+
* credential acquisition and refresh.
|
|
7
|
+
*
|
|
8
|
+
* **Capture** reads the extension's file-storage token file (written when
|
|
9
|
+
* the user runs the extension with `GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE=true`)
|
|
10
|
+
* and imports the tokens into Crewly's own encrypted store.
|
|
11
|
+
*
|
|
12
|
+
* **Refresh** POSTs the stored `refresh_token` to the extension's Cloud
|
|
13
|
+
* Function `/refreshToken` endpoint — no client_secret needed on our side.
|
|
14
|
+
*
|
|
15
|
+
* After capture, Crewly owns the tokens; the extension's own state is not
|
|
16
|
+
* depended on for day-to-day operation.
|
|
17
|
+
*
|
|
18
|
+
* @module services/credential/helpers/gemini-cli-workspace.helper
|
|
19
|
+
*/
|
|
20
|
+
import { CredentialHelper, CredentialHelperName, CredentialRegistryEntry, GoogleOAuthPayload } from '../../../types/credential.types.js';
|
|
21
|
+
import { CredentialStoreService } from '../credential-store.service.js';
|
|
22
|
+
/**
|
|
23
|
+
* Minimal fetch-compatible function shape (for test injection).
|
|
24
|
+
*/
|
|
25
|
+
export type FetchLike = (url: string, init?: {
|
|
26
|
+
method?: string;
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
body?: string;
|
|
29
|
+
}) => Promise<{
|
|
30
|
+
ok: boolean;
|
|
31
|
+
status: number;
|
|
32
|
+
text(): Promise<string>;
|
|
33
|
+
json(): Promise<unknown>;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Helper configuration — all fields optional; defaults match production.
|
|
37
|
+
*/
|
|
38
|
+
export interface GeminiCliHelperConfig {
|
|
39
|
+
/** Path to the extension install directory (default: `~/.gemini/extensions/google-workspace`). */
|
|
40
|
+
extensionPath?: string;
|
|
41
|
+
/** Cloud Function base URL. */
|
|
42
|
+
cloudFunctionUrl?: string;
|
|
43
|
+
/** Path on the cloud function for refresh calls. */
|
|
44
|
+
refreshPath?: string;
|
|
45
|
+
/** Client ID to store on the credential (for later refresh identification). */
|
|
46
|
+
clientId?: string;
|
|
47
|
+
/** Refresh buffer in ms — refresh if token expires within this window. */
|
|
48
|
+
expiryBufferMs?: number;
|
|
49
|
+
/** Fetch implementation (injectable for tests). */
|
|
50
|
+
fetch?: FetchLike;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Credential helper that reads tokens from the gemini-cli-workspace extension
|
|
54
|
+
* and refreshes them via the extension's Cloud Function.
|
|
55
|
+
*/
|
|
56
|
+
export declare class GeminiCliWorkspaceHelper implements CredentialHelper {
|
|
57
|
+
readonly name: CredentialHelperName;
|
|
58
|
+
private readonly extensionPath;
|
|
59
|
+
private readonly cloudFunctionUrl;
|
|
60
|
+
private readonly refreshPath;
|
|
61
|
+
private readonly clientId;
|
|
62
|
+
private readonly expiryBufferMs;
|
|
63
|
+
private readonly fetchFn;
|
|
64
|
+
private readonly store;
|
|
65
|
+
/** Per-credential refresh in-flight promises (serializes concurrent refreshes). */
|
|
66
|
+
private readonly refreshInFlight;
|
|
67
|
+
/** Per-credential last refresh attempt timestamp (for cooldown). */
|
|
68
|
+
private readonly lastRefreshAttempt;
|
|
69
|
+
constructor(store: CredentialStoreService, config?: GeminiCliHelperConfig);
|
|
70
|
+
/**
|
|
71
|
+
* Read the extension's current token file and return its contents as a
|
|
72
|
+
* `GoogleOAuthPayload` ready to persist in Crewly's store.
|
|
73
|
+
*
|
|
74
|
+
* The user must have completed extension login with
|
|
75
|
+
* `GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE=true` before calling this.
|
|
76
|
+
*/
|
|
77
|
+
captureFromFile(): Promise<GoogleOAuthPayload>;
|
|
78
|
+
/**
|
|
79
|
+
* Remove the extension's token file. Call after `captureFromFile()` to
|
|
80
|
+
* prepare for the next account's login (the extension otherwise returns
|
|
81
|
+
* cached credentials if scopes match, skipping the login prompt).
|
|
82
|
+
* Leaves the master.key file untouched so existing ciphertexts remain
|
|
83
|
+
* decryptable if needed.
|
|
84
|
+
*/
|
|
85
|
+
clearExtensionFile(): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Return a valid `GoogleOAuthPayload`, refreshing via the extension's
|
|
88
|
+
* cloud function if the access token is within the expiry buffer.
|
|
89
|
+
*
|
|
90
|
+
* Concurrent calls for the same credential share a single refresh.
|
|
91
|
+
*
|
|
92
|
+
* @throws CredentialRevokedError if the refresh token is no longer valid
|
|
93
|
+
*/
|
|
94
|
+
getAccessToken(entry: CredentialRegistryEntry, payload: GoogleOAuthPayload): Promise<GoogleOAuthPayload>;
|
|
95
|
+
private doRefresh;
|
|
96
|
+
/**
|
|
97
|
+
* Decrypt the extension's `{iv_hex}:{authTag_hex}:{ciphertext_hex}`
|
|
98
|
+
* format (AES-256-GCM).
|
|
99
|
+
*/
|
|
100
|
+
private decryptExtensionFormat;
|
|
101
|
+
/**
|
|
102
|
+
* Fetch the user's email via Google's userinfo endpoint. Best-effort;
|
|
103
|
+
* returns undefined on failure.
|
|
104
|
+
*/
|
|
105
|
+
private fetchUserEmail;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Encrypt a plaintext string with the extension's file format. Used in
|
|
109
|
+
* tests to generate fake token files.
|
|
110
|
+
*/
|
|
111
|
+
export declare function encryptExtensionFormatForTesting(plaintext: string, key: Buffer): string;
|
|
112
|
+
/**
|
|
113
|
+
* Derive the extension's encryption key using the real scrypt + salt.
|
|
114
|
+
* Test-only export.
|
|
115
|
+
*/
|
|
116
|
+
export declare function deriveExtensionKeyForTesting(masterKey: Buffer): Buffer;
|
|
117
|
+
//# sourceMappingURL=gemini-cli-workspace.helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini-cli-workspace.helper.d.ts","sourceRoot":"","sources":["../../../../../../../backend/src/services/credential/helpers/gemini-cli-workspace.helper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAOH,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,EACvB,kBAAkB,EAEnB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAmDxE;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,KACxE,OAAO,CAAC;IACX,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,kGAAkG;IAClG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+BAA+B;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAMD;;;GAGG;AACH,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAA0B;IAE7D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAY;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAE/C,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAG5B;IACJ,oEAAoE;IACpE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;gBAG9D,KAAK,EAAE,sBAAsB,EAC7B,MAAM,CAAC,EAAE,qBAAqB;IAkBhC;;;;;;OAMG;IACG,eAAe,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAoEpD;;;;;;OAMG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAczC;;;;;;;OAOG;IACG,cAAc,CAClB,KAAK,EAAE,uBAAuB,EAC9B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,kBAAkB,CAAC;YA4BhB,SAAS;IAuEvB;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAoB9B;;;OAGG;YACW,cAAc;CAe7B;AAMD;;;GAGG;AACH,wBAAgB,gCAAgC,CAC9C,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,MAAM,CAOR;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGtE"}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Workspace Credential Helper
|
|
3
|
+
*
|
|
4
|
+
* Piggybacks on the Google Workspace extension for Gemini CLI
|
|
5
|
+
* (https://github.com/gemini-cli-extensions/workspace) for Google OAuth
|
|
6
|
+
* credential acquisition and refresh.
|
|
7
|
+
*
|
|
8
|
+
* **Capture** reads the extension's file-storage token file (written when
|
|
9
|
+
* the user runs the extension with `GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE=true`)
|
|
10
|
+
* and imports the tokens into Crewly's own encrypted store.
|
|
11
|
+
*
|
|
12
|
+
* **Refresh** POSTs the stored `refresh_token` to the extension's Cloud
|
|
13
|
+
* Function `/refreshToken` endpoint — no client_secret needed on our side.
|
|
14
|
+
*
|
|
15
|
+
* After capture, Crewly owns the tokens; the extension's own state is not
|
|
16
|
+
* depended on for day-to-day operation.
|
|
17
|
+
*
|
|
18
|
+
* @module services/credential/helpers/gemini-cli-workspace.helper
|
|
19
|
+
*/
|
|
20
|
+
import { promises as fs } from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import os from 'os';
|
|
23
|
+
import crypto from 'crypto';
|
|
24
|
+
import { CredentialRevokedError, } from '../../../types/credential.types.js';
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Constants — extracted from the extension source
|
|
27
|
+
// (github.com/gemini-cli-extensions/workspace)
|
|
28
|
+
// ============================================================================
|
|
29
|
+
const DEFAULT_EXTENSION_PATH = path.join(os.homedir(), '.gemini', 'extensions', 'google-workspace');
|
|
30
|
+
const TOKEN_FILENAME = 'gemini-cli-workspace-token.json';
|
|
31
|
+
const MASTER_KEY_FILENAME = '.gemini-cli-workspace-master-key';
|
|
32
|
+
const MAIN_ACCOUNT_KEY = 'main-account';
|
|
33
|
+
const SALT_SUFFIX = '-gemini-cli-workspace';
|
|
34
|
+
const DEFAULT_CLOUD_FUNCTION_URL = 'https://google-workspace-extension.geminicli.com';
|
|
35
|
+
const DEFAULT_REFRESH_PATH = '/refreshToken';
|
|
36
|
+
const DEFAULT_CLIENT_ID = '338689075775-o75k922vn5fdl18qergr96rp8g63e4d7.apps.googleusercontent.com';
|
|
37
|
+
/** Refresh if the current access token expires within this window. */
|
|
38
|
+
const DEFAULT_EXPIRY_BUFFER_MS = 60_000;
|
|
39
|
+
/** Minimum gap between refresh attempts for the same credential. */
|
|
40
|
+
const REFRESH_COOLDOWN_MS = 30_000;
|
|
41
|
+
const USERINFO_ENDPOINT = 'https://www.googleapis.com/oauth2/v3/userinfo';
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Helper
|
|
44
|
+
// ============================================================================
|
|
45
|
+
/**
|
|
46
|
+
* Credential helper that reads tokens from the gemini-cli-workspace extension
|
|
47
|
+
* and refreshes them via the extension's Cloud Function.
|
|
48
|
+
*/
|
|
49
|
+
export class GeminiCliWorkspaceHelper {
|
|
50
|
+
name = 'gemini-cli-workspace';
|
|
51
|
+
extensionPath;
|
|
52
|
+
cloudFunctionUrl;
|
|
53
|
+
refreshPath;
|
|
54
|
+
clientId;
|
|
55
|
+
expiryBufferMs;
|
|
56
|
+
fetchFn;
|
|
57
|
+
store;
|
|
58
|
+
/** Per-credential refresh in-flight promises (serializes concurrent refreshes). */
|
|
59
|
+
refreshInFlight = new Map();
|
|
60
|
+
/** Per-credential last refresh attempt timestamp (for cooldown). */
|
|
61
|
+
lastRefreshAttempt = new Map();
|
|
62
|
+
constructor(store, config) {
|
|
63
|
+
this.store = store;
|
|
64
|
+
this.extensionPath = config?.extensionPath ?? DEFAULT_EXTENSION_PATH;
|
|
65
|
+
this.cloudFunctionUrl =
|
|
66
|
+
config?.cloudFunctionUrl ?? DEFAULT_CLOUD_FUNCTION_URL;
|
|
67
|
+
this.refreshPath = config?.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
68
|
+
this.clientId = config?.clientId ?? DEFAULT_CLIENT_ID;
|
|
69
|
+
this.expiryBufferMs = config?.expiryBufferMs ?? DEFAULT_EXPIRY_BUFFER_MS;
|
|
70
|
+
this.fetchFn =
|
|
71
|
+
config?.fetch ??
|
|
72
|
+
((url, init) => fetch(url, init));
|
|
73
|
+
}
|
|
74
|
+
// ------------------------------------------------------------------
|
|
75
|
+
// Capture
|
|
76
|
+
// ------------------------------------------------------------------
|
|
77
|
+
/**
|
|
78
|
+
* Read the extension's current token file and return its contents as a
|
|
79
|
+
* `GoogleOAuthPayload` ready to persist in Crewly's store.
|
|
80
|
+
*
|
|
81
|
+
* The user must have completed extension login with
|
|
82
|
+
* `GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE=true` before calling this.
|
|
83
|
+
*/
|
|
84
|
+
async captureFromFile() {
|
|
85
|
+
const tokenPath = path.join(this.extensionPath, TOKEN_FILENAME);
|
|
86
|
+
const masterKeyPath = path.join(this.extensionPath, MASTER_KEY_FILENAME);
|
|
87
|
+
// Surface a user-friendly error if login hasn't happened yet
|
|
88
|
+
try {
|
|
89
|
+
await fs.access(tokenPath);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new Error(`Gemini CLI Workspace token file not found at ${tokenPath}. ` +
|
|
93
|
+
`Please run 'GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE=true gemini' ` +
|
|
94
|
+
`and complete Google sign-in first, then retry.`);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await fs.access(masterKeyPath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new Error(`Gemini CLI Workspace master key file not found at ${masterKeyPath}. ` +
|
|
101
|
+
`The extension should create this on first login.`);
|
|
102
|
+
}
|
|
103
|
+
const encryptedText = await fs.readFile(tokenPath, 'utf8');
|
|
104
|
+
const masterKey = await fs.readFile(masterKeyPath);
|
|
105
|
+
// Derive the extension's encryption key (scrypt with hostname-username salt).
|
|
106
|
+
// Must match `file-token-storage.ts` exactly.
|
|
107
|
+
const salt = `${os.hostname()}-${os.userInfo().username}${SALT_SUFFIX}`;
|
|
108
|
+
const encryptionKey = crypto.scryptSync(masterKey, salt, 32);
|
|
109
|
+
const decrypted = this.decryptExtensionFormat(encryptedText, encryptionKey);
|
|
110
|
+
const parsed = JSON.parse(decrypted);
|
|
111
|
+
const entry = parsed[MAIN_ACCOUNT_KEY];
|
|
112
|
+
if (!entry) {
|
|
113
|
+
throw new Error(`Extension token file does not contain a '${MAIN_ACCOUNT_KEY}' entry. ` +
|
|
114
|
+
`Did login complete successfully?`);
|
|
115
|
+
}
|
|
116
|
+
const { accessToken, refreshToken, tokenType, scope, expiresAt } = entry.token;
|
|
117
|
+
if (!accessToken || !refreshToken) {
|
|
118
|
+
throw new Error(`Extension token is missing accessToken or refreshToken — login may be incomplete.`);
|
|
119
|
+
}
|
|
120
|
+
const scopes = typeof scope === 'string' ? scope.split(' ').filter(Boolean) : [];
|
|
121
|
+
const accountEmail = await this.fetchUserEmail(accessToken);
|
|
122
|
+
return {
|
|
123
|
+
type: 'google-oauth',
|
|
124
|
+
accessToken,
|
|
125
|
+
refreshToken,
|
|
126
|
+
tokenType: tokenType ?? 'Bearer',
|
|
127
|
+
expiresAt: expiresAt ?? Date.now() + 3600_000,
|
|
128
|
+
scopes,
|
|
129
|
+
accountEmail,
|
|
130
|
+
clientId: this.clientId,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Remove the extension's token file. Call after `captureFromFile()` to
|
|
135
|
+
* prepare for the next account's login (the extension otherwise returns
|
|
136
|
+
* cached credentials if scopes match, skipping the login prompt).
|
|
137
|
+
* Leaves the master.key file untouched so existing ciphertexts remain
|
|
138
|
+
* decryptable if needed.
|
|
139
|
+
*/
|
|
140
|
+
async clearExtensionFile() {
|
|
141
|
+
const tokenPath = path.join(this.extensionPath, TOKEN_FILENAME);
|
|
142
|
+
try {
|
|
143
|
+
await fs.unlink(tokenPath);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (err.code === 'ENOENT')
|
|
147
|
+
return;
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ------------------------------------------------------------------
|
|
152
|
+
// Refresh / getAccessToken
|
|
153
|
+
// ------------------------------------------------------------------
|
|
154
|
+
/**
|
|
155
|
+
* Return a valid `GoogleOAuthPayload`, refreshing via the extension's
|
|
156
|
+
* cloud function if the access token is within the expiry buffer.
|
|
157
|
+
*
|
|
158
|
+
* Concurrent calls for the same credential share a single refresh.
|
|
159
|
+
*
|
|
160
|
+
* @throws CredentialRevokedError if the refresh token is no longer valid
|
|
161
|
+
*/
|
|
162
|
+
async getAccessToken(entry, payload) {
|
|
163
|
+
// Fast path: still valid
|
|
164
|
+
if (payload.expiresAt - Date.now() > this.expiryBufferMs) {
|
|
165
|
+
return payload;
|
|
166
|
+
}
|
|
167
|
+
// Coalesce concurrent refreshes for the same credential
|
|
168
|
+
const existing = this.refreshInFlight.get(entry.id);
|
|
169
|
+
if (existing)
|
|
170
|
+
return existing;
|
|
171
|
+
// Cooldown — avoid hammering the refresh endpoint
|
|
172
|
+
const last = this.lastRefreshAttempt.get(entry.id) ?? 0;
|
|
173
|
+
if (Date.now() - last < REFRESH_COOLDOWN_MS) {
|
|
174
|
+
// Within cooldown — return the (possibly expired) payload; caller will
|
|
175
|
+
// get a fresh attempt after the cooldown window.
|
|
176
|
+
return payload;
|
|
177
|
+
}
|
|
178
|
+
const promise = this.doRefresh(entry, payload);
|
|
179
|
+
this.refreshInFlight.set(entry.id, promise);
|
|
180
|
+
this.lastRefreshAttempt.set(entry.id, Date.now());
|
|
181
|
+
try {
|
|
182
|
+
return await promise;
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
this.refreshInFlight.delete(entry.id);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async doRefresh(entry, payload) {
|
|
189
|
+
const url = this.cloudFunctionUrl.replace(/\/$/, '') + this.refreshPath;
|
|
190
|
+
const response = await this.fetchFn(url, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
193
|
+
body: JSON.stringify({ refresh_token: payload.refreshToken }),
|
|
194
|
+
});
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
const body = await response.text().catch(() => '');
|
|
197
|
+
// Google responds 400 with body containing "invalid_grant" when the
|
|
198
|
+
// refresh token is revoked/expired beyond recovery.
|
|
199
|
+
if (response.status === 400 &&
|
|
200
|
+
/invalid_grant/i.test(body)) {
|
|
201
|
+
await this.store.updateCredential(entry.id, { status: 'revoked' });
|
|
202
|
+
throw new CredentialRevokedError(entry.id, entry.name, `Log in to the Gemini CLI Workspace extension again and re-import this credential in Crewly.`);
|
|
203
|
+
}
|
|
204
|
+
throw new Error(`Token refresh failed (${response.status}): ${body.slice(0, 500)}`);
|
|
205
|
+
}
|
|
206
|
+
const data = (await response.json());
|
|
207
|
+
if (!data.access_token) {
|
|
208
|
+
throw new Error('Token refresh response missing access_token');
|
|
209
|
+
}
|
|
210
|
+
const expiresAt = Date.now() + (data.expires_in ?? 3600) * 1000;
|
|
211
|
+
const updated = {
|
|
212
|
+
...payload,
|
|
213
|
+
accessToken: data.access_token,
|
|
214
|
+
expiresAt,
|
|
215
|
+
// Google typically does NOT issue a new refresh_token on refresh,
|
|
216
|
+
// but honor it if present
|
|
217
|
+
refreshToken: data.refresh_token ?? payload.refreshToken,
|
|
218
|
+
scopes: typeof data.scope === 'string'
|
|
219
|
+
? data.scope.split(' ').filter(Boolean)
|
|
220
|
+
: payload.scopes,
|
|
221
|
+
};
|
|
222
|
+
await this.store.setPayload(entry.id, updated);
|
|
223
|
+
await this.store.updateCredential(entry.id, {
|
|
224
|
+
expiresAt,
|
|
225
|
+
lastUsedAt: new Date().toISOString(),
|
|
226
|
+
});
|
|
227
|
+
return updated;
|
|
228
|
+
}
|
|
229
|
+
// ------------------------------------------------------------------
|
|
230
|
+
// Internals
|
|
231
|
+
// ------------------------------------------------------------------
|
|
232
|
+
/**
|
|
233
|
+
* Decrypt the extension's `{iv_hex}:{authTag_hex}:{ciphertext_hex}`
|
|
234
|
+
* format (AES-256-GCM).
|
|
235
|
+
*/
|
|
236
|
+
decryptExtensionFormat(encryptedText, key) {
|
|
237
|
+
const parts = encryptedText.split(':');
|
|
238
|
+
if (parts.length !== 3) {
|
|
239
|
+
throw new Error('Invalid extension token format (expected iv:tag:ciphertext)');
|
|
240
|
+
}
|
|
241
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
242
|
+
const authTag = Buffer.from(parts[1], 'hex');
|
|
243
|
+
const ciphertextHex = parts[2];
|
|
244
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
245
|
+
decipher.setAuthTag(authTag);
|
|
246
|
+
let decrypted = decipher.update(ciphertextHex, 'hex', 'utf8');
|
|
247
|
+
decrypted += decipher.final('utf8');
|
|
248
|
+
return decrypted;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Fetch the user's email via Google's userinfo endpoint. Best-effort;
|
|
252
|
+
* returns undefined on failure.
|
|
253
|
+
*/
|
|
254
|
+
async fetchUserEmail(accessToken) {
|
|
255
|
+
try {
|
|
256
|
+
const response = await this.fetchFn(USERINFO_ENDPOINT, {
|
|
257
|
+
method: 'GET',
|
|
258
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
259
|
+
});
|
|
260
|
+
if (!response.ok)
|
|
261
|
+
return undefined;
|
|
262
|
+
const data = (await response.json());
|
|
263
|
+
return data.email;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Test helpers (not exported from public barrel — intended for tests only)
|
|
272
|
+
// ============================================================================
|
|
273
|
+
/**
|
|
274
|
+
* Encrypt a plaintext string with the extension's file format. Used in
|
|
275
|
+
* tests to generate fake token files.
|
|
276
|
+
*/
|
|
277
|
+
export function encryptExtensionFormatForTesting(plaintext, key) {
|
|
278
|
+
const iv = crypto.randomBytes(16);
|
|
279
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
280
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|
281
|
+
encrypted += cipher.final('hex');
|
|
282
|
+
const authTag = cipher.getAuthTag();
|
|
283
|
+
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Derive the extension's encryption key using the real scrypt + salt.
|
|
287
|
+
* Test-only export.
|
|
288
|
+
*/
|
|
289
|
+
export function deriveExtensionKeyForTesting(masterKey) {
|
|
290
|
+
const salt = `${os.hostname()}-${os.userInfo().username}${SALT_SUFFIX}`;
|
|
291
|
+
return crypto.scryptSync(masterKey, salt, 32);
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=gemini-cli-workspace.helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini-cli-workspace.helper.js","sourceRoot":"","sources":["../../../../../../../backend/src/services/credential/helpers/gemini-cli-workspace.helper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,EAKL,sBAAsB,GACvB,MAAM,oCAAoC,CAAC;AAG5C,+EAA+E;AAC/E,kDAAkD;AAClD,+CAA+C;AAC/C,+EAA+E;AAE/E,MAAM,sBAAsB,GAAG,IAAI,CAAC,IAAI,CACtC,EAAE,CAAC,OAAO,EAAE,EACZ,SAAS,EACT,YAAY,EACZ,kBAAkB,CACnB,CAAC;AACF,MAAM,cAAc,GAAG,iCAAiC,CAAC;AACzD,MAAM,mBAAmB,GAAG,kCAAkC,CAAC;AAC/D,MAAM,gBAAgB,GAAG,cAAc,CAAC;AACxC,MAAM,WAAW,GAAG,uBAAuB,CAAC;AAE5C,MAAM,0BAA0B,GAC9B,kDAAkD,CAAC;AACrD,MAAM,oBAAoB,GAAG,eAAe,CAAC;AAC7C,MAAM,iBAAiB,GACrB,0EAA0E,CAAC;AAE7E,sEAAsE;AACtE,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,oEAAoE;AACpE,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,MAAM,iBAAiB,GAAG,+CAA+C,CAAC;AAqD1E,+EAA+E;AAC/E,SAAS;AACT,+EAA+E;AAE/E;;;GAGG;AACH,MAAM,OAAO,wBAAwB;IAC1B,IAAI,GAAyB,sBAAsB,CAAC;IAE5C,aAAa,CAAS;IACtB,gBAAgB,CAAS;IACzB,WAAW,CAAS;IACpB,QAAQ,CAAS;IACjB,cAAc,CAAS;IACvB,OAAO,CAAY;IACnB,KAAK,CAAyB;IAE/C,mFAAmF;IAClE,eAAe,GAAG,IAAI,GAAG,EAGvC,CAAC;IACJ,oEAAoE;IACnD,kBAAkB,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEhE,YACE,KAA6B,EAC7B,MAA8B;QAE9B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,aAAa,GAAG,MAAM,EAAE,aAAa,IAAI,sBAAsB,CAAC;QACrE,IAAI,CAAC,gBAAgB;YACnB,MAAM,EAAE,gBAAgB,IAAI,0BAA0B,CAAC;QACzD,IAAI,CAAC,WAAW,GAAG,MAAM,EAAE,WAAW,IAAI,oBAAoB,CAAC;QAC/D,IAAI,CAAC,QAAQ,GAAG,MAAM,EAAE,QAAQ,IAAI,iBAAiB,CAAC;QACtD,IAAI,CAAC,cAAc,GAAG,MAAM,EAAE,cAAc,IAAI,wBAAwB,CAAC;QACzE,IAAI,CAAC,OAAO;YACV,MAAM,EAAE,KAAK;gBACb,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,IAAmB,CAAqC,CAAC,CAAC;IACzF,CAAC;IAED,qEAAqE;IACrE,WAAW;IACX,qEAAqE;IAErE;;;;;;OAMG;IACH,KAAK,CAAC,eAAe;QACnB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;QAChE,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,mBAAmB,CAAC,CAAC;QAEzE,6DAA6D;QAC7D,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,gDAAgD,SAAS,IAAI;gBAC3D,mEAAmE;gBACnE,gDAAgD,CACnD,CAAC;QACJ,CAAC;QACD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,qDAAqD,aAAa,IAAI;gBACpE,kDAAkD,CACrD,CAAC;QACJ,CAAC;QAED,MAAM,aAAa,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAC3D,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;QAEnD,8EAA8E;QAC9E,8CAA8C;QAC9C,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,GAAG,WAAW,EAAE,CAAC;QACxE,MAAM,aAAa,GAAG,MAAM,CAAC,UAAU,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QAE7D,MAAM,SAAS,GAAG,IAAI,CAAC,sBAAsB,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QAC5E,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAGlC,CAAC;QACF,MAAM,KAAK,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;QACvC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CACb,4CAA4C,gBAAgB,WAAW;gBACrE,kCAAkC,CACrC,CAAC;QACJ,CAAC;QAED,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,GAC9D,KAAK,CAAC,KAAK,CAAC;QACd,IAAI,CAAC,WAAW,IAAI,CAAC,YAAY,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACb,mFAAmF,CACpF,CAAC;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAEjF,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;QAE5D,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,WAAW;YACX,YAAY;YACZ,SAAS,EAAG,SAAsB,IAAI,QAAQ;YAC9C,SAAS,EAAE,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ;YAC7C,MAAM;YACN,YAAY;YACZ,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC;IACJ,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,kBAAkB;QACtB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;QAChE,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO;YAC7D,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,qEAAqE;IACrE,4BAA4B;IAC5B,qEAAqE;IAErE;;;;;;;OAOG;IACH,KAAK,CAAC,cAAc,CAClB,KAA8B,EAC9B,OAA2B;QAE3B,yBAAyB;QACzB,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;YACzD,OAAO,OAAO,CAAC;QACjB,CAAC;QAED,wDAAwD;QACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACpD,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAE9B,kDAAkD;QAClD,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QACxD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,mBAAmB,EAAE,CAAC;YAC5C,uEAAuE;YACvE,iDAAiD;YACjD,OAAO,OAAO,CAAC;QACjB,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAC/C,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QAC5C,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAClD,IAAI,CAAC;YACH,OAAO,MAAM,OAAO,CAAC;QACvB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,SAAS,CACrB,KAA8B,EAC9B,OAA2B;QAE3B,MAAM,GAAG,GAAG,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC;QACxE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;YACvC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC;SAC9D,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YACnD,oEAAoE;YACpE,oDAAoD;YACpD,IACE,QAAQ,CAAC,MAAM,KAAK,GAAG;gBACvB,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAC3B,CAAC;gBACD,MAAM,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;gBACnE,MAAM,IAAI,sBAAsB,CAC9B,KAAK,CAAC,EAAE,EACR,KAAK,CAAC,IAAI,EACV,6FAA6F,CAC9F,CAAC;YACJ,CAAC;YACD,MAAM,IAAI,KAAK,CACb,yBAAyB,QAAQ,CAAC,MAAM,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CACnE,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAMlC,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;QACjE,CAAC;QAED,MAAM,SAAS,GACb,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC;QAEhD,MAAM,OAAO,GAAuB;YAClC,GAAG,OAAO;YACV,WAAW,EAAE,IAAI,CAAC,YAAY;YAC9B,SAAS;YACT,kEAAkE;YAClE,0BAA0B;YAC1B,YAAY,EAAE,IAAI,CAAC,aAAa,IAAI,OAAO,CAAC,YAAY;YACxD,MAAM,EACJ,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;gBAC5B,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;gBACvC,CAAC,CAAC,OAAO,CAAC,MAAM;SACrB,CAAC;QAEF,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QAC/C,MAAM,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,EAAE;YAC1C,SAAS;YACT,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACrC,CAAC,CAAC;QAEH,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,qEAAqE;IACrE,aAAa;IACb,qEAAqE;IAErE;;;OAGG;IACK,sBAAsB,CAC5B,aAAqB,EACrB,GAAW;QAEX,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACjF,CAAC;QACD,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACxC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAC7C,MAAM,aAAa,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QAE/B,MAAM,QAAQ,GAAG,MAAM,CAAC,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;QACjE,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAE7B,IAAI,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QAC9D,SAAS,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACpC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,cAAc,CAC1B,WAAmB;QAEnB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE;gBACrD,MAAM,EAAE,KAAK;gBACb,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,WAAW,EAAE,EAAE;aACpD,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,OAAO,SAAS,CAAC;YACnC,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAuB,CAAC;YAC3D,OAAO,IAAI,CAAC,KAAK,CAAC;QACpB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;CACF;AAED,+EAA+E;AAC/E,2EAA2E;AAC3E,+EAA+E;AAE/E;;;GAGG;AACH,MAAM,UAAU,gCAAgC,CAC9C,SAAiB,EACjB,GAAW;IAEX,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAC7D,IAAI,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IACxD,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IACpC,OAAO,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,SAAS,CAAC;AAC9E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,4BAA4B,CAAC,SAAiB;IAC5D,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,GAAG,WAAW,EAAE,CAAC;IACxE,OAAO,MAAM,CAAC,UAAU,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Management Service
|
|
3
|
+
*
|
|
4
|
+
* Handles persistence and validation of Crewly application settings.
|
|
5
|
+
* Settings are stored in ~/.crewly/settings.json.
|
|
6
|
+
*
|
|
7
|
+
* @module services/settings/settings.service
|
|
8
|
+
*/
|
|
9
|
+
import { CrewlySettings, UpdateSettingsInput, SettingsValidationResult, ApiKeyProvider, ApiKeyResolutionContext } from '../../types/settings.types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Error thrown when settings validation fails
|
|
12
|
+
*/
|
|
13
|
+
export declare class SettingsValidationError extends Error {
|
|
14
|
+
readonly errors: string[];
|
|
15
|
+
constructor(errors: string[]);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Error thrown when settings file cannot be read or parsed
|
|
19
|
+
*/
|
|
20
|
+
export declare class SettingsFileError extends Error {
|
|
21
|
+
readonly cause?: Error | undefined;
|
|
22
|
+
constructor(message: string, cause?: Error | undefined);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Service for managing Crewly application settings
|
|
26
|
+
*
|
|
27
|
+
* Settings are stored in ~/.crewly/settings.json by default.
|
|
28
|
+
* Default values are used when no settings file exists.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const service = getSettingsService();
|
|
33
|
+
*
|
|
34
|
+
* // Get current settings
|
|
35
|
+
* const settings = await service.getSettings();
|
|
36
|
+
*
|
|
37
|
+
* // Update settings
|
|
38
|
+
* await service.updateSettings({
|
|
39
|
+
* general: { verboseLogging: true },
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* // Reset to defaults
|
|
43
|
+
* await service.resetSettings();
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare class SettingsService {
|
|
47
|
+
private readonly settingsDir;
|
|
48
|
+
private readonly settingsFile;
|
|
49
|
+
private settingsCache;
|
|
50
|
+
/**
|
|
51
|
+
* Create a new SettingsService instance
|
|
52
|
+
*
|
|
53
|
+
* @param options - Configuration options
|
|
54
|
+
* @param options.settingsDir - Directory for settings file
|
|
55
|
+
*/
|
|
56
|
+
constructor(options?: {
|
|
57
|
+
settingsDir?: string;
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Get current settings, loading from file if not cached
|
|
61
|
+
*
|
|
62
|
+
* If settings file doesn't exist or is invalid, returns default settings.
|
|
63
|
+
*
|
|
64
|
+
* @returns Current settings
|
|
65
|
+
*/
|
|
66
|
+
getSettings(): Promise<CrewlySettings>;
|
|
67
|
+
/**
|
|
68
|
+
* Update settings with partial input
|
|
69
|
+
*
|
|
70
|
+
* Merges the input with current settings and validates before saving.
|
|
71
|
+
*
|
|
72
|
+
* @param input - Partial settings to update
|
|
73
|
+
* @returns Updated settings
|
|
74
|
+
* @throws {SettingsValidationError} If validation fails
|
|
75
|
+
*/
|
|
76
|
+
updateSettings(input: UpdateSettingsInput): Promise<CrewlySettings>;
|
|
77
|
+
/**
|
|
78
|
+
* Reset all settings to defaults
|
|
79
|
+
*
|
|
80
|
+
* @returns Default settings
|
|
81
|
+
*/
|
|
82
|
+
resetSettings(): Promise<CrewlySettings>;
|
|
83
|
+
/**
|
|
84
|
+
* Reset a specific settings section to defaults
|
|
85
|
+
*
|
|
86
|
+
* @param section - The section to reset ('general', 'chat', or 'skills')
|
|
87
|
+
* @returns Updated settings
|
|
88
|
+
*/
|
|
89
|
+
resetSection(section: keyof CrewlySettings): Promise<CrewlySettings>;
|
|
90
|
+
/**
|
|
91
|
+
* Validate settings input without saving
|
|
92
|
+
*
|
|
93
|
+
* Useful for form validation before submission.
|
|
94
|
+
*
|
|
95
|
+
* @param input - Settings to validate
|
|
96
|
+
* @returns Validation result
|
|
97
|
+
*/
|
|
98
|
+
validateSettingsInput(input: UpdateSettingsInput): Promise<SettingsValidationResult>;
|
|
99
|
+
/**
|
|
100
|
+
* Export settings to a file
|
|
101
|
+
*
|
|
102
|
+
* @param exportPath - Path to export file
|
|
103
|
+
*/
|
|
104
|
+
exportSettings(exportPath: string): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Import settings from a file
|
|
107
|
+
*
|
|
108
|
+
* Validates imported settings before saving.
|
|
109
|
+
*
|
|
110
|
+
* @param importPath - Path to import file
|
|
111
|
+
* @returns Imported settings
|
|
112
|
+
* @throws {SettingsValidationError} If imported settings are invalid
|
|
113
|
+
* @throws {SettingsFileError} If file cannot be read or parsed
|
|
114
|
+
*/
|
|
115
|
+
importSettings(importPath: string): Promise<CrewlySettings>;
|
|
116
|
+
/**
|
|
117
|
+
* Resolve an API key through the override chain: skill → runtime → global → env var
|
|
118
|
+
*
|
|
119
|
+
* @param provider - The API key provider (gemini, anthropic, openai)
|
|
120
|
+
* @param context - Optional context with runtime and/or skill for override resolution
|
|
121
|
+
* @returns The resolved API key or undefined if not configured anywhere
|
|
122
|
+
*/
|
|
123
|
+
getApiKey(provider: ApiKeyProvider, context?: ApiKeyResolutionContext): Promise<string | undefined>;
|
|
124
|
+
/**
|
|
125
|
+
* Clear the settings cache
|
|
126
|
+
*
|
|
127
|
+
* Forces the next getSettings call to reload from disk.
|
|
128
|
+
*/
|
|
129
|
+
clearCache(): void;
|
|
130
|
+
/**
|
|
131
|
+
* Get the path to the settings file
|
|
132
|
+
*
|
|
133
|
+
* @returns Absolute path to settings.json
|
|
134
|
+
*/
|
|
135
|
+
getSettingsFilePath(): string;
|
|
136
|
+
/**
|
|
137
|
+
* Check if a settings file exists
|
|
138
|
+
*
|
|
139
|
+
* @returns True if settings file exists
|
|
140
|
+
*/
|
|
141
|
+
hasSettingsFile(): Promise<boolean>;
|
|
142
|
+
/**
|
|
143
|
+
* Migrate legacy settings format to current format
|
|
144
|
+
*
|
|
145
|
+
* Converts old claudeCodeCommand/claudeCodeInitScript fields
|
|
146
|
+
* to the new runtimeCommands map.
|
|
147
|
+
*
|
|
148
|
+
* @param loaded - Partially loaded settings from disk
|
|
149
|
+
*/
|
|
150
|
+
private migrateSettings;
|
|
151
|
+
/**
|
|
152
|
+
* Save settings to disk
|
|
153
|
+
*
|
|
154
|
+
* @param settings - Settings to save
|
|
155
|
+
*/
|
|
156
|
+
private saveSettings;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Get the singleton SettingsService instance
|
|
160
|
+
*
|
|
161
|
+
* @returns The SettingsService instance
|
|
162
|
+
*/
|
|
163
|
+
export declare function getSettingsService(): SettingsService;
|
|
164
|
+
/**
|
|
165
|
+
* Reset the singleton instance (for testing)
|
|
166
|
+
*/
|
|
167
|
+
export declare function resetSettingsService(): void;
|
|
168
|
+
//# sourceMappingURL=settings.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/settings/settings.service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,wBAAwB,EAKxB,cAAc,EACd,uBAAuB,EAExB,MAAM,+BAA+B,CAAC;AAOvC;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,SAAgB,MAAM,EAAE,MAAM,EAAE,CAAC;gBAErB,MAAM,EAAE,MAAM,EAAE;CAK7B;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;aACG,KAAK,CAAC,EAAE,KAAK;gBAA9C,OAAO,EAAE,MAAM,EAAkB,KAAK,CAAC,EAAE,KAAK,YAAA;CAI3D;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,aAAa,CAA+B;IAEpD;;;;;OAKG;gBACS,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;IAM9C;;;;;;OAMG;IACG,WAAW,IAAI,OAAO,CAAC,cAAc,CAAC;IAe5C;;;;;;;;OAQG;IACG,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC;IAczE;;;;OAIG;IACG,aAAa,IAAI,OAAO,CAAC,cAAc,CAAC;IAO9C;;;;;OAKG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IAc1E;;;;;;;OAOG;IACG,qBAAqB,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,wBAAwB,CAAC;IAM1F;;;;OAIG;IACG,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvD;;;;;;;;;OASG;IACG,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IA6BjE;;;;;;OAMG;IACG,SAAS,CAAC,QAAQ,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAKzG;;;;OAIG;IACH,UAAU,IAAI,IAAI;IAIlB;;;;OAIG;IACH,mBAAmB,IAAI,MAAM;IAI7B;;;;OAIG;IACG,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAazC;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IA6BvB;;;;OAIG;YACW,YAAY;CAI3B;AAQD;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,CAKpD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}
|