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.
Files changed (113) hide show
  1. package/config/roles/orchestrator/prompt.md +182 -25
  2. package/config/skills/agent/core/cancel-followup/SKILL.md +38 -0
  3. package/config/skills/agent/core/cancel-followup/execute.sh +111 -0
  4. package/config/skills/agent/core/cancel-followup/execute.test.sh +42 -0
  5. package/config/skills/agent/core/list-my-followups/SKILL.md +36 -0
  6. package/config/skills/agent/core/list-my-followups/execute.sh +93 -0
  7. package/config/skills/agent/core/list-my-followups/execute.test.sh +41 -0
  8. package/config/skills/agent/core/schedule-followup/SKILL.md +53 -0
  9. package/config/skills/agent/core/schedule-followup/execute.sh +195 -0
  10. package/config/skills/agent/core/schedule-followup/execute.test.sh +48 -0
  11. package/config/skills/agent/core/watch-for-event/SKILL.md +60 -0
  12. package/config/skills/agent/core/watch-for-event/execute.sh +177 -0
  13. package/config/skills/agent/core/watch-for-event/execute.test.sh +43 -0
  14. package/config/skills/orchestrator/credential-manager/SKILL.md +218 -0
  15. package/config/skills/orchestrator/credential-manager/execute.sh +166 -0
  16. package/dist/backend/backend/src/controllers/credentials/credentials.controller.d.ts +80 -0
  17. package/dist/backend/backend/src/controllers/credentials/credentials.controller.d.ts.map +1 -0
  18. package/dist/backend/backend/src/controllers/credentials/credentials.controller.js +365 -0
  19. package/dist/backend/backend/src/controllers/credentials/credentials.controller.js.map +1 -0
  20. package/dist/backend/backend/src/controllers/credentials/credentials.routes.d.ts +26 -0
  21. package/dist/backend/backend/src/controllers/credentials/credentials.routes.d.ts.map +1 -0
  22. package/dist/backend/backend/src/controllers/credentials/credentials.routes.js +40 -0
  23. package/dist/backend/backend/src/controllers/credentials/credentials.routes.js.map +1 -0
  24. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +23 -14
  25. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
  26. package/dist/backend/backend/src/scripts/backfill-mission-priority.d.ts +3 -1
  27. package/dist/backend/backend/src/scripts/backfill-mission-priority.d.ts.map +1 -1
  28. package/dist/backend/backend/src/scripts/backfill-mission-priority.js +16 -4
  29. package/dist/backend/backend/src/scripts/backfill-mission-priority.js.map +1 -1
  30. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -1
  31. package/dist/backend/backend/src/services/browser/browser-proxy.service.js +22 -2
  32. package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
  33. package/dist/backend/backend/src/services/credential/credential-store.service.d.ts +161 -0
  34. package/dist/backend/backend/src/services/credential/credential-store.service.d.ts.map +1 -0
  35. package/dist/backend/backend/src/services/credential/credential-store.service.js +298 -0
  36. package/dist/backend/backend/src/services/credential/credential-store.service.js.map +1 -0
  37. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts +117 -0
  38. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -0
  39. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +293 -0
  40. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -0
  41. package/dist/backend/backend/src/services/project/task.service.d.ts +18 -2
  42. package/dist/backend/backend/src/services/project/task.service.d.ts.map +1 -1
  43. package/dist/backend/backend/src/services/project/task.service.js +69 -53
  44. package/dist/backend/backend/src/services/project/task.service.js.map +1 -1
  45. package/dist/backend/backend/src/services/v3/contract-matcher.d.ts +20 -0
  46. package/dist/backend/backend/src/services/v3/contract-matcher.d.ts.map +1 -0
  47. package/dist/backend/backend/src/services/v3/contract-matcher.js +33 -0
  48. package/dist/backend/backend/src/services/v3/contract-matcher.js.map +1 -0
  49. package/dist/backend/backend/src/services/v3/escalation.service.d.ts +20 -1
  50. package/dist/backend/backend/src/services/v3/escalation.service.d.ts.map +1 -1
  51. package/dist/backend/backend/src/services/v3/escalation.service.js +97 -28
  52. package/dist/backend/backend/src/services/v3/escalation.service.js.map +1 -1
  53. package/dist/backend/backend/src/services/v3/service-contract-gate.service.d.ts +6 -4
  54. package/dist/backend/backend/src/services/v3/service-contract-gate.service.d.ts.map +1 -1
  55. package/dist/backend/backend/src/services/v3/service-contract-gate.service.js +18 -28
  56. package/dist/backend/backend/src/services/v3/service-contract-gate.service.js.map +1 -1
  57. package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.d.ts.map +1 -1
  58. package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js +14 -9
  59. package/dist/backend/backend/src/services/v3/team-trigger-reconciler.service.js.map +1 -1
  60. package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts +34 -1
  61. package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts.map +1 -1
  62. package/dist/backend/backend/src/services/v3/trigger-engine.service.js +115 -5
  63. package/dist/backend/backend/src/services/v3/trigger-engine.service.js.map +1 -1
  64. package/dist/backend/backend/src/types/credential.types.d.ts +185 -0
  65. package/dist/backend/backend/src/types/credential.types.d.ts.map +1 -0
  66. package/dist/backend/backend/src/types/credential.types.js +76 -0
  67. package/dist/backend/backend/src/types/credential.types.js.map +1 -0
  68. package/dist/backend/backend/src/utils/encryption.utils.d.ts +57 -0
  69. package/dist/backend/backend/src/utils/encryption.utils.d.ts.map +1 -0
  70. package/dist/backend/backend/src/utils/encryption.utils.js +162 -0
  71. package/dist/backend/backend/src/utils/encryption.utils.js.map +1 -0
  72. package/dist/cli/backend/src/services/credential/credential-store.service.d.ts +161 -0
  73. package/dist/cli/backend/src/services/credential/credential-store.service.d.ts.map +1 -0
  74. package/dist/cli/backend/src/services/credential/credential-store.service.js +298 -0
  75. package/dist/cli/backend/src/services/credential/credential-store.service.js.map +1 -0
  76. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts +117 -0
  77. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -0
  78. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +293 -0
  79. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -0
  80. package/dist/cli/backend/src/services/settings/settings.service.d.ts +168 -0
  81. package/dist/cli/backend/src/services/settings/settings.service.d.ts.map +1 -0
  82. package/dist/cli/backend/src/services/settings/settings.service.js +312 -0
  83. package/dist/cli/backend/src/services/settings/settings.service.js.map +1 -0
  84. package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts +159 -0
  85. package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts.map +1 -0
  86. package/dist/cli/backend/src/services/skill/skill-executor.service.js +626 -0
  87. package/dist/cli/backend/src/services/skill/skill-executor.service.js.map +1 -0
  88. package/dist/cli/backend/src/services/skill/skill.service.d.ts +273 -0
  89. package/dist/cli/backend/src/services/skill/skill.service.d.ts.map +1 -0
  90. package/dist/cli/backend/src/services/skill/skill.service.js +655 -0
  91. package/dist/cli/backend/src/services/skill/skill.service.js.map +1 -0
  92. package/dist/cli/backend/src/types/credential.types.d.ts +185 -0
  93. package/dist/cli/backend/src/types/credential.types.d.ts.map +1 -0
  94. package/dist/cli/backend/src/types/credential.types.js +76 -0
  95. package/dist/cli/backend/src/types/credential.types.js.map +1 -0
  96. package/dist/cli/backend/src/utils/encryption.utils.d.ts +57 -0
  97. package/dist/cli/backend/src/utils/encryption.utils.d.ts.map +1 -0
  98. package/dist/cli/backend/src/utils/encryption.utils.js +162 -0
  99. package/dist/cli/backend/src/utils/encryption.utils.js.map +1 -0
  100. package/dist/cli/backend/src/utils/skill-md-parser.d.ts +38 -0
  101. package/dist/cli/backend/src/utils/skill-md-parser.d.ts.map +1 -0
  102. package/dist/cli/backend/src/utils/skill-md-parser.js +47 -0
  103. package/dist/cli/backend/src/utils/skill-md-parser.js.map +1 -0
  104. package/frontend/dist/assets/{index-dc92ab64.css → index-6aaa0630.css} +1 -1
  105. package/frontend/dist/assets/{index-76d76633.js → index-9e6d97d1.js} +334 -328
  106. package/frontend/dist/index.html +2 -2
  107. package/package.json +1 -1
  108. package/config/experts/empathetic-resolver/expert.json +0 -11
  109. package/config/experts/empathetic-resolver.md +0 -32
  110. package/config/experts/pragmatic-architect/expert.json +0 -11
  111. package/config/experts/pragmatic-architect.md +0 -32
  112. package/config/experts/viral-alchemist/expert.json +0 -11
  113. 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":"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"}
@@ -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"}