crewly 1.20.131 → 1.20.133

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 (186) hide show
  1. package/config/constants.ts +49 -0
  2. package/config/index.ts +2 -0
  3. package/config/skills/agent/core/find-skill/SKILL.md +58 -0
  4. package/config/skills/agent/core/find-skill/execute.sh +67 -0
  5. package/config/skills/agent/core/find-skill/execute.test.sh +69 -0
  6. package/config/skills/agent/core/generate-pdf/SKILL.md +24 -29
  7. package/config/skills/agent/core/generate-pdf/execute.sh +22 -78
  8. package/config/skills/agent/core/generate-pdf/execute.test.sh +45 -0
  9. package/config/skills/agent/core/install-skill/SKILL.md +67 -0
  10. package/config/skills/agent/core/install-skill/execute.sh +80 -0
  11. package/config/skills/agent/core/install-skill/execute.test.sh +74 -0
  12. package/config/skills/agent/pdf-tools/SKILL.md +103 -0
  13. package/config/skills/agent/pdf-tools/execute.sh +301 -0
  14. package/config/skills/agent/pdf-tools/execute.test.sh +218 -0
  15. package/config/skills/agent/pdf-tools/instructions.md +14 -0
  16. package/config/skills/agent/pdf-tools/pdf_tools.py +268 -0
  17. package/config/skills/agent/pdf-tools/skill.json +103 -0
  18. package/config/skills/agent/pdf-tools/style.css +143 -0
  19. package/config/skills/agent/transcribe-audio/SKILL.md +42 -5
  20. package/config/skills/agent/transcribe-audio/execute.sh +44 -11
  21. package/config/skills/agent/transcribe-audio/execute.test.sh +90 -0
  22. package/config/skills/agent/transcribe-audio/install-whisper-cpp.sh +126 -0
  23. package/config/skills/agent/transcribe-audio/install-whisper-cpp.test.sh +69 -0
  24. package/config/skills/agent/transcribe-audio/skill.json +67 -6
  25. package/config/skills/marketplace-registry.ts +8 -1
  26. package/config/skills/registry.json +337 -3
  27. package/dist/backend/backend/src/constants.d.ts +71 -0
  28. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  29. package/dist/backend/backend/src/constants.js +96 -0
  30. package/dist/backend/backend/src/constants.js.map +1 -1
  31. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts +56 -0
  32. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts.map +1 -0
  33. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js +114 -0
  34. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js.map +1 -0
  35. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +25 -0
  36. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
  37. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +73 -55
  38. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
  39. package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts +3 -0
  40. package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
  41. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +9 -0
  42. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
  43. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts +49 -0
  44. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts.map +1 -0
  45. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js +185 -0
  46. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js.map +1 -0
  47. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts +21 -0
  48. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts.map +1 -0
  49. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js +29 -0
  50. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js.map +1 -0
  51. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  52. package/dist/backend/backend/src/routes/api.routes.js +5 -0
  53. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  54. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
  55. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  56. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
  57. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  58. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
  59. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
  60. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
  61. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
  62. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts +115 -0
  63. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts.map +1 -0
  64. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js +160 -0
  65. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js.map +1 -0
  66. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
  67. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +8 -0
  68. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
  69. package/dist/backend/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -1
  70. package/dist/backend/backend/src/services/marketplace/marketplace.service.js +11 -3
  71. package/dist/backend/backend/src/services/marketplace/marketplace.service.js.map +1 -1
  72. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
  73. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
  74. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
  75. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
  76. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts +243 -0
  77. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts.map +1 -0
  78. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js +439 -0
  79. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js.map +1 -0
  80. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
  81. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
  82. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
  83. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
  84. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
  85. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
  86. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
  87. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
  88. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  89. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
  90. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  91. package/dist/backend/backend/src/types/marketplace.types.d.ts +16 -1
  92. package/dist/backend/backend/src/types/marketplace.types.d.ts.map +1 -1
  93. package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
  94. package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
  95. package/dist/backend/backend/src/utils/inbound-file-hint.utils.js +78 -0
  96. package/dist/backend/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
  97. package/dist/backend/build-info.json +2 -2
  98. package/dist/backend/config/constants.d.ts +48 -0
  99. package/dist/backend/config/constants.d.ts.map +1 -1
  100. package/dist/backend/config/constants.js +48 -0
  101. package/dist/backend/config/constants.js.map +1 -1
  102. package/dist/backend/config/index.d.ts +2 -2
  103. package/dist/backend/config/index.d.ts.map +1 -1
  104. package/dist/backend/config/index.js +2 -2
  105. package/dist/backend/config/index.js.map +1 -1
  106. package/dist/cli/backend/src/constants.d.ts +71 -0
  107. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  108. package/dist/cli/backend/src/constants.js +96 -0
  109. package/dist/cli/backend/src/constants.js.map +1 -1
  110. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
  111. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  112. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
  113. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  114. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
  115. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
  116. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
  117. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
  118. package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts +101 -0
  119. package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -0
  120. package/dist/cli/backend/src/services/marketplace/marketplace.service.js +392 -0
  121. package/dist/cli/backend/src/services/marketplace/marketplace.service.js.map +1 -0
  122. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
  123. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
  124. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
  125. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
  126. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
  127. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
  128. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
  129. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
  130. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
  131. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
  132. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
  133. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
  134. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  135. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
  136. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  137. package/dist/cli/backend/src/types/marketplace.types.d.ts +219 -0
  138. package/dist/cli/backend/src/types/marketplace.types.d.ts.map +1 -0
  139. package/dist/cli/backend/src/types/marketplace.types.js +6 -0
  140. package/dist/cli/backend/src/types/marketplace.types.js.map +1 -0
  141. package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
  142. package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
  143. package/dist/cli/backend/src/utils/inbound-file-hint.utils.js +78 -0
  144. package/dist/cli/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
  145. package/dist/cli/backend/src/utils/package-root.d.ts +15 -0
  146. package/dist/cli/backend/src/utils/package-root.d.ts.map +1 -0
  147. package/dist/cli/backend/src/utils/package-root.js +38 -0
  148. package/dist/cli/backend/src/utils/package-root.js.map +1 -0
  149. package/dist/cli/cli/src/commands/cloud.d.ts +45 -25
  150. package/dist/cli/cli/src/commands/cloud.d.ts.map +1 -1
  151. package/dist/cli/cli/src/commands/cloud.js +153 -47
  152. package/dist/cli/cli/src/commands/cloud.js.map +1 -1
  153. package/dist/cli/cli/src/commands/install.d.ts +13 -1
  154. package/dist/cli/cli/src/commands/install.d.ts.map +1 -1
  155. package/dist/cli/cli/src/commands/install.js +40 -3
  156. package/dist/cli/cli/src/commands/install.js.map +1 -1
  157. package/dist/cli/cli/src/commands/onboard-checklist.js +3 -3
  158. package/dist/cli/cli/src/commands/onboard-checklist.js.map +1 -1
  159. package/dist/cli/cli/src/commands/skills.d.ts +58 -0
  160. package/dist/cli/cli/src/commands/skills.d.ts.map +1 -0
  161. package/dist/cli/cli/src/commands/skills.js +132 -0
  162. package/dist/cli/cli/src/commands/skills.js.map +1 -0
  163. package/dist/cli/cli/src/index.js +18 -7
  164. package/dist/cli/cli/src/index.js.map +1 -1
  165. package/dist/cli/cli/src/utils/archive-creator.d.ts +14 -0
  166. package/dist/cli/cli/src/utils/archive-creator.d.ts.map +1 -1
  167. package/dist/cli/cli/src/utils/archive-creator.js +24 -3
  168. package/dist/cli/cli/src/utils/archive-creator.js.map +1 -1
  169. package/dist/cli/cli/src/utils/cloud-submit.d.ts.map +1 -1
  170. package/dist/cli/cli/src/utils/cloud-submit.js +14 -5
  171. package/dist/cli/cli/src/utils/cloud-submit.js.map +1 -1
  172. package/dist/cli/cli/src/utils/package-validator.d.ts +2 -0
  173. package/dist/cli/cli/src/utils/package-validator.d.ts.map +1 -1
  174. package/dist/cli/cli/src/utils/package-validator.js +16 -0
  175. package/dist/cli/cli/src/utils/package-validator.js.map +1 -1
  176. package/dist/cli/config/constants.d.ts +48 -0
  177. package/dist/cli/config/constants.d.ts.map +1 -1
  178. package/dist/cli/config/constants.js +48 -0
  179. package/dist/cli/config/constants.js.map +1 -1
  180. package/dist/cli/config/index.d.ts +2 -2
  181. package/dist/cli/config/index.d.ts.map +1 -1
  182. package/dist/cli/config/index.js +2 -2
  183. package/dist/cli/config/index.js.map +1 -1
  184. package/frontend/dist/assets/{index-5b98bf77.js → index-e6c403b6.js} +265 -265
  185. package/frontend/dist/index.html +1 -1
  186. package/package.json +1 -1
@@ -0,0 +1,55 @@
1
+ /**
2
+ * One-line hints for files that arrive with a chat message.
3
+ *
4
+ * A Slack voice message reaches the agent as `[Slack File: /…/F123-Audio_Clip.m4a
5
+ * (audio/mp4, 214KB)]`. One agent knew to run `transcribe-audio`; another said
6
+ * "I can't transcribe audio" and stopped. The path alone does not say which
7
+ * skill reads the file, so the bridge adds a hint naming it — and what to do
8
+ * when that skill is not set up yet (specs/skill-auto-install.md).
9
+ *
10
+ * PDFs already get their text extracted inline by the Slack bridge; a hint is
11
+ * added only when that extraction failed or was cut short, so the agent is not
12
+ * told twice about content it already has.
13
+ *
14
+ * @module utils/inbound-file-hint.utils
15
+ */
16
+ /** File extensions treated as audio/video even when the MIME type is generic. */
17
+ export declare const AUDIO_VIDEO_EXTENSIONS: readonly ["m4a", "mp3", "wav", "aac", "ogg", "oga", "opus", "flac", "amr", "webm", "mp4", "m4v", "mov", "mkv", "caf"];
18
+ /** Marker the Slack bridge appends when it cuts extracted PDF text. */
19
+ export declare const EXTRACTED_TEXT_TRUNCATED_MARKER = "[truncated]";
20
+ /** The fields of an inbound file the hint needs. */
21
+ export interface InboundFile {
22
+ name: string;
23
+ mimetype: string;
24
+ localPath: string;
25
+ /** Text already extracted inline (PDFs) */
26
+ extractedText?: string;
27
+ }
28
+ /**
29
+ * Whether a file is audio or video.
30
+ *
31
+ * @param file - Inbound file
32
+ * @returns True for audio/* or video/* MIME types or a known extension
33
+ */
34
+ export declare function isAudioOrVideo(file: Pick<InboundFile, 'name' | 'mimetype'>): boolean;
35
+ /**
36
+ * Whether a file is a PDF.
37
+ *
38
+ * @param file - Inbound file
39
+ * @returns True for application/pdf or a .pdf name
40
+ */
41
+ export declare function isPdf(file: Pick<InboundFile, 'name' | 'mimetype'>): boolean;
42
+ /**
43
+ * The hint line for one inbound file, or null when none is needed.
44
+ *
45
+ * @param file - Inbound file
46
+ * @returns e.g. `[Hint: voice/audio or video file — transcribe it with the transcribe-audio skill …]`
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * inboundFileHint({ name: 'Audio Clip.m4a', mimetype: 'audio/mp4', localPath: '/tmp/a.m4a' });
51
+ * // '[Hint: voice/audio or video file — use the transcribe-audio skill: {"audioFile":"/tmp/a.m4a"} …]'
52
+ * ```
53
+ */
54
+ export declare function inboundFileHint(file: InboundFile): string | null;
55
+ //# sourceMappingURL=inbound-file-hint.utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbound-file-hint.utils.d.ts","sourceRoot":"","sources":["../../../../../backend/src/utils/inbound-file-hint.utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,iFAAiF;AACjF,eAAO,MAAM,sBAAsB,uHAAwH,CAAC;AAE5J,uEAAuE;AACvE,eAAO,MAAM,+BAA+B,gBAAgB,CAAC;AAE7D,oDAAoD;AACpD,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAaD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,CAAC,GAAG,OAAO,CAIpF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,CAAC,GAAG,OAAO,CAE3E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAehE"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * One-line hints for files that arrive with a chat message.
3
+ *
4
+ * A Slack voice message reaches the agent as `[Slack File: /…/F123-Audio_Clip.m4a
5
+ * (audio/mp4, 214KB)]`. One agent knew to run `transcribe-audio`; another said
6
+ * "I can't transcribe audio" and stopped. The path alone does not say which
7
+ * skill reads the file, so the bridge adds a hint naming it — and what to do
8
+ * when that skill is not set up yet (specs/skill-auto-install.md).
9
+ *
10
+ * PDFs already get their text extracted inline by the Slack bridge; a hint is
11
+ * added only when that extraction failed or was cut short, so the agent is not
12
+ * told twice about content it already has.
13
+ *
14
+ * @module utils/inbound-file-hint.utils
15
+ */
16
+ /** File extensions treated as audio/video even when the MIME type is generic. */
17
+ export const AUDIO_VIDEO_EXTENSIONS = ['m4a', 'mp3', 'wav', 'aac', 'ogg', 'oga', 'opus', 'flac', 'amr', 'webm', 'mp4', 'm4v', 'mov', 'mkv', 'caf'];
18
+ /** Marker the Slack bridge appends when it cuts extracted PDF text. */
19
+ export const EXTRACTED_TEXT_TRUNCATED_MARKER = '[truncated]';
20
+ /**
21
+ * The file extension, lower-cased.
22
+ *
23
+ * @param name - File name
24
+ * @returns Extension without the dot ('' when none)
25
+ */
26
+ function extensionOf(name) {
27
+ const dot = name.lastIndexOf('.');
28
+ return dot >= 0 ? name.slice(dot + 1).toLowerCase() : '';
29
+ }
30
+ /**
31
+ * Whether a file is audio or video.
32
+ *
33
+ * @param file - Inbound file
34
+ * @returns True for audio/* or video/* MIME types or a known extension
35
+ */
36
+ export function isAudioOrVideo(file) {
37
+ const mime = (file.mimetype ?? '').toLowerCase();
38
+ if (mime.startsWith('audio/') || mime.startsWith('video/'))
39
+ return true;
40
+ return AUDIO_VIDEO_EXTENSIONS.includes(extensionOf(file.name ?? ''));
41
+ }
42
+ /**
43
+ * Whether a file is a PDF.
44
+ *
45
+ * @param file - Inbound file
46
+ * @returns True for application/pdf or a .pdf name
47
+ */
48
+ export function isPdf(file) {
49
+ return (file.mimetype ?? '').toLowerCase() === 'application/pdf' || extensionOf(file.name ?? '') === 'pdf';
50
+ }
51
+ /**
52
+ * The hint line for one inbound file, or null when none is needed.
53
+ *
54
+ * @param file - Inbound file
55
+ * @returns e.g. `[Hint: voice/audio or video file — transcribe it with the transcribe-audio skill …]`
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * inboundFileHint({ name: 'Audio Clip.m4a', mimetype: 'audio/mp4', localPath: '/tmp/a.m4a' });
60
+ * // '[Hint: voice/audio or video file — use the transcribe-audio skill: {"audioFile":"/tmp/a.m4a"} …]'
61
+ * ```
62
+ */
63
+ export function inboundFileHint(file) {
64
+ const pathJson = JSON.stringify(file.localPath);
65
+ if (isAudioOrVideo(file)) {
66
+ return (`[Hint: voice/audio or video file — use the transcribe-audio skill: {"audioFile":${pathJson}}. ` +
67
+ `If it answers "needsSetup": true, tell the user you are setting up transcription and run install-skill --id transcribe-audio — do not just say you cannot.]`);
68
+ }
69
+ if (isPdf(file)) {
70
+ const text = file.extractedText?.trim() ?? '';
71
+ if (text && !text.endsWith(EXTRACTED_TEXT_TRUNCATED_MARKER))
72
+ return null;
73
+ const why = text ? 'only the start of its text is included above' : 'its text could not be extracted here';
74
+ return `[Hint: PDF — ${why}; use the PDF reading skill (pdf-tools): {"action":"read","input":${pathJson}}.]`;
75
+ }
76
+ return null;
77
+ }
78
+ //# sourceMappingURL=inbound-file-hint.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbound-file-hint.utils.js","sourceRoot":"","sources":["../../../../../backend/src/utils/inbound-file-hint.utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,iFAAiF;AACjF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAU,CAAC;AAE5J,uEAAuE;AACvE,MAAM,CAAC,MAAM,+BAA+B,GAAG,aAAa,CAAC;AAW7D;;;;;GAKG;AACH,SAAS,WAAW,CAAC,IAAY;IAChC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC1D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAA4C;IAC1E,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACjD,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACxE,OAAQ,sBAA4C,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;AAC7F,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,KAAK,CAAC,IAA4C;IACjE,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,iBAAiB,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,KAAK,KAAK,CAAC;AAC5G,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,IAAiB;IAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAChD,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,OAAO,CACN,mFAAmF,QAAQ,KAAK;YAChG,6JAA6J,CAC7J,CAAC;IACH,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAC9C,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,+BAA+B,CAAC;YAAE,OAAO,IAAI,CAAC;QACzE,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,8CAA8C,CAAC,CAAC,CAAC,sCAAsC,CAAC;QAC3G,OAAO,gBAAgB,GAAG,qEAAqE,QAAQ,KAAK,CAAC;IAC9G,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Finds the Crewly package root by walking up from the given directory,
3
+ * looking for a package.json with `"name": "crewly"`.
4
+ *
5
+ * This works correctly in both development mode (where source lives at backend/src/)
6
+ * and compiled/npm-installed mode (where code lives at dist/backend/backend/src/).
7
+ * The naive `path.resolve(__dirname, '../..')` approach breaks in compiled mode
8
+ * because the directory depth differs.
9
+ *
10
+ * @param startDir - Directory to start searching from (typically the caller's __dirname).
11
+ * @returns The absolute path to the package root directory.
12
+ * @throws Error if no matching package.json is found after reaching the filesystem root.
13
+ */
14
+ export declare function findPackageRoot(startDir: string): string;
15
+ //# sourceMappingURL=package-root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-root.d.ts","sourceRoot":"","sources":["../../../../../backend/src/utils/package-root.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAwBxD"}
@@ -0,0 +1,38 @@
1
+ import path from 'path';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ /**
4
+ * Finds the Crewly package root by walking up from the given directory,
5
+ * looking for a package.json with `"name": "crewly"`.
6
+ *
7
+ * This works correctly in both development mode (where source lives at backend/src/)
8
+ * and compiled/npm-installed mode (where code lives at dist/backend/backend/src/).
9
+ * The naive `path.resolve(__dirname, '../..')` approach breaks in compiled mode
10
+ * because the directory depth differs.
11
+ *
12
+ * @param startDir - Directory to start searching from (typically the caller's __dirname).
13
+ * @returns The absolute path to the package root directory.
14
+ * @throws Error if no matching package.json is found after reaching the filesystem root.
15
+ */
16
+ export function findPackageRoot(startDir) {
17
+ let current = path.resolve(startDir);
18
+ while (true) {
19
+ const pkgPath = path.join(current, 'package.json');
20
+ if (existsSync(pkgPath)) {
21
+ try {
22
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
23
+ if (pkg.name === 'crewly') {
24
+ return current;
25
+ }
26
+ }
27
+ catch {
28
+ // Malformed package.json — keep searching
29
+ }
30
+ }
31
+ const parent = path.dirname(current);
32
+ if (parent === current) {
33
+ throw new Error('Could not find Crewly package root (no package.json with name "crewly" found in any parent directory)');
34
+ }
35
+ current = parent;
36
+ }
37
+ }
38
+ //# sourceMappingURL=package-root.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-root.js","sourceRoot":"","sources":["../../../../../backend/src/utils/package-root.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC/C,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAErC,OAAO,IAAI,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;QACnD,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC;gBACJ,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;gBACvD,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAC3B,OAAO,OAAO,CAAC;gBAChB,CAAC;YACF,CAAC;YAAC,MAAM,CAAC;gBACR,0CAA0C;YAC3C,CAAC;QACF,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CACd,uGAAuG,CACvG,CAAC;QACH,CAAC;QACD,OAAO,GAAG,MAAM,CAAC;IAClB,CAAC;AACF,CAAC"}
@@ -6,22 +6,52 @@
6
6
  * crewly cloud status — Show current connection status
7
7
  * crewly cloud logout — Disconnect from CrewlyAI Cloud
8
8
  *
9
- * The login flow supports three modes:
10
- * 1. Direct token login via --token flag
11
- * 2. Browser-based OAuth: starts a local HTTP callback server, opens the
12
- * backend Google OAuth URL, receives the token via redirect, persists
13
- * credentials to ~/.crewly/cloud/config.json, then calls POST /api/cloud/connect.
14
- * 3. Mobile/no-browser login via --no-browser flag: prints a URL for the user
15
- * to open on their phone, which displays the token after OAuth for manual copy-paste.
9
+ * Login modes:
10
+ * 1. **Device pairing (default)**: asks Crewly Cloud for a pairing, prints a
11
+ * link + short code (`crewlyai.com/cloud/pair?code=ABCD-2345`) the owner
12
+ * opens on any device — usually their phone — and approves. The CLI polls
13
+ * and receives the token pair by itself: nobody copies a token, and nobody
14
+ * has to be at this machine. Credentials are saved to
15
+ * `$CREWLY_HOME/cloud/config.json` and `POST /api/cloud/connect` is called.
16
+ * 2. `--token <token>`: direct token login.
17
+ * 3. `--web`: the old localhost-callback flow (local callback server + Google
18
+ * OAuth in this machine's browser).
19
+ * 4. `--paste`: the old copy-paste flow (sign in on the portal's token page,
20
+ * paste the token and refresh token here).
16
21
  *
17
22
  * @module cli/commands/cloud
18
23
  */
19
24
  import http from 'http';
25
+ import { startCloudDevicePairing, type DevicePairingOutcome, type WaitForApprovalOptions } from '../../../backend/src/services/cloud/cloud-device-pairing.client.js';
26
+ /**
27
+ * Directory for cloud credentials — `$CREWLY_HOME/cloud` (default
28
+ * `~/.crewly/cloud`). Resolved per call so `CREWLY_HOME` set after import
29
+ * (tests, isolated profiles) is honoured; it must match the backend's
30
+ * `CloudClientService` path so both read the same file.
31
+ *
32
+ * @returns Absolute directory path
33
+ */
34
+ export declare function getCloudConfigDir(): string;
35
+ /**
36
+ * Path to the cloud credentials file.
37
+ *
38
+ * @returns Absolute file path
39
+ */
40
+ export declare function getCloudConfigFile(): string;
20
41
  /** Options for the login subcommand */
21
- interface LoginOptions {
42
+ export interface LoginOptions {
22
43
  token?: string;
23
44
  /** Commander.js sets this to false when --no-browser is passed */
24
45
  browser?: boolean;
46
+ /** `--web`: old flow — localhost callback server + Google OAuth in this machine's browser */
47
+ web?: boolean;
48
+ /** `--paste`: old flow — sign in on the portal's token page and paste the tokens here */
49
+ paste?: boolean;
50
+ }
51
+ /** Injectable pairing client (tests). */
52
+ export interface LoginDeps {
53
+ startPairing: typeof startCloudDevicePairing;
54
+ waitForApproval: (options: WaitForApprovalOptions) => Promise<DevicePairingOutcome>;
25
55
  }
26
56
  /**
27
57
  * Shape of the credentials saved to config.json.
@@ -37,15 +67,7 @@ interface CloudCredentials {
37
67
  connectedAt: string;
38
68
  }
39
69
  /**
40
- * Save cloud credentials to ~/.crewly/cloud/config.json.
41
- *
42
- * Creates the directory hierarchy if it does not exist.
43
- *
44
- * @param token - JWT access token
45
- * @param refreshToken - Optional refresh token
46
- */
47
- /**
48
- * Save cloud credentials to ~/.crewly/cloud/config.json.
70
+ * Save cloud credentials to $CREWLY_HOME/cloud/config.json.
49
71
  *
50
72
  * Creates the directory hierarchy if it does not exist. The format matches
51
73
  * PersistedCloudConfig so the backend's loadPersistedConfig() can read it.
@@ -55,7 +77,7 @@ interface CloudCredentials {
55
77
  */
56
78
  export declare function saveCloudCredentials(token: string, refreshToken?: string): void;
57
79
  /**
58
- * Load cloud credentials from ~/.crewly/cloud/config.json.
80
+ * Load cloud credentials from $CREWLY_HOME/cloud/config.json.
59
81
  *
60
82
  * @returns The saved credentials, or null if the file does not exist or is invalid
61
83
  */
@@ -109,15 +131,13 @@ export declare function promptForToken(prompt: string): Promise<string>;
109
131
  /**
110
132
  * Handle the `crewly cloud login` subcommand.
111
133
  *
112
- * Supports three login modes:
113
- * 1. `--token <token>`: saves the token and calls POST /api/cloud/connect directly.
114
- * 2. `--no-browser`: prints a URL for mobile login, waits for user to paste token.
115
- * 3. Default: starts a local HTTP callback server, opens the browser to the
116
- * backend OAuth URL, waits for the callback, saves credentials.
134
+ * Modes (see the module comment): device pairing by default; `--token`,
135
+ * `--web` and `--paste` keep the older flows.
117
136
  *
118
- * @param options - Login command options (optional --token, --no-browser)
137
+ * @param options - Login command options
138
+ * @param deps - Pairing client (tests)
119
139
  */
120
- export declare function loginCommand(options: LoginOptions): Promise<void>;
140
+ export declare function loginCommand(options: LoginOptions, deps?: LoginDeps): Promise<void>;
121
141
  /**
122
142
  * Handle the `crewly cloud status` subcommand.
123
143
  *
@@ -1 +1 @@
1
- {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../../../../cli/src/commands/cloud.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,IAAI,MAAM,MAAM,CAAC;AAiDxB,uCAAuC;AACvC,UAAU,YAAY;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAcD;;;;;;;GAOG;AACH;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAyB/E;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,gBAAgB,GAAG,IAAI,CAU9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,OAAO,CA0BxC;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAG7C;AAiCD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,IAAI;IACrC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChE,CAsDA;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY9D;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA+DvE;AA4GD;;;;;GAKG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAqCnD;AAMD;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAkBnD"}
1
+ {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../../../../cli/src/commands/cloud.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,IAAI,MAAM,MAAM,CAAC;AASxB,OAAO,EACL,uBAAuB,EAEvB,KAAK,oBAAoB,EAEzB,KAAK,sBAAsB,EAC5B,MAAM,oEAAoE,CAAC;AAuC5E;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAMD,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,6FAA6F;IAC7F,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,yFAAyF;IACzF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,yCAAyC;AACzC,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,OAAO,uBAAuB,CAAC;IAC7C,eAAe,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACrF;AAQD;;;;;GAKG;AACH,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAcD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CA2B/E;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,gBAAgB,GAAG,IAAI,CAW9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,OAAO,CA0BxC;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAG7C;AAiCD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,IAAI;IACrC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChE,CAsDA;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY9D;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,GAAE,SAA8B,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB7G;AA2OD;;;;;GAKG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAqCnD;AAMD;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAkBnD"}
@@ -6,13 +6,18 @@
6
6
  * crewly cloud status — Show current connection status
7
7
  * crewly cloud logout — Disconnect from CrewlyAI Cloud
8
8
  *
9
- * The login flow supports three modes:
10
- * 1. Direct token login via --token flag
11
- * 2. Browser-based OAuth: starts a local HTTP callback server, opens the
12
- * backend Google OAuth URL, receives the token via redirect, persists
13
- * credentials to ~/.crewly/cloud/config.json, then calls POST /api/cloud/connect.
14
- * 3. Mobile/no-browser login via --no-browser flag: prints a URL for the user
15
- * to open on their phone, which displays the token after OAuth for manual copy-paste.
9
+ * Login modes:
10
+ * 1. **Device pairing (default)**: asks Crewly Cloud for a pairing, prints a
11
+ * link + short code (`crewlyai.com/cloud/pair?code=ABCD-2345`) the owner
12
+ * opens on any device — usually their phone — and approves. The CLI polls
13
+ * and receives the token pair by itself: nobody copies a token, and nobody
14
+ * has to be at this machine. Credentials are saved to
15
+ * `$CREWLY_HOME/cloud/config.json` and `POST /api/cloud/connect` is called.
16
+ * 2. `--token <token>`: direct token login.
17
+ * 3. `--web`: the old localhost-callback flow (local callback server + Google
18
+ * OAuth in this machine's browser).
19
+ * 4. `--paste`: the old copy-paste flow (sign in on the portal's token page,
20
+ * paste the token and refresh token here).
16
21
  *
17
22
  * @module cli/commands/cloud
18
23
  */
@@ -23,8 +28,11 @@ import readline from 'readline';
23
28
  import { exec } from 'child_process';
24
29
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
25
30
  import { join } from 'path';
26
- import { homedir, platform } from 'os';
31
+ import { hostname, platform } from 'os';
27
32
  import { DEFAULT_WEB_PORT } from '../constants.js';
33
+ import { CLOUD_DEVICE_PAIRING_CONSTANTS } from '../../../config/constants.js';
34
+ import { getCrewlyHomePath } from '../../../backend/src/services/core/crewly-home.utils.js';
35
+ import { startCloudDevicePairing, waitForCloudDeviceApproval, } from '../../../backend/src/services/cloud/cloud-device-pairing.client.js';
28
36
  // ---------------------------------------------------------------------------
29
37
  // Constants
30
38
  // ---------------------------------------------------------------------------
@@ -46,23 +54,39 @@ const CLOUD_CONSOLE_URL = process.env['CLOUD_CONSOLE_URL'] || 'https://crewlyai.
46
54
  const CLI_TOKEN_PAGE_PATH = '/cloud/cli-token';
47
55
  /** Timeout for backend API requests (ms) */
48
56
  const API_TIMEOUT_MS = 15_000;
49
- /** Directory for cloud credentials */
50
- const CLOUD_CONFIG_DIR = join(homedir(), '.crewly', 'cloud');
51
- /** Path to cloud config file */
52
- const CLOUD_CONFIG_FILE = join(CLOUD_CONFIG_DIR, 'config.json');
53
- // ---------------------------------------------------------------------------
54
- // Helpers
55
- // ---------------------------------------------------------------------------
57
+ /** Sub-directory of the Crewly home that holds cloud credentials */
58
+ const CLOUD_CONFIG_SUBDIR = 'cloud';
59
+ /** Cloud credentials file name */
60
+ const CLOUD_CONFIG_FILENAME = 'config.json';
56
61
  /**
57
- * Save cloud credentials to ~/.crewly/cloud/config.json.
62
+ * Directory for cloud credentials — `$CREWLY_HOME/cloud` (default
63
+ * `~/.crewly/cloud`). Resolved per call so `CREWLY_HOME` set after import
64
+ * (tests, isolated profiles) is honoured; it must match the backend's
65
+ * `CloudClientService` path so both read the same file.
58
66
  *
59
- * Creates the directory hierarchy if it does not exist.
67
+ * @returns Absolute directory path
68
+ */
69
+ export function getCloudConfigDir() {
70
+ return join(getCrewlyHomePath(), CLOUD_CONFIG_SUBDIR);
71
+ }
72
+ /**
73
+ * Path to the cloud credentials file.
60
74
  *
61
- * @param token - JWT access token
62
- * @param refreshToken - Optional refresh token
75
+ * @returns Absolute file path
63
76
  */
77
+ export function getCloudConfigFile() {
78
+ return join(getCloudConfigDir(), CLOUD_CONFIG_FILENAME);
79
+ }
80
+ /** The real pairing client. */
81
+ const DEFAULT_LOGIN_DEPS = {
82
+ startPairing: startCloudDevicePairing,
83
+ waitForApproval: waitForCloudDeviceApproval,
84
+ };
85
+ // ---------------------------------------------------------------------------
86
+ // Helpers
87
+ // ---------------------------------------------------------------------------
64
88
  /**
65
- * Save cloud credentials to ~/.crewly/cloud/config.json.
89
+ * Save cloud credentials to $CREWLY_HOME/cloud/config.json.
66
90
  *
67
91
  * Creates the directory hierarchy if it does not exist. The format matches
68
92
  * PersistedCloudConfig so the backend's loadPersistedConfig() can read it.
@@ -71,14 +95,16 @@ const CLOUD_CONFIG_FILE = join(CLOUD_CONFIG_DIR, 'config.json');
71
95
  * @param refreshToken - Optional refresh token for auto-renewal
72
96
  */
73
97
  export function saveCloudCredentials(token, refreshToken) {
74
- if (!existsSync(CLOUD_CONFIG_DIR)) {
75
- mkdirSync(CLOUD_CONFIG_DIR, { recursive: true });
98
+ const configDir = getCloudConfigDir();
99
+ const configFile = getCloudConfigFile();
100
+ if (!existsSync(configDir)) {
101
+ mkdirSync(configDir, { recursive: true });
76
102
  }
77
103
  // Merge with existing config to preserve fields the backend may have written
78
104
  let existing = {};
79
105
  try {
80
- if (existsSync(CLOUD_CONFIG_FILE)) {
81
- existing = JSON.parse(readFileSync(CLOUD_CONFIG_FILE, 'utf-8'));
106
+ if (existsSync(configFile)) {
107
+ existing = JSON.parse(readFileSync(configFile, 'utf-8'));
82
108
  }
83
109
  }
84
110
  catch {
@@ -93,19 +119,20 @@ export function saveCloudCredentials(token, refreshToken) {
93
119
  // Preserve existing refreshToken if new one not provided
94
120
  ...(!refreshToken && existing.refreshToken && { refreshToken: existing.refreshToken }),
95
121
  };
96
- writeFileSync(CLOUD_CONFIG_FILE, JSON.stringify(credentials, null, 2), 'utf-8');
122
+ writeFileSync(configFile, JSON.stringify(credentials, null, 2), 'utf-8');
97
123
  }
98
124
  /**
99
- * Load cloud credentials from ~/.crewly/cloud/config.json.
125
+ * Load cloud credentials from $CREWLY_HOME/cloud/config.json.
100
126
  *
101
127
  * @returns The saved credentials, or null if the file does not exist or is invalid
102
128
  */
103
129
  export function loadCloudCredentials() {
104
- if (!existsSync(CLOUD_CONFIG_FILE)) {
130
+ const configFile = getCloudConfigFile();
131
+ if (!existsSync(configFile)) {
105
132
  return null;
106
133
  }
107
134
  try {
108
- const raw = readFileSync(CLOUD_CONFIG_FILE, 'utf-8');
135
+ const raw = readFileSync(configFile, 'utf-8');
109
136
  return JSON.parse(raw);
110
137
  }
111
138
  catch {
@@ -270,35 +297,114 @@ export function promptForToken(prompt) {
270
297
  /**
271
298
  * Handle the `crewly cloud login` subcommand.
272
299
  *
273
- * Supports three login modes:
274
- * 1. `--token <token>`: saves the token and calls POST /api/cloud/connect directly.
275
- * 2. `--no-browser`: prints a URL for mobile login, waits for user to paste token.
276
- * 3. Default: starts a local HTTP callback server, opens the browser to the
277
- * backend OAuth URL, waits for the callback, saves credentials.
300
+ * Modes (see the module comment): device pairing by default; `--token`,
301
+ * `--web` and `--paste` keep the older flows.
278
302
  *
279
- * @param options - Login command options (optional --token, --no-browser)
303
+ * @param options - Login command options
304
+ * @param deps - Pairing client (tests)
280
305
  */
281
- export async function loginCommand(options) {
306
+ export async function loginCommand(options, deps = DEFAULT_LOGIN_DEPS) {
282
307
  if (options.token) {
283
308
  // Direct token login
284
309
  console.log(chalk.blue('Logging in with provided token...'));
285
310
  await connectWithToken(options.token);
286
311
  return;
287
312
  }
288
- // Auto-detect: use mobile flow if --no-browser was passed OR environment
289
- // cannot open a browser (SSH, Docker, CI, headless Linux).
290
- const useBrowser = options.browser !== false && canOpenBrowser();
291
- if (!useBrowser) {
292
- if (options.browser !== false) {
293
- // Auto-detected headless — inform the user
294
- console.log(chalk.yellow(' Headless environment detected — using manual token flow.'));
295
- }
313
+ if (options.paste) {
296
314
  await mobileLoginFlow();
297
315
  return;
298
316
  }
299
- // Browser-based OAuth flow — redirects to crewlyai.com (Cloud), NOT local backend.
300
- // The Cloud server handles Google OAuth and redirects back to our local callback
301
- // with token + refreshToken as query parameters.
317
+ if (options.web) {
318
+ if (options.browser === false || !canOpenBrowser()) {
319
+ console.log(chalk.red(' ✗ --web needs a browser on this machine. Run `crewly cloud login` to approve from your phone instead.'));
320
+ process.exit(1);
321
+ }
322
+ await browserLoginFlow();
323
+ return;
324
+ }
325
+ await deviceLoginFlow(options, deps);
326
+ }
327
+ /**
328
+ * Print a pairing's link and code where the owner can use them from any device.
329
+ *
330
+ * @param started - The pairing
331
+ */
332
+ function printPairingInstructions(started) {
333
+ console.log(chalk.white(' On your phone or any browser, open:'));
334
+ console.log('');
335
+ console.log(chalk.cyan(` ${started.verificationUrl}`));
336
+ console.log('');
337
+ console.log(chalk.white(` and check the code matches: ${chalk.bold(started.userCode)}`));
338
+ if (started.verificationUri) {
339
+ console.log(chalk.gray(` (or go to ${started.verificationUri} and type the code)`));
340
+ }
341
+ console.log('');
342
+ console.log(chalk.gray(` The link expires in ${Math.round(started.expiresIn / 60)} minutes.`));
343
+ }
344
+ /**
345
+ * Default login: device-code pairing.
346
+ *
347
+ * Prints a link + code, optionally opens the link locally, and waits while
348
+ * the owner approves from wherever they are. On approval the token pair
349
+ * arrives here by itself and is saved + connected like any other login.
350
+ *
351
+ * @param options - Login options (`--no-browser` stops the local browser from opening)
352
+ * @param deps - Pairing client
353
+ */
354
+ async function deviceLoginFlow(options, deps) {
355
+ console.log('');
356
+ console.log(chalk.blue('CrewlyAI Cloud Login'));
357
+ console.log(chalk.gray('─'.repeat(40)));
358
+ console.log('');
359
+ let started;
360
+ try {
361
+ started = await deps.startPairing(CLOUD_API_URL, {
362
+ deviceName: hostname(),
363
+ purpose: CLOUD_DEVICE_PAIRING_CONSTANTS.PURPOSES.CLI,
364
+ });
365
+ }
366
+ catch (error) {
367
+ const msg = error instanceof Error ? error.message : String(error);
368
+ console.log(chalk.red(` ✗ Could not start login: ${msg}`));
369
+ console.log(chalk.gray(' Other ways in: crewly cloud login --paste | crewly cloud login --token <token>'));
370
+ process.exit(1);
371
+ }
372
+ printPairingInstructions(started);
373
+ if (options.browser !== false && canOpenBrowser()) {
374
+ openBrowser(started.verificationUrl);
375
+ }
376
+ console.log('');
377
+ console.log(chalk.gray(' Waiting for approval… (Ctrl+C to cancel)'));
378
+ let outcome;
379
+ try {
380
+ outcome = await deps.waitForApproval({ cloudUrl: CLOUD_API_URL, start: started });
381
+ }
382
+ catch (error) {
383
+ const msg = error instanceof Error ? error.message : String(error);
384
+ console.log(chalk.red(` ✗ Login failed: ${msg}`));
385
+ process.exit(1);
386
+ }
387
+ if (outcome.status !== 'approved') {
388
+ const reason = outcome.status === 'denied'
389
+ ? 'The request was denied on crewlyai.com.'
390
+ : outcome.status === 'expired'
391
+ ? 'The link expired before it was approved.'
392
+ : 'Login cancelled.';
393
+ console.log(chalk.red(` ✗ ${reason}`));
394
+ console.log(chalk.gray(' Run `crewly cloud login` again for a new link.'));
395
+ process.exit(1);
396
+ }
397
+ const { credentials } = outcome;
398
+ console.log(chalk.green(` ✓ Approved${credentials.email ? ` by ${credentials.email}` : ''}`));
399
+ await connectWithToken(credentials.token, credentials.refreshToken);
400
+ }
401
+ /**
402
+ * `--web`: the localhost-callback flow.
403
+ *
404
+ * Starts a local HTTP callback server, opens Google OAuth on Crewly Cloud in
405
+ * this machine's browser and waits for the redirect back with the tokens.
406
+ */
407
+ async function browserLoginFlow() {
302
408
  console.log('');
303
409
  console.log(chalk.blue('CrewlyAI Cloud Login'));
304
410
  console.log(chalk.gray('─'.repeat(40)));
@@ -413,7 +519,7 @@ async function connectWithToken(token, refreshToken) {
413
519
  catch (error) {
414
520
  if (axios.isAxiosError(error) && error.code === 'ECONNREFUSED') {
415
521
  // Backend not running — this is OK, credentials are saved
416
- console.log(chalk.green(' ✓ Credentials saved to ~/.crewly/cloud/config.json'));
522
+ console.log(chalk.green(` ✓ Credentials saved to ${getCloudConfigFile()}`));
417
523
  console.log(chalk.gray(' Backend not running — Cloud will connect automatically on next crewly start'));
418
524
  }
419
525
  else {