tide-commander 1.216.0 → 1.217.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 (122) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/assets/{AudioPlayer-Chu8I0tg.js → AudioPlayer-wYzUPbI7.js} +1 -1
  3. package/dist/assets/{BossLogsModal-DR0ejza7.js → BossLogsModal-CmZNDIpu.js} +1 -1
  4. package/dist/assets/{BossSpawnModal-LqXBwD2F.js → BossSpawnModal-DtVfSG5G.js} +1 -1
  5. package/dist/assets/{ControlsModal-BItvxY7x.js → ControlsModal-C0tQf4xy.js} +1 -1
  6. package/dist/assets/{DockerLogsModal-DfR1XadQ.js → DockerLogsModal-DL_eRO-m.js} +1 -1
  7. package/dist/assets/{EmbeddedEditor-dMOVrrnl.js → EmbeddedEditor-DAr7_QUH.js} +1 -1
  8. package/dist/assets/{FcStdViewer-CXDb_hmK.js → FcStdViewer-MDGJiz2T.js} +1 -1
  9. package/dist/assets/{GcodeViewer-BJGaDLF8.js → GcodeViewer-BHVydtrp.js} +1 -1
  10. package/dist/assets/{GlbViewer-CrLi0LWd.js → GlbViewer-Bwr_Tayz.js} +1 -1
  11. package/dist/assets/{GmailOAuthSetup-CzinZ6aZ.js → GmailOAuthSetup-D_uF2cyB.js} +1 -1
  12. package/dist/assets/{GoogleOAuthSetup-DhVaIj11.js → GoogleOAuthSetup-BCmzQl5e.js} +1 -1
  13. package/dist/assets/{IframeModal-NJJxvrBg.js → IframeModal-CQmCu_P4.js} +1 -1
  14. package/dist/assets/{IntegrationsPanel-DDht5-zS.js → IntegrationsPanel-BmRLC7dz.js} +2 -2
  15. package/dist/assets/{LogViewerModal-DobAgsRk.js → LogViewerModal-CrnaI78u.js} +1 -1
  16. package/dist/assets/{MonitoringModal-ugppuuGW.js → MonitoringModal-C6J4g3Jf.js} +1 -1
  17. package/dist/assets/{PM2LogsModal-C28LrhXo.js → PM2LogsModal-Ds3dRnuE.js} +1 -1
  18. package/dist/assets/{PluginsPanel-BrllpLPF.js → PluginsPanel-BWG2NKdJ.js} +1 -1
  19. package/dist/assets/{RestoreArchivedAreaModal-DUjvqOIA.js → RestoreArchivedAreaModal-BNccBE4Z.js} +1 -1
  20. package/dist/assets/{Scene2DCanvas-IHlXkbK5.js → Scene2DCanvas-CPvMvm9m.js} +1 -1
  21. package/dist/assets/{SceneManager-BzRqBmgj.js → SceneManager-lb8uiOCO.js} +1 -1
  22. package/dist/assets/{SkillsPanel-B7ePWL4w.js → SkillsPanel-BTO2Ow7t.js} +1 -1
  23. package/dist/assets/{SlackMultiInstanceSetup-bpmQ5yCA.js → SlackMultiInstanceSetup-Da_GWe5W.js} +1 -1
  24. package/dist/assets/{SpawnModal-Bu7udoPb.js → SpawnModal-JbFRYxZa.js} +1 -1
  25. package/dist/assets/{StatisticsModal-teyupFGW.js → StatisticsModal-ChSnL5yu.js} +1 -1
  26. package/dist/assets/{StlViewer-DTuw4IoN.js → StlViewer-BFr-I14v.js} +1 -1
  27. package/dist/assets/{SubordinateAssignmentModal-De7mzS6q.js → SubordinateAssignmentModal-DchdwFLE.js} +1 -1
  28. package/dist/assets/{ThreeAreaSelector-B4FdXGTQ.js → ThreeAreaSelector-D18G4udc.js} +1 -1
  29. package/dist/assets/{ThreeViewShortcuts-C50gwWcg.js → ThreeViewShortcuts-vNiMNFPJ.js} +1 -1
  30. package/dist/assets/{TriggerManagerPanel-NhdLnKIE.js → TriggerManagerPanel-DjpLl7cf.js} +1 -1
  31. package/dist/assets/{VideoPlayer-DR13OKH9.js → VideoPlayer-Drj9r9Dx.js} +1 -1
  32. package/dist/assets/{WorkflowEditorPanel-HR2fDtcS.js → WorkflowEditorPanel-DLwkYQ8M.js} +1 -1
  33. package/dist/assets/{abnfDiagram-VRR7QNED-Q73b9mfz.js → abnfDiagram-VRR7QNED-BVuUyMTx.js} +1 -1
  34. package/dist/assets/{arc-CVKFmkgP.js → arc-_phD079o.js} +1 -1
  35. package/dist/assets/{architectureDiagram-ZJ3FMSHR-Bbf_Qcjn.js → architectureDiagram-ZJ3FMSHR-B7fAK4WF.js} +1 -1
  36. package/dist/assets/{area-logos-gYv8s8T3.js → area-logos-RCA-q_Vf.js} +1 -1
  37. package/dist/assets/{blockDiagram-677ZJIJ3-DaheCa8j.js → blockDiagram-677ZJIJ3-C-OlY0-3.js} +1 -1
  38. package/dist/assets/{c4Diagram-LMCZKHZV-UZ1L_YBs.js → c4Diagram-LMCZKHZV-DrwXTZgK.js} +1 -1
  39. package/dist/assets/channel-BWg1ErwG.js +1 -0
  40. package/dist/assets/{chunk-2Q5K7J3B-D8YOZZA-.js → chunk-2Q5K7J3B-DCcJNg3d.js} +1 -1
  41. package/dist/assets/{chunk-32BRIVSS-BQmKdeF2.js → chunk-32BRIVSS-CZZkOSIs.js} +1 -1
  42. package/dist/assets/{chunk-5VM5RSS4-CT5m9OBR.js → chunk-5VM5RSS4-BCFrP-q5.js} +1 -1
  43. package/dist/assets/{chunk-EX3LRPZG-BwQ_dYOl.js → chunk-EX3LRPZG-Bjnq5732.js} +1 -1
  44. package/dist/assets/{chunk-JWPE2WC7-DVPlE4Zc.js → chunk-JWPE2WC7-CPjRN_WZ.js} +1 -1
  45. package/dist/assets/{chunk-MOJQB5TN-DGrSLKap.js → chunk-MOJQB5TN-B6jVWpSe.js} +1 -1
  46. package/dist/assets/{chunk-RYQCIY6F-BGpC2Lvw.js → chunk-RYQCIY6F-0QrHhHTV.js} +1 -1
  47. package/dist/assets/{chunk-V7JOEXUC-T5kHrm0m.js → chunk-V7JOEXUC-DwLSEIop.js} +1 -1
  48. package/dist/assets/{chunk-VR4S4FIN-DsjgCLci.js → chunk-VR4S4FIN-R-sa6MjG.js} +1 -1
  49. package/dist/assets/{chunk-XXDRQBXY-CeisNHDz.js → chunk-XXDRQBXY-FuYmcHPd.js} +1 -1
  50. package/dist/assets/classDiagram-OUVF2IWQ-Cdy8Clus.js +1 -0
  51. package/dist/assets/classDiagram-v2-EOCWNBFH-Cdy8Clus.js +1 -0
  52. package/dist/assets/{cose-bilkent-JH36ORCC-DGc7pmnL.js → cose-bilkent-JH36ORCC-CvD5NdQ_.js} +1 -1
  53. package/dist/assets/{cynefin-VYW2F7L2-B4WOzgxr.js → cynefin-VYW2F7L2-C8_VnooS.js} +1 -1
  54. package/dist/assets/{cynefinDiagram-TSTJHNR4-C_DBldqW.js → cynefinDiagram-TSTJHNR4-SD_F8ul6.js} +1 -1
  55. package/dist/assets/{dagre-VKFMJZFB-CLbdjSVi.js → dagre-VKFMJZFB-Cpeo3cpw.js} +1 -1
  56. package/dist/assets/{diagram-FQU43EPY-rFdDLhQC.js → diagram-FQU43EPY-DsBnlRvM.js} +1 -1
  57. package/dist/assets/{diagram-G47NLZAW-B7IY4xJj.js → diagram-G47NLZAW-BHMRe5db.js} +1 -1
  58. package/dist/assets/{diagram-NH7WQ7WH-Dffo88fG.js → diagram-NH7WQ7WH-D8Rm6qlg.js} +1 -1
  59. package/dist/assets/{diagram-OA4YK3LP-CI24r0oP.js → diagram-OA4YK3LP-C4Svb0Zw.js} +1 -1
  60. package/dist/assets/{diagram-WEI45ONY-CpYaSMOz.js → diagram-WEI45ONY-BbFXrI2b.js} +1 -1
  61. package/dist/assets/{ebnfDiagram-CCIWWBDH-CVE6IpE1.js → ebnfDiagram-CCIWWBDH-CCBonTYH.js} +1 -1
  62. package/dist/assets/{erDiagram-Q63AITRT-BfedFNid.js → erDiagram-Q63AITRT-CGGw9uIx.js} +1 -1
  63. package/dist/assets/{flowDiagram-23GEKE2U-BMSNxq_P.js → flowDiagram-23GEKE2U-DWxHXzyo.js} +1 -1
  64. package/dist/assets/{ganttDiagram-NO4QXBWP-CFmNsZuP.js → ganttDiagram-NO4QXBWP-D74TSIvX.js} +1 -1
  65. package/dist/assets/{gitGraphDiagram-IHSO6WYX-BPZCZyak.js → gitGraphDiagram-IHSO6WYX-Cwv5rBxw.js} +1 -1
  66. package/dist/assets/{index-CE74eZqz.js → index-6lLWOBQK.js} +1 -1
  67. package/dist/assets/{index-DAKI-8ry.js → index-B9rURqfo.js} +1 -1
  68. package/dist/assets/{index-DYtuoGp_.js → index-BMXP1Yd8.js} +3 -3
  69. package/dist/assets/{index-DbOvHwxP.js → index-BZztJf0b.js} +1 -1
  70. package/dist/assets/{index-BZPSXTAd.js → index-D-5PhYjN.js} +1 -1
  71. package/dist/assets/{index-CHl4bmrT.js → index-DOcVAWbZ.js} +1 -1
  72. package/dist/assets/{index-DjYjfA28.js → index-DW1gJ5Cs.js} +1 -1
  73. package/dist/assets/{index-DKoidOEn.js → index-DrIXfMcD.js} +1 -1
  74. package/dist/assets/{index-BRdYSHCt.js → index-s33Ke8oj.js} +2 -2
  75. package/dist/assets/{infoDiagram-FWYZ7A6U-i4zJwolP.js → infoDiagram-FWYZ7A6U-VSfMHDx9.js} +1 -1
  76. package/dist/assets/{ishikawaDiagram-FXEZZL3T-Mx_Omxks.js → ishikawaDiagram-FXEZZL3T-CgWr16Qr.js} +1 -1
  77. package/dist/assets/{journeyDiagram-5HDEW3XC-3Rokfnmf.js → journeyDiagram-5HDEW3XC-Bv4ioJyG.js} +1 -1
  78. package/dist/assets/{kanban-definition-HUTT4EX6-CEPx7BrA.js → kanban-definition-HUTT4EX6-DsUoZOQz.js} +1 -1
  79. package/dist/assets/{linear-DNXvsxYb.js → linear-_3oDVafz.js} +1 -1
  80. package/dist/assets/{main-BBvM-_Ia.js → main-BKRg0DBA.js} +160 -160
  81. package/dist/assets/{mermaid.core-0qDPsI0T.js → mermaid.core-spKo10MG.js} +4 -4
  82. package/dist/assets/{mindmap-definition-LN4V7U3C-DwCHykip.js → mindmap-definition-LN4V7U3C-C6uCJ4IN.js} +1 -1
  83. package/dist/assets/{pegDiagram-2B236MQR-DO7OR5bN.js → pegDiagram-2B236MQR-BTIBB-Uw.js} +1 -1
  84. package/dist/assets/{pieDiagram-ENE6RG2P--1xKfxMr.js → pieDiagram-ENE6RG2P-D7oP-ve7.js} +1 -1
  85. package/dist/assets/{quadrantDiagram-ABIIQ3AL-rBNhtAik.js → quadrantDiagram-ABIIQ3AL-BfmcdARc.js} +1 -1
  86. package/dist/assets/{railroadDiagram-RFXS5EU6-Bh0AsgK_.js → railroadDiagram-RFXS5EU6-CplgjUu4.js} +1 -1
  87. package/dist/assets/{requirementDiagram-TGXJPOKE-2oc4ih2c.js → requirementDiagram-TGXJPOKE-BiRTuy5B.js} +1 -1
  88. package/dist/assets/{sankeyDiagram-HTMAVEWB-B-ysEqL4.js → sankeyDiagram-HTMAVEWB-C81HtBjy.js} +1 -1
  89. package/dist/assets/{sequenceDiagram-DBY2YBRQ-6kNR0UNq.js → sequenceDiagram-DBY2YBRQ-DFhPtqpN.js} +1 -1
  90. package/dist/assets/{sizeCapture-X5ZJPWSS-C6PbumMD.js → sizeCapture-X5ZJPWSS-BkT4qJkY.js} +1 -1
  91. package/dist/assets/{stateDiagram-2N3HPSRC-CvLiUbMc.js → stateDiagram-2N3HPSRC-CKvn1pfo.js} +1 -1
  92. package/dist/assets/stateDiagram-v2-6OUMAXLB-blzdlH4v.js +1 -0
  93. package/dist/assets/{swimlanes-5IMT3BWC-dAgZbHOX.js → swimlanes-5IMT3BWC-D5WhCwkM.js} +2 -2
  94. package/dist/assets/swimlanesDiagram-G3AALYLV-BiOWb3s9.js +8 -0
  95. package/dist/assets/{timeline-definition-FHXFAJF6-B4uBF1kH.js → timeline-definition-FHXFAJF6-B-UTX_B8.js} +1 -1
  96. package/dist/assets/{vennDiagram-L72KCM5P-DmICWssw.js → vennDiagram-L72KCM5P-DelObiCK.js} +1 -1
  97. package/dist/assets/{wardleyDiagram-EHGQE667-CmKrH6Q5.js → wardleyDiagram-EHGQE667-DCWJdXij.js} +1 -1
  98. package/dist/assets/{web-DC2SNPaM.js → web-BOFBvPGv.js} +1 -1
  99. package/dist/assets/{web-eQSHkDHy.js → web-C2DocoQz.js} +1 -1
  100. package/dist/assets/{web-CnrzUv_G.js → web-qtcj3i0F.js} +1 -1
  101. package/dist/assets/{xychartDiagram-FW5EYKEG-B9wmI4CY.js → xychartDiagram-FW5EYKEG-CzbE79ZQ.js} +1 -1
  102. package/dist/build-info.json +1 -1
  103. package/dist/index.html +1 -1
  104. package/dist/src/packages/server/data/builtin-skills/image-generation.js +64 -0
  105. package/dist/src/packages/server/data/builtin-skills/index.js +2 -0
  106. package/dist/src/packages/server/integrations/slack/slack-client.js +3 -0
  107. package/dist/src/packages/server/integrations/slack/slack-instance.js +12 -0
  108. package/dist/src/packages/server/integrations/slack/slack-routes.js +19 -0
  109. package/dist/src/packages/server/integrations/slack/slack-skill.js +13 -1
  110. package/dist/src/packages/server/routes/files.js +4 -0
  111. package/dist/src/packages/server/routes/images.js +53 -0
  112. package/dist/src/packages/server/routes/index.js +2 -0
  113. package/dist/src/packages/server/services/image-service.js +364 -0
  114. package/dist/src/packages/server/services/index.js +1 -0
  115. package/dist/src/packages/shared/image-types.js +11 -0
  116. package/dist/src/packages/shared/types.js +1 -0
  117. package/package.json +1 -1
  118. package/dist/assets/channel-BgIK1LRk.js +0 -1
  119. package/dist/assets/classDiagram-OUVF2IWQ-DqGhAOej.js +0 -1
  120. package/dist/assets/classDiagram-v2-EOCWNBFH-DqGhAOej.js +0 -1
  121. package/dist/assets/stateDiagram-v2-6OUMAXLB-Ds464XsG.js +0 -1
  122. package/dist/assets/swimlanesDiagram-G3AALYLV-B1Qwmr49.js +0 -8
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Image Generation Service
3
+ *
4
+ * Wraps the OpenAI Images API (`/v1/images/generations`) so agents can turn a
5
+ * prompt into real files on disk through `/api/images/generate` instead of
6
+ * hand-rolling a curl with a raw API key in their shell history.
7
+ *
8
+ * Design constraints:
9
+ * - THE KEY NEVER LEAVES THE SERVER. It is resolved here (secrets store →
10
+ * environment → key file) and never accepted as a request field, so it
11
+ * cannot end up in an agent transcript, a log line or the exec history.
12
+ * - NEVER THROWS on a generation failure. Model refusals, HTTP errors and
13
+ * timeouts come back as `{ ok: false, error }` so the caller can report
14
+ * them as a normal result. Only programmer errors (bad arguments) throw.
15
+ * - Writes stay inside home / cwd / tmp — same containment rule the .http
16
+ * runner uses for the folders it will touch.
17
+ */
18
+ import * as fs from 'fs';
19
+ import * as fsp from 'fs/promises';
20
+ import * as os from 'os';
21
+ import * as path from 'path';
22
+ import { getDataDir } from '../data/index.js';
23
+ import { createLogger } from '../utils/logger.js';
24
+ import { getSecretByKey } from './secrets-service.js';
25
+ import { DEFAULT_IMAGE_MODEL, OPENAI_API_KEY_SECRET, } from '../../shared/image-types.js';
26
+ const log = createLogger('ImageService');
27
+ const OPENAI_IMAGES_URL = 'https://api.openai.com/v1/images/generations';
28
+ const DEFAULT_TIMEOUT_MS = 180_000;
29
+ const MAX_TIMEOUT_MS = 600_000;
30
+ const MAX_IMAGES = 10;
31
+ const MAX_PROMPT_CHARS = 32_000;
32
+ const FORMAT_EXTENSIONS = {
33
+ png: '.png',
34
+ jpeg: '.jpg',
35
+ webp: '.webp',
36
+ };
37
+ /** Path of the key file consulted as the last fallback. */
38
+ export function getKeyFilePath() {
39
+ return process.env.TIDE_OPENAI_KEY_FILE || path.join(os.homedir(), '.tide_openai_key');
40
+ }
41
+ /**
42
+ * Find the OpenAI API key. Secrets first (that's the one the user configured
43
+ * in the UI, so it should win over whatever the server process inherited),
44
+ * then `OPENAI_API_KEY`, then the key file.
45
+ */
46
+ export function resolveApiKey() {
47
+ const fromSecret = getSecretByKey(OPENAI_API_KEY_SECRET)?.value?.trim();
48
+ if (fromSecret)
49
+ return { key: fromSecret, source: 'secret' };
50
+ const fromEnv = process.env.OPENAI_API_KEY?.trim();
51
+ if (fromEnv)
52
+ return { key: fromEnv, source: 'env' };
53
+ try {
54
+ const fromFile = fs.readFileSync(getKeyFilePath(), 'utf-8').trim();
55
+ if (fromFile)
56
+ return { key: fromFile, source: 'file' };
57
+ }
58
+ catch { /* no key file — not an error, just unconfigured */ }
59
+ return { key: null, source: null };
60
+ }
61
+ /** `sk-proj-…f3a9` — enough to tell two keys apart, useless if leaked. */
62
+ export function maskKey(key) {
63
+ if (key.length <= 12)
64
+ return `${key.slice(0, 3)}…`;
65
+ return `${key.slice(0, 8)}…${key.slice(-4)}`;
66
+ }
67
+ /** Directory used when the caller does not pass `outputDir`. */
68
+ export function getDefaultOutputDir() {
69
+ return path.join(getDataDir(), 'generated-images');
70
+ }
71
+ export function getStatus() {
72
+ const { key, source } = resolveApiKey();
73
+ return {
74
+ configured: Boolean(key),
75
+ source: source ?? undefined,
76
+ maskedKey: key ? maskKey(key) : undefined,
77
+ defaultModel: DEFAULT_IMAGE_MODEL,
78
+ defaultOutputDir: getDefaultOutputDir(),
79
+ keyFilePath: getKeyFilePath(),
80
+ secretKey: OPENAI_API_KEY_SECRET,
81
+ };
82
+ }
83
+ // ============================================================================
84
+ // Output paths
85
+ // ============================================================================
86
+ function realpathOrResolve(target) {
87
+ try {
88
+ return fs.realpathSync(target);
89
+ }
90
+ catch {
91
+ return path.resolve(target);
92
+ }
93
+ }
94
+ /**
95
+ * Resolve `dir` through symlinks as far as it exists, keeping the
96
+ * not-yet-created tail. Without this a brand-new output directory could never
97
+ * be checked against the allowed roots (realpath throws on missing paths), and
98
+ * `/home/user/link-to-elsewhere/new-dir` would slip past the containment test.
99
+ */
100
+ function resolveThroughExistingAncestor(dir) {
101
+ const absolute = path.resolve(dir);
102
+ const tail = [];
103
+ let current = absolute;
104
+ while (!fs.existsSync(current)) {
105
+ const parent = path.dirname(current);
106
+ if (parent === current)
107
+ return absolute; // hit the fs root, nothing exists
108
+ tail.unshift(path.basename(current));
109
+ current = parent;
110
+ }
111
+ return path.join(realpathOrResolve(current), ...tail);
112
+ }
113
+ function containmentRoots() {
114
+ return [process.cwd(), os.homedir(), os.tmpdir()].filter(Boolean).map(realpathOrResolve);
115
+ }
116
+ /**
117
+ * True when `dir` sits inside the working directory, the home directory or the
118
+ * system temp dir — the only places we will write generated images.
119
+ */
120
+ export function isSafeOutputDir(dir) {
121
+ if (!dir)
122
+ return false;
123
+ const resolved = resolveThroughExistingAncestor(dir);
124
+ return containmentRoots().some((root) => {
125
+ const rel = path.relative(root, resolved);
126
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
127
+ });
128
+ }
129
+ /** Lowercase, dash-separated stem derived from the prompt. */
130
+ export function slugifyPrompt(prompt, maxLength = 48) {
131
+ const slug = prompt
132
+ .toLowerCase()
133
+ .normalize('NFD')
134
+ .replace(/[\u0300-\u036f]/g, '')
135
+ .replace(/[^a-z0-9]+/g, '-')
136
+ .replace(/^-+|-+$/g, '')
137
+ .slice(0, maxLength)
138
+ .replace(/-+$/g, '');
139
+ return slug || 'image';
140
+ }
141
+ /**
142
+ * Turn a caller-supplied filename into a safe basename stem: directory
143
+ * components, traversal and shell-hostile characters are stripped, and any
144
+ * image extension is dropped (the format decides the real one).
145
+ */
146
+ export function sanitizeFilenameStem(filename) {
147
+ const base = path.basename(filename.trim())
148
+ .replace(/\.(png|jpe?g|webp)$/i, '')
149
+ .replace(/[^A-Za-z0-9._-]+/g, '-')
150
+ .replace(/^[-.]+|-+$/g, '')
151
+ .slice(0, 120);
152
+ return base || 'image';
153
+ }
154
+ /**
155
+ * Build the file names for one run. A single image keeps the bare stem; a
156
+ * batch gets `-1`, `-2`, … so nothing overwrites itself. Existing files are
157
+ * never clobbered — a numeric suffix is appended instead.
158
+ */
159
+ export function buildOutputPaths(outputDir, stem, count, format, exists = fs.existsSync) {
160
+ const ext = FORMAT_EXTENSIONS[format];
161
+ const paths = [];
162
+ for (let i = 0; i < count; i++) {
163
+ const base = count > 1 ? `${stem}-${i + 1}` : stem;
164
+ let candidate = path.join(outputDir, `${base}${ext}`);
165
+ let dedupe = 2;
166
+ while (exists(candidate) || paths.includes(candidate)) {
167
+ candidate = path.join(outputDir, `${base}-${dedupe}${ext}`);
168
+ dedupe++;
169
+ }
170
+ paths.push(candidate);
171
+ }
172
+ return paths;
173
+ }
174
+ // ============================================================================
175
+ // Request payload
176
+ // ============================================================================
177
+ /** DALL·E models take a different parameter vocabulary than the gpt-image family. */
178
+ function isDalleModel(model) {
179
+ return model.startsWith('dall-e');
180
+ }
181
+ /**
182
+ * Translate our request body into the OpenAI payload for the target model.
183
+ * Exported for tests — the gpt-image / DALL·E split is easy to get wrong and
184
+ * the API rejects the whole call on one unknown field.
185
+ */
186
+ export function buildRequestPayload(opts) {
187
+ const model = opts.model?.trim() || DEFAULT_IMAGE_MODEL;
188
+ const n = Math.min(Math.max(Math.trunc(opts.n ?? 1) || 1, 1), MAX_IMAGES);
189
+ const payload = { model, prompt: opts.prompt, n };
190
+ if (opts.size)
191
+ payload.size = opts.size;
192
+ if (isDalleModel(model)) {
193
+ // DALL·E only knows standard/hd, and only returns base64 when asked to.
194
+ if (opts.quality) {
195
+ payload.quality = opts.quality === 'high' || opts.quality === 'hd' ? 'hd' : 'standard';
196
+ }
197
+ payload.response_format = 'b64_json';
198
+ return payload;
199
+ }
200
+ if (opts.quality)
201
+ payload.quality = opts.quality;
202
+ if (opts.background)
203
+ payload.background = opts.background;
204
+ if (opts.format)
205
+ payload.output_format = opts.format;
206
+ if (typeof opts.compression === 'number' && (opts.format === 'jpeg' || opts.format === 'webp')) {
207
+ payload.output_compression = Math.min(Math.max(Math.trunc(opts.compression), 0), 100);
208
+ }
209
+ return payload;
210
+ }
211
+ /** Readable one-liner for an OpenAI error body / HTTP status. */
212
+ function describeApiError(status, body) {
213
+ let message = '';
214
+ try {
215
+ const parsed = JSON.parse(body);
216
+ message = parsed.error?.message || '';
217
+ }
218
+ catch { /* non-JSON error body (proxy/gateway) — fall back to the text */ }
219
+ const detail = message || body.slice(0, 400).trim() || 'no response body';
220
+ if (status === 401)
221
+ return `OpenAI rejected the API key (401): ${detail}`;
222
+ if (status === 403)
223
+ return `OpenAI denied access (403) — the org may not be verified for this model: ${detail}`;
224
+ if (status === 429)
225
+ return `OpenAI rate limit or quota exceeded (429): ${detail}`;
226
+ return `OpenAI images API failed (${status}): ${detail}`;
227
+ }
228
+ async function fetchImageBytes(entry) {
229
+ if (entry.b64_json)
230
+ return Buffer.from(entry.b64_json, 'base64');
231
+ if (entry.url) {
232
+ // DALL·E can hand back a short-lived CDN URL instead of base64.
233
+ const res = await fetch(entry.url);
234
+ if (!res.ok)
235
+ return null;
236
+ return Buffer.from(await res.arrayBuffer());
237
+ }
238
+ return null;
239
+ }
240
+ /**
241
+ * Generate one or more images and write them to disk. Resolves with
242
+ * `ok: false` and a human-readable `error` for every failure mode except
243
+ * invalid arguments, which reject.
244
+ */
245
+ export async function generateImages(opts) {
246
+ const startedAt = Date.now();
247
+ const prompt = (opts.prompt || '').trim();
248
+ const model = opts.model?.trim() || DEFAULT_IMAGE_MODEL;
249
+ const format = opts.format || 'png';
250
+ const outputDir = path.resolve(opts.outputDir?.trim() || getDefaultOutputDir());
251
+ const n = Math.min(Math.max(Math.trunc(opts.n ?? 1) || 1, 1), MAX_IMAGES);
252
+ const fail = (error) => ({
253
+ ok: false,
254
+ model,
255
+ prompt,
256
+ outputDir,
257
+ images: [],
258
+ timeMs: Date.now() - startedAt,
259
+ size: opts.size,
260
+ quality: opts.quality,
261
+ error,
262
+ agentId: opts.agentId,
263
+ });
264
+ if (!prompt)
265
+ throw new Error('Missing required field: prompt');
266
+ if (prompt.length > MAX_PROMPT_CHARS) {
267
+ throw new Error(`Prompt is too long (${prompt.length} chars, max ${MAX_PROMPT_CHARS})`);
268
+ }
269
+ if (!FORMAT_EXTENSIONS[format])
270
+ throw new Error(`Unsupported format: ${format}`);
271
+ if (!isSafeOutputDir(outputDir)) {
272
+ throw new Error('Refusing to write images outside the home, working or temp directory.');
273
+ }
274
+ const { key } = resolveApiKey();
275
+ if (!key) {
276
+ return fail(`No OpenAI API key configured. Add a secret named ${OPENAI_API_KEY_SECRET} in Settings → Secrets, ` +
277
+ `set OPENAI_API_KEY in the server environment, or write the key to ${getKeyFilePath()}.`);
278
+ }
279
+ const timeoutMs = Math.min(Math.max(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, 1_000), MAX_TIMEOUT_MS);
280
+ const controller = new AbortController();
281
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
282
+ let parsed;
283
+ try {
284
+ const response = await fetch(OPENAI_IMAGES_URL, {
285
+ method: 'POST',
286
+ headers: {
287
+ Authorization: `Bearer ${key}`,
288
+ 'Content-Type': 'application/json',
289
+ },
290
+ body: JSON.stringify(buildRequestPayload({ ...opts, prompt, model, n, format })),
291
+ signal: controller.signal,
292
+ });
293
+ const text = await response.text();
294
+ if (!response.ok)
295
+ return fail(describeApiError(response.status, text));
296
+ parsed = JSON.parse(text);
297
+ }
298
+ catch (err) {
299
+ const message = err instanceof Error ? err.message : String(err);
300
+ if (controller.signal.aborted) {
301
+ return fail(`Image generation timed out after ${timeoutMs}ms`);
302
+ }
303
+ return fail(`Image generation request failed: ${message}`);
304
+ }
305
+ finally {
306
+ clearTimeout(timer);
307
+ }
308
+ const entries = parsed.data ?? [];
309
+ if (entries.length === 0)
310
+ return fail('OpenAI returned no images');
311
+ try {
312
+ await fsp.mkdir(outputDir, { recursive: true });
313
+ }
314
+ catch (err) {
315
+ return fail(`Could not create output directory ${outputDir}: ${err instanceof Error ? err.message : String(err)}`);
316
+ }
317
+ const stem = opts.filename ? sanitizeFilenameStem(opts.filename) : slugifyPrompt(prompt);
318
+ const targets = buildOutputPaths(outputDir, stem, entries.length, format);
319
+ const images = [];
320
+ for (let i = 0; i < entries.length; i++) {
321
+ let bytes;
322
+ try {
323
+ bytes = await fetchImageBytes(entries[i]);
324
+ }
325
+ catch (err) {
326
+ return fail(`Could not download generated image ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
327
+ }
328
+ if (!bytes || bytes.length === 0)
329
+ return fail(`OpenAI returned an empty image (index ${i})`);
330
+ try {
331
+ await fsp.writeFile(targets[i], bytes);
332
+ }
333
+ catch (err) {
334
+ return fail(`Could not write ${targets[i]}: ${err instanceof Error ? err.message : String(err)}`);
335
+ }
336
+ images.push({
337
+ path: targets[i],
338
+ filename: path.basename(targets[i]),
339
+ bytes: bytes.length,
340
+ format,
341
+ });
342
+ }
343
+ const timeMs = Date.now() - startedAt;
344
+ log.log(`Generated ${images.length} image(s) with ${model} in ${timeMs}ms → ${outputDir}`);
345
+ return {
346
+ ok: true,
347
+ model,
348
+ prompt,
349
+ revisedPrompt: entries[0]?.revised_prompt,
350
+ outputDir,
351
+ images,
352
+ timeMs,
353
+ size: opts.size,
354
+ quality: opts.quality,
355
+ usage: parsed.usage
356
+ ? {
357
+ totalTokens: parsed.usage.total_tokens,
358
+ inputTokens: parsed.usage.input_tokens,
359
+ outputTokens: parsed.usage.output_tokens,
360
+ }
361
+ : undefined,
362
+ agentId: opts.agentId,
363
+ };
364
+ }
@@ -27,4 +27,5 @@ export * as workflowChatService from './workflow-chat-service.js';
27
27
  export * as whatsappNotificationConfigService from './whatsapp-notification-config-service.js';
28
28
  export * as testRunnerService from './test-runner-service.js';
29
29
  export * as httpRequestsService from './http-requests-service.js';
30
+ export * as imageService from './image-service.js';
30
31
  export { cadService } from './cad-service.js';
@@ -0,0 +1,11 @@
1
+ // ============================================================================
2
+ // Image Generation (OpenAI Images API)
3
+ // ============================================================================
4
+ // Shared types for `/api/images/*`: agents describe an image in a prompt and
5
+ // Tide Commander writes the rendered PNG/JPEG/WebP files into a directory on
6
+ // disk. The API key never travels over this API — it is resolved server-side
7
+ // from the secrets store, the environment, or a key file.
8
+ /** Secret key (Settings → Secrets) the server reads the OpenAI key from. */
9
+ export const OPENAI_API_KEY_SECRET = 'OPENAI_API_KEY';
10
+ /** Image model used when the caller does not pick one. */
11
+ export const DEFAULT_IMAGE_MODEL = 'gpt-image-1';
@@ -12,6 +12,7 @@ export * from './database-types.js';
12
12
  export * from './common-types.js';
13
13
  export * from './test-runner-types.js';
14
14
  export * from './http-requests-types.js';
15
+ export * from './image-types.js';
15
16
  export * from './plugin-types.js';
16
17
  export * from './websocket-messages.js';
17
18
  export * from './trigger-types.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.216.0",
3
+ "version": "1.217.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-0qDPsI0T.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-T5kHrm0m.js";import{_ as i}from"./mermaid.core-0qDPsI0T.js";import"./chunk-5VM5RSS4-CT5m9OBR.js";import"./chunk-XXDRQBXY-CeisNHDz.js";import"./chunk-VR4S4FIN-DsjgCLci.js";import"./chunk-32BRIVSS-BQmKdeF2.js";import"./main-BBvM-_Ia.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-DSCAlQQW.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-T5kHrm0m.js";import{_ as i}from"./mermaid.core-0qDPsI0T.js";import"./chunk-5VM5RSS4-CT5m9OBR.js";import"./chunk-XXDRQBXY-CeisNHDz.js";import"./chunk-VR4S4FIN-DsjgCLci.js";import"./chunk-32BRIVSS-BQmKdeF2.js";import"./main-BBvM-_Ia.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-DSCAlQQW.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
@@ -1 +0,0 @@
1
- import{s as r,b as e,a,S as s}from"./chunk-EX3LRPZG-BwQ_dYOl.js";import{_ as i}from"./mermaid.core-0qDPsI0T.js";import"./chunk-XXDRQBXY-CeisNHDz.js";import"./chunk-VR4S4FIN-DsjgCLci.js";import"./chunk-32BRIVSS-BQmKdeF2.js";import"./main-BBvM-_Ia.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-DSCAlQQW.js";var b={parser:a,get db(){return new s(2)},renderer:e,styles:r,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{b as diagram};
@@ -1,8 +0,0 @@
1
- import{c as r,s as e}from"./flowDiagram-23GEKE2U-BMSNxq_P.js";import{_ as a}from"./mermaid.core-0qDPsI0T.js";import"./chunk-5VM5RSS4-CT5m9OBR.js";import"./chunk-XXDRQBXY-CeisNHDz.js";import"./chunk-VR4S4FIN-DsjgCLci.js";import"./chunk-32BRIVSS-BQmKdeF2.js";import"./channel-BgIK1LRk.js";import"./main-BBvM-_Ia.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-DSCAlQQW.js";var o=a(t=>`${e(t)}
2
- .swimlane.cluster rect {
3
- stroke: ${t.clusterBorder} !important;
4
- }
5
- [data-look="neo"].cluster rect {
6
- filter: none;
7
- }
8
- `,"getStyles"),m=o,_=r({defaultLayout:"swimlane",styles:m});export{_ as diagram};