ima2-gen 1.1.9 → 1.1.11

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 (234) hide show
  1. package/README.md +10 -1
  2. package/bin/commands/annotate.js +5 -3
  3. package/bin/commands/annotate.ts +13 -12
  4. package/bin/commands/cancel.js +5 -2
  5. package/bin/commands/cancel.ts +6 -3
  6. package/bin/commands/canvas-versions.js +4 -4
  7. package/bin/commands/canvas-versions.ts +10 -10
  8. package/bin/commands/capabilities.js +90 -0
  9. package/bin/commands/capabilities.ts +93 -0
  10. package/bin/commands/cardnews.js +3 -1
  11. package/bin/commands/cardnews.ts +18 -17
  12. package/bin/commands/comfy.js +2 -2
  13. package/bin/commands/comfy.ts +4 -4
  14. package/bin/commands/config.js +11 -143
  15. package/bin/commands/config.ts +33 -147
  16. package/bin/commands/defaults.js +180 -0
  17. package/bin/commands/defaults.ts +192 -0
  18. package/bin/commands/edit.js +45 -9
  19. package/bin/commands/edit.ts +44 -10
  20. package/bin/commands/gen.js +55 -14
  21. package/bin/commands/gen.ts +56 -18
  22. package/bin/commands/history.js +2 -1
  23. package/bin/commands/history.ts +11 -10
  24. package/bin/commands/ls.js +10 -5
  25. package/bin/commands/ls.ts +15 -11
  26. package/bin/commands/metadata.js +4 -1
  27. package/bin/commands/metadata.ts +5 -2
  28. package/bin/commands/multimode.js +64 -7
  29. package/bin/commands/multimode.ts +59 -9
  30. package/bin/commands/node.js +18 -8
  31. package/bin/commands/node.ts +24 -15
  32. package/bin/commands/observability.js +7 -7
  33. package/bin/commands/observability.ts +21 -21
  34. package/bin/commands/ping.js +4 -2
  35. package/bin/commands/ping.ts +5 -3
  36. package/bin/commands/prompt.js +14 -11
  37. package/bin/commands/prompt.ts +40 -38
  38. package/bin/commands/ps.js +10 -7
  39. package/bin/commands/ps.ts +15 -12
  40. package/bin/commands/session.js +4 -3
  41. package/bin/commands/session.ts +23 -22
  42. package/bin/commands/show.js +5 -2
  43. package/bin/commands/show.ts +8 -5
  44. package/bin/commands/skill.js +62 -0
  45. package/bin/commands/skill.ts +70 -0
  46. package/bin/ima2.js +14 -5
  47. package/bin/ima2.ts +16 -7
  48. package/bin/lib/args.ts +20 -1
  49. package/bin/lib/client.ts +20 -7
  50. package/bin/lib/config-store.js +156 -0
  51. package/bin/lib/config-store.ts +163 -0
  52. package/bin/lib/error-hints.ts +3 -3
  53. package/bin/lib/files.ts +8 -8
  54. package/bin/lib/output.js +19 -17
  55. package/bin/lib/output.ts +38 -23
  56. package/bin/lib/platform.js +3 -1
  57. package/bin/lib/platform.ts +9 -7
  58. package/bin/lib/recover-output.js +104 -0
  59. package/bin/lib/recover-output.ts +139 -0
  60. package/bin/lib/star-prompt.ts +1 -1
  61. package/bin/lib/storage-doctor.ts +3 -2
  62. package/config.js +1 -1
  63. package/config.ts +8 -6
  64. package/docs/CLI.md +64 -8
  65. package/docs/migration/runtime-test-inventory.md +146 -0
  66. package/lib/assetLifecycle.ts +8 -8
  67. package/lib/canvasVersionStore.js +2 -0
  68. package/lib/canvasVersionStore.ts +54 -12
  69. package/lib/capabilities.js +70 -0
  70. package/lib/capabilities.ts +84 -0
  71. package/lib/cardNewsGenerator.js +13 -3
  72. package/lib/cardNewsGenerator.ts +123 -14
  73. package/lib/cardNewsJobStore.ts +47 -12
  74. package/lib/cardNewsManifestStore.js +13 -6
  75. package/lib/cardNewsManifestStore.ts +56 -14
  76. package/lib/cardNewsPlanner.js +11 -9
  77. package/lib/cardNewsPlanner.ts +86 -30
  78. package/lib/cardNewsPlannerClient.js +23 -10
  79. package/lib/cardNewsPlannerClient.ts +58 -17
  80. package/lib/cardNewsPlannerPrompt.js +2 -0
  81. package/lib/cardNewsPlannerPrompt.ts +2 -0
  82. package/lib/cardNewsPlannerSchema.js +43 -36
  83. package/lib/cardNewsPlannerSchema.ts +120 -58
  84. package/lib/cardNewsTemplateStore.js +20 -35
  85. package/lib/cardNewsTemplateStore.ts +100 -58
  86. package/lib/codexDetect.js +5 -3
  87. package/lib/codexDetect.ts +5 -3
  88. package/lib/comfyBridge.js +3 -1
  89. package/lib/comfyBridge.ts +37 -16
  90. package/lib/db.ts +5 -5
  91. package/lib/errInfo.js +32 -0
  92. package/lib/errInfo.ts +43 -0
  93. package/lib/errorClassify.ts +2 -2
  94. package/lib/generationCancel.js +18 -0
  95. package/lib/generationCancel.ts +28 -0
  96. package/lib/generationErrors.ts +37 -11
  97. package/lib/historyIndex.js +34 -0
  98. package/lib/historyIndex.ts +51 -0
  99. package/lib/historyList.js +10 -6
  100. package/lib/historyList.ts +17 -13
  101. package/lib/imageMetadata.js +1 -1
  102. package/lib/imageMetadata.ts +8 -8
  103. package/lib/imageMetadataStore.ts +6 -6
  104. package/lib/imageModels.ts +6 -4
  105. package/lib/inflight.js +32 -8
  106. package/lib/inflight.ts +93 -16
  107. package/lib/localImportStore.js +2 -0
  108. package/lib/localImportStore.ts +8 -5
  109. package/lib/logger.js +7 -5
  110. package/lib/logger.ts +34 -23
  111. package/lib/nodeStore.js +2 -0
  112. package/lib/nodeStore.ts +20 -10
  113. package/lib/oauthLauncher.js +2 -2
  114. package/lib/oauthLauncher.ts +7 -6
  115. package/lib/oauthNormalize.ts +1 -1
  116. package/lib/oauthProxy/errors.js +93 -0
  117. package/lib/oauthProxy/errors.ts +128 -0
  118. package/lib/oauthProxy/generators.js +426 -0
  119. package/lib/oauthProxy/generators.ts +494 -0
  120. package/lib/oauthProxy/index.js +8 -0
  121. package/lib/oauthProxy/index.ts +28 -0
  122. package/lib/oauthProxy/prompts.js +99 -0
  123. package/lib/oauthProxy/prompts.ts +123 -0
  124. package/lib/oauthProxy/references.js +32 -0
  125. package/lib/oauthProxy/references.ts +45 -0
  126. package/lib/oauthProxy/runtime.js +101 -0
  127. package/lib/oauthProxy/runtime.ts +115 -0
  128. package/lib/oauthProxy/streams.js +211 -0
  129. package/lib/oauthProxy/streams.ts +232 -0
  130. package/lib/oauthProxy/types.js +6 -0
  131. package/lib/oauthProxy/types.ts +9 -0
  132. package/lib/oauthProxy.js +3 -911
  133. package/lib/oauthProxy.ts +3 -995
  134. package/lib/openDirectory.js +5 -3
  135. package/lib/openDirectory.ts +9 -7
  136. package/lib/pngInfo.ts +2 -2
  137. package/lib/promptImport/curatedSources.ts +4 -2
  138. package/lib/promptImport/discoveryRegistry.js +17 -9
  139. package/lib/promptImport/discoveryRegistry.ts +121 -28
  140. package/lib/promptImport/errors.ts +6 -6
  141. package/lib/promptImport/githubDiscovery.js +13 -7
  142. package/lib/promptImport/githubDiscovery.ts +84 -23
  143. package/lib/promptImport/githubFolder.js +22 -14
  144. package/lib/promptImport/githubFolder.ts +130 -41
  145. package/lib/promptImport/githubSource.js +3 -1
  146. package/lib/promptImport/githubSource.ts +32 -14
  147. package/lib/promptImport/gptImageHints.ts +10 -8
  148. package/lib/promptImport/parsePromptCandidates.js +2 -0
  149. package/lib/promptImport/parsePromptCandidates.ts +43 -17
  150. package/lib/promptImport/promptIndex.js +33 -23
  151. package/lib/promptImport/promptIndex.ts +124 -46
  152. package/lib/promptImport/rankPromptCandidates.js +2 -2
  153. package/lib/promptImport/rankPromptCandidates.ts +22 -6
  154. package/lib/promptImport/types.js +1 -0
  155. package/lib/promptImport/types.ts +103 -0
  156. package/lib/promptSafetyPolicy.js +5 -0
  157. package/lib/promptSafetyPolicy.ts +5 -0
  158. package/lib/providerOptions.ts +3 -2
  159. package/lib/referenceImageCompress.ts +15 -6
  160. package/lib/refs.js +2 -0
  161. package/lib/refs.ts +27 -11
  162. package/lib/requestLogger.ts +4 -3
  163. package/lib/responsesImageAdapter.js +54 -17
  164. package/lib/responsesImageAdapter.ts +169 -36
  165. package/lib/runtimeContext.js +100 -0
  166. package/lib/runtimeContext.ts +131 -0
  167. package/lib/runtimePorts.js +2 -1
  168. package/lib/runtimePorts.ts +28 -16
  169. package/lib/sessionStore.js +7 -5
  170. package/lib/sessionStore.ts +73 -37
  171. package/lib/storageMigration.js +18 -11
  172. package/lib/storageMigration.ts +63 -37
  173. package/lib/styleSheet.js +7 -6
  174. package/lib/styleSheet.ts +34 -23
  175. package/lib/visibleTextLanguagePolicy.js +7 -0
  176. package/lib/visibleTextLanguagePolicy.ts +7 -0
  177. package/package.json +6 -3
  178. package/routes/annotations.js +9 -4
  179. package/routes/annotations.ts +35 -12
  180. package/routes/canvasVersions.js +8 -3
  181. package/routes/canvasVersions.ts +14 -9
  182. package/routes/capabilities.js +13 -0
  183. package/routes/capabilities.ts +18 -0
  184. package/routes/cardNews.js +31 -18
  185. package/routes/cardNews.ts +66 -38
  186. package/routes/comfy.js +6 -3
  187. package/routes/comfy.ts +10 -6
  188. package/routes/edit.js +38 -7
  189. package/routes/edit.ts +63 -12
  190. package/routes/generate.js +71 -23
  191. package/routes/generate.ts +85 -31
  192. package/routes/health.js +9 -6
  193. package/routes/health.ts +17 -13
  194. package/routes/history.js +85 -36
  195. package/routes/history.ts +112 -44
  196. package/routes/imageImport.js +6 -2
  197. package/routes/imageImport.ts +9 -5
  198. package/routes/index.js +5 -1
  199. package/routes/index.ts +6 -1
  200. package/routes/metadata.js +9 -4
  201. package/routes/metadata.ts +13 -7
  202. package/routes/multimode.js +161 -38
  203. package/routes/multimode.ts +216 -58
  204. package/routes/nodes.js +55 -26
  205. package/routes/nodes.ts +108 -40
  206. package/routes/promptImport.js +42 -36
  207. package/routes/promptImport.ts +89 -64
  208. package/routes/prompts.js +62 -39
  209. package/routes/prompts.ts +120 -71
  210. package/routes/sessions.js +46 -24
  211. package/routes/sessions.ts +60 -35
  212. package/routes/storage.js +4 -2
  213. package/routes/storage.ts +13 -5
  214. package/server.js +25 -21
  215. package/server.ts +57 -37
  216. package/skills/ima2/SKILL.md +206 -0
  217. package/ui/dist/.vite/manifest.json +11 -10
  218. package/ui/dist/assets/{CardNewsWorkspace-BJOCey7Z.js → CardNewsWorkspace-j4ULtNdk.js} +1 -1
  219. package/ui/dist/assets/NodeCanvas-Bc7BUViM.js +7 -0
  220. package/ui/dist/assets/{PromptImportDialog-Dqu1VpUh.js → PromptImportDialog-DBKprBEo.js} +2 -2
  221. package/ui/dist/assets/{PromptImportDiscoverySection-Dg8T9X0L.js → PromptImportDiscoverySection-m5v55Zsy.js} +1 -1
  222. package/ui/dist/assets/PromptImportFolderSection-DnPvJkfJ.js +1 -0
  223. package/ui/dist/assets/PromptLibraryPanel-BMSqfK9C.js +2 -0
  224. package/ui/dist/assets/SettingsWorkspace-Cj3LD0uu.js +1 -0
  225. package/ui/dist/assets/{index-Cvld7dUZ.js → index-9aOJKFI-.js} +1 -1
  226. package/ui/dist/assets/index-De-AWE6B.css +1 -0
  227. package/ui/dist/assets/index-tQhOLR-C.js +28 -0
  228. package/ui/dist/index.html +2 -2
  229. package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +0 -7
  230. package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +0 -1
  231. package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +0 -2
  232. package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +0 -1
  233. package/ui/dist/assets/index-C9cXwiWE.js +0 -25
  234. package/ui/dist/assets/index-CGMIkZXn.css +0 -1
@@ -18,7 +18,7 @@ const FLAGS = {
18
18
  help: { short: "h", type: "boolean" },
19
19
  };
20
20
 
21
- async function exportSub(argv) {
21
+ async function exportSub(argv: string[]) {
22
22
  const args = parseArgs(argv, { flags: FLAGS });
23
23
  const filename = args.positional[0];
24
24
  if (!filename) die(2, "filename required");
@@ -28,8 +28,8 @@ async function exportSub(argv) {
28
28
  const resp: any = await request(server.base, "/api/comfy/export-image", {
29
29
  method: "POST",
30
30
  body: { filename },
31
- }).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
32
- const target = args.out || `${filename}.workflow.json`;
31
+ }).catch((e: unknown) => { const err = e as { message?: string; code?: string }; die(exitCodeForError(e), `${err.message}${err.code ? ` (${err.code})` : ""}`); });
32
+ const target = String(args.out || `${filename}.workflow.json`);
33
33
  if (!args.force) {
34
34
  try {
35
35
  await access(target);
@@ -45,7 +45,7 @@ const SUB: Record<string, (argv: any[]) => Promise<void>> = {
45
45
  export: exportSub,
46
46
  };
47
47
 
48
- export default async function comfyCmd(argv) {
48
+ export default async function comfyCmd(argv: string[]) {
49
49
  const sub = argv[0];
50
50
  if (!sub || sub === "--help" || sub === "-h") { out(HELP); return; }
51
51
  const handler = SUB[sub];
@@ -1,10 +1,7 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
1
  import { createInterface } from "readline/promises";
3
2
  import { parseArgs } from "../lib/args.js";
4
3
  import { out, die, color, json } from "../lib/output.js";
5
- import { config as runtimeConfig } from "../../config.js";
6
- const CONFIG_FILE = runtimeConfig.storage.configFile;
7
- const CONFIG_DIR = runtimeConfig.storage.configDir;
4
+ import { CONFIG_FILE, buildEffectiveConfig, deleteNestedKey, displayPath, envOverrideForKey, getNestedKey, isAuthConfigKey, isSensitiveConfigKey, isWritableConfigKey, loadFileCfg, parseConfigValue, redactValue, restartNotice, saveFileCfg, setNestedKey, } from "../lib/config-store.js";
8
5
  const HELP = `
9
6
  ima2 config <subcommand> [options]
10
7
 
@@ -28,130 +25,6 @@ const FLAGS = {
28
25
  yes: { short: "y", type: "boolean" },
29
26
  help: { short: "h", type: "boolean" },
30
27
  };
31
- // Keys config set is allowed to write
32
- const KNOWN_KEYS = new Set([
33
- "imageModels.default",
34
- "imageModels.reasoningEffort",
35
- "log.level",
36
- "features.cardNews",
37
- "cardNewsPlanner.enabled",
38
- "cardNewsPlanner.model",
39
- "cardNewsPlanner.timeoutMs",
40
- "cardNewsPlanner.deterministicFallback",
41
- "comfy.defaultUrl",
42
- "comfy.uploadTimeoutMs",
43
- "comfy.maxUploadBytes",
44
- "storage.generatedDir",
45
- "storage.generatedDirName",
46
- "server.port",
47
- "server.host",
48
- "server.bodyLimit",
49
- "oauth.proxyPort",
50
- "oauth.statusTimeoutMs",
51
- "oauth.restartDelayMs",
52
- "limits.maxRefCount",
53
- "limits.maxParallel",
54
- "history.defaultPageSize",
55
- "history.maxPageCap",
56
- ]);
57
- // Auth keys live in the same file but must go through setup/login
58
- const AUTH_KEYS = new Set(["provider", "apiKey"]);
59
- const REDACT_PATTERN = /token|secret|apikey|password/i;
60
- const ALWAYS_REDACT = new Set(["provider", "apiKey", "oauth.token", "oauth.refreshToken"]);
61
- // Env var that overrides each writable key
62
- const KEY_TO_ENV = {
63
- "imageModels.default": "IMA2_IMAGE_MODEL_DEFAULT",
64
- "imageModels.reasoningEffort": "IMA2_REASONING_EFFORT",
65
- "log.level": "IMA2_LOG_LEVEL",
66
- "features.cardNews": "IMA2_CARD_NEWS",
67
- "server.port": "IMA2_PORT",
68
- "server.host": "IMA2_HOST",
69
- "server.bodyLimit": "IMA2_BODY_LIMIT",
70
- "oauth.proxyPort": "IMA2_OAUTH_PROXY_PORT",
71
- "storage.generatedDir": "IMA2_GENERATED_DIR",
72
- "cardNewsPlanner.enabled": "IMA2_CARD_NEWS_PLANNER",
73
- "cardNewsPlanner.model": "IMA2_CARD_NEWS_PLANNER_MODEL",
74
- "cardNewsPlanner.timeoutMs": "IMA2_CARD_NEWS_PLANNER_TIMEOUT_MS",
75
- "limits.maxParallel": "IMA2_MAX_PARALLEL",
76
- "limits.maxRefCount": "IMA2_MAX_REF_COUNT",
77
- "history.defaultPageSize": "IMA2_HISTORY_PAGE_SIZE",
78
- };
79
- function redactValue(key, value) {
80
- if (ALWAYS_REDACT.has(key) || REDACT_PATTERN.test(key)) {
81
- return value ? "<redacted>" : value;
82
- }
83
- return value;
84
- }
85
- function loadFileCfg() {
86
- if (!existsSync(CONFIG_FILE))
87
- return {};
88
- try {
89
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
90
- }
91
- catch {
92
- return {};
93
- }
94
- }
95
- function saveFileCfg(cfg) {
96
- mkdirSync(CONFIG_DIR, { recursive: true });
97
- writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
98
- }
99
- function getNestedKey(obj, dotKey) {
100
- const parts = dotKey.split(".");
101
- let cur = obj;
102
- for (const p of parts) {
103
- if (cur == null || typeof cur !== "object")
104
- return undefined;
105
- cur = cur[p];
106
- }
107
- return cur;
108
- }
109
- function setNestedKey(obj, dotKey, value) {
110
- const parts = dotKey.split(".");
111
- let cur = obj;
112
- for (let i = 0; i < parts.length - 1; i++) {
113
- if (cur[parts[i]] == null || typeof cur[parts[i]] !== "object")
114
- cur[parts[i]] = {};
115
- cur = cur[parts[i]];
116
- }
117
- cur[parts[parts.length - 1]] = value;
118
- }
119
- function deleteNestedKey(obj, dotKey) {
120
- const parts = dotKey.split(".");
121
- let cur = obj;
122
- for (let i = 0; i < parts.length - 1; i++) {
123
- if (cur == null || typeof cur !== "object")
124
- return false;
125
- cur = cur[parts[i]];
126
- }
127
- if (cur == null || typeof cur !== "object")
128
- return false;
129
- const last = parts[parts.length - 1];
130
- if (!(last in cur))
131
- return false;
132
- delete cur[last];
133
- return true;
134
- }
135
- function stripSets(v) {
136
- if (v instanceof Set)
137
- return [...v];
138
- if (Array.isArray(v))
139
- return v.map(stripSets);
140
- if (v && typeof v === "object") {
141
- const r = {};
142
- for (const [k, val] of Object.entries(v))
143
- r[k] = stripSets(val);
144
- return r;
145
- }
146
- return v;
147
- }
148
- function buildEffectiveConfig() {
149
- return stripSets(runtimeConfig);
150
- }
151
- function displayPath(p) {
152
- const home = process.env.HOME || "";
153
- return home && p.startsWith(home) ? p.replace(home, "~") : p;
154
- }
155
28
  async function pathSub(_argv) {
156
29
  out(CONFIG_FILE);
157
30
  }
@@ -198,26 +71,21 @@ async function setSub(argv) {
198
71
  const [key, rawValue] = args.positional;
199
72
  if (!key || rawValue === undefined)
200
73
  die(2, "usage: config set <key> <value>");
201
- if (AUTH_KEYS.has(key)) {
74
+ if (isAuthConfigKey(key)) {
202
75
  die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
203
76
  }
204
- if (!KNOWN_KEYS.has(key)) {
77
+ if (!isWritableConfigKey(key)) {
205
78
  die(2, `unknown config key: "${key}". Run 'ima2 config ls --effective' to see the config structure.`);
206
79
  }
207
- // Parse value: try JSON, fall back to raw string
208
- let value = rawValue;
209
- try {
210
- value = JSON.parse(rawValue);
211
- }
212
- catch { }
80
+ const value = parseConfigValue(rawValue);
213
81
  // Warn if env var is overriding this key
214
- const envVar = KEY_TO_ENV[key];
215
- if (envVar && process.env[envVar] !== undefined) {
216
- out(color.yellow(`warning: env ${envVar}=${process.env[envVar]} is currently overriding this value.`));
82
+ const override = envOverrideForKey(key);
83
+ if (override) {
84
+ out(color.yellow(`warning: env ${override.envVar}=${override.value} is currently overriding this value.`));
217
85
  out(`The file change will only apply after unsetting the env var and restarting the server.`);
218
86
  }
219
87
  // Confirm if writing a sensitive key
220
- if ((ALWAYS_REDACT.has(key) || REDACT_PATTERN.test(key)) && !args.yes) {
88
+ if (isSensitiveConfigKey(key) && !args.yes) {
221
89
  const rl = createInterface({ input: process.stdin, output: process.stdout });
222
90
  const ans = await rl.question(`warning: "${key}" is a sensitive credential. Write to config file? [y/N] `);
223
91
  rl.close();
@@ -230,14 +98,14 @@ async function setSub(argv) {
230
98
  setNestedKey(fileCfg, key, value);
231
99
  saveFileCfg(fileCfg);
232
100
  out(color.green("✓ ") + `wrote ${key}=${JSON.stringify(value)} to ${displayPath(CONFIG_FILE)}`);
233
- out(color.dim("note: server must be restarted to pick up config changes (run `ima2 serve`)"));
101
+ out(color.dim(restartNotice()));
234
102
  }
235
103
  async function rmSub(argv) {
236
104
  const args = parseArgs(argv, { flags: FLAGS });
237
105
  const key = args.positional[0];
238
106
  if (!key)
239
107
  die(2, "key required. Usage: config rm <key>");
240
- if (AUTH_KEYS.has(key)) {
108
+ if (isAuthConfigKey(key)) {
241
109
  die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
242
110
  }
243
111
  const fileCfg = loadFileCfg();
@@ -248,7 +116,7 @@ async function rmSub(argv) {
248
116
  }
249
117
  saveFileCfg(fileCfg);
250
118
  out(color.green("✓ ") + `removed ${key} from ${displayPath(CONFIG_FILE)}`);
251
- out(color.dim("note: server must be restarted to pick up config changes (run `ima2 serve`)"));
119
+ out(color.dim(restartNotice()));
252
120
  }
253
121
  const SUB = {
254
122
  path: pathSub,
@@ -1,11 +1,23 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
1
  import { createInterface } from "readline/promises";
3
2
  import { parseArgs } from "../lib/args.js";
4
3
  import { out, die, color, json } from "../lib/output.js";
5
- import { config as runtimeConfig } from "../../config.js";
6
-
7
- const CONFIG_FILE = runtimeConfig.storage.configFile;
8
- const CONFIG_DIR = runtimeConfig.storage.configDir;
4
+ import {
5
+ CONFIG_FILE,
6
+ buildEffectiveConfig,
7
+ deleteNestedKey,
8
+ displayPath,
9
+ envOverrideForKey,
10
+ getNestedKey,
11
+ isAuthConfigKey,
12
+ isSensitiveConfigKey,
13
+ isWritableConfigKey,
14
+ loadFileCfg,
15
+ parseConfigValue,
16
+ redactValue,
17
+ restartNotice,
18
+ saveFileCfg,
19
+ setNestedKey,
20
+ } from "../lib/config-store.js";
9
21
 
10
22
  const HELP = `
11
23
  ima2 config <subcommand> [options]
@@ -32,135 +44,11 @@ const FLAGS = {
32
44
  help: { short: "h", type: "boolean" },
33
45
  };
34
46
 
35
- // Keys config set is allowed to write
36
- const KNOWN_KEYS = new Set([
37
- "imageModels.default",
38
- "imageModels.reasoningEffort",
39
- "log.level",
40
- "features.cardNews",
41
- "cardNewsPlanner.enabled",
42
- "cardNewsPlanner.model",
43
- "cardNewsPlanner.timeoutMs",
44
- "cardNewsPlanner.deterministicFallback",
45
- "comfy.defaultUrl",
46
- "comfy.uploadTimeoutMs",
47
- "comfy.maxUploadBytes",
48
- "storage.generatedDir",
49
- "storage.generatedDirName",
50
- "server.port",
51
- "server.host",
52
- "server.bodyLimit",
53
- "oauth.proxyPort",
54
- "oauth.statusTimeoutMs",
55
- "oauth.restartDelayMs",
56
- "limits.maxRefCount",
57
- "limits.maxParallel",
58
- "history.defaultPageSize",
59
- "history.maxPageCap",
60
- ]);
61
-
62
- // Auth keys live in the same file but must go through setup/login
63
- const AUTH_KEYS = new Set(["provider", "apiKey"]);
64
-
65
- const REDACT_PATTERN = /token|secret|apikey|password/i;
66
- const ALWAYS_REDACT = new Set(["provider", "apiKey", "oauth.token", "oauth.refreshToken"]);
67
-
68
- // Env var that overrides each writable key
69
- const KEY_TO_ENV: Record<string, string> = {
70
- "imageModels.default": "IMA2_IMAGE_MODEL_DEFAULT",
71
- "imageModels.reasoningEffort": "IMA2_REASONING_EFFORT",
72
- "log.level": "IMA2_LOG_LEVEL",
73
- "features.cardNews": "IMA2_CARD_NEWS",
74
- "server.port": "IMA2_PORT",
75
- "server.host": "IMA2_HOST",
76
- "server.bodyLimit": "IMA2_BODY_LIMIT",
77
- "oauth.proxyPort": "IMA2_OAUTH_PROXY_PORT",
78
- "storage.generatedDir": "IMA2_GENERATED_DIR",
79
- "cardNewsPlanner.enabled": "IMA2_CARD_NEWS_PLANNER",
80
- "cardNewsPlanner.model": "IMA2_CARD_NEWS_PLANNER_MODEL",
81
- "cardNewsPlanner.timeoutMs": "IMA2_CARD_NEWS_PLANNER_TIMEOUT_MS",
82
- "limits.maxParallel": "IMA2_MAX_PARALLEL",
83
- "limits.maxRefCount": "IMA2_MAX_REF_COUNT",
84
- "history.defaultPageSize": "IMA2_HISTORY_PAGE_SIZE",
85
- };
86
-
87
- function redactValue(key: string, value: any): any {
88
- if (ALWAYS_REDACT.has(key) || REDACT_PATTERN.test(key)) {
89
- return value ? "<redacted>" : value;
90
- }
91
- return value;
92
- }
93
-
94
- function loadFileCfg(): Record<string, any> {
95
- if (!existsSync(CONFIG_FILE)) return {};
96
- try { return JSON.parse(readFileSync(CONFIG_FILE, "utf-8")); }
97
- catch { return {}; }
98
- }
99
-
100
- function saveFileCfg(cfg: Record<string, any>) {
101
- mkdirSync(CONFIG_DIR, { recursive: true });
102
- writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
103
- }
104
-
105
- function getNestedKey(obj: any, dotKey: string): any {
106
- const parts = dotKey.split(".");
107
- let cur = obj;
108
- for (const p of parts) {
109
- if (cur == null || typeof cur !== "object") return undefined;
110
- cur = cur[p];
111
- }
112
- return cur;
113
- }
114
-
115
- function setNestedKey(obj: any, dotKey: string, value: any): void {
116
- const parts = dotKey.split(".");
117
- let cur = obj;
118
- for (let i = 0; i < parts.length - 1; i++) {
119
- if (cur[parts[i]] == null || typeof cur[parts[i]] !== "object") cur[parts[i]] = {};
120
- cur = cur[parts[i]];
121
- }
122
- cur[parts[parts.length - 1]] = value;
123
- }
124
-
125
- function deleteNestedKey(obj: any, dotKey: string): boolean {
126
- const parts = dotKey.split(".");
127
- let cur = obj;
128
- for (let i = 0; i < parts.length - 1; i++) {
129
- if (cur == null || typeof cur !== "object") return false;
130
- cur = cur[parts[i]];
131
- }
132
- if (cur == null || typeof cur !== "object") return false;
133
- const last = parts[parts.length - 1];
134
- if (!(last in cur)) return false;
135
- delete cur[last];
136
- return true;
137
- }
138
-
139
- function stripSets(v: any): any {
140
- if (v instanceof Set) return [...v];
141
- if (Array.isArray(v)) return v.map(stripSets);
142
- if (v && typeof v === "object") {
143
- const r: any = {};
144
- for (const [k, val] of Object.entries(v)) r[k] = stripSets(val);
145
- return r;
146
- }
147
- return v;
148
- }
149
-
150
- function buildEffectiveConfig(): Record<string, any> {
151
- return stripSets(runtimeConfig);
152
- }
153
-
154
- function displayPath(p: string): string {
155
- const home = process.env.HOME || "";
156
- return home && p.startsWith(home) ? p.replace(home, "~") : p;
157
- }
158
-
159
- async function pathSub(_argv) {
47
+ async function pathSub(_argv: string[]) {
160
48
  out(CONFIG_FILE);
161
49
  }
162
50
 
163
- async function lsSub(argv) {
51
+ async function lsSub(argv: string[]) {
164
52
  const args = parseArgs(argv, { flags: FLAGS });
165
53
  if (args.effective) {
166
54
  const eff = buildEffectiveConfig();
@@ -173,7 +61,7 @@ async function lsSub(argv) {
173
61
  }
174
62
  }
175
63
 
176
- async function getSub(argv) {
64
+ async function getSub(argv: string[]) {
177
65
  const args = parseArgs(argv, { flags: FLAGS });
178
66
  const key = args.positional[0];
179
67
  if (!key) die(2, "key required. Usage: config get <dotted.key>");
@@ -188,31 +76,29 @@ async function getSub(argv) {
188
76
  }
189
77
  }
190
78
 
191
- async function setSub(argv) {
79
+ async function setSub(argv: string[]) {
192
80
  const args = parseArgs(argv, { flags: FLAGS });
193
81
  const [key, rawValue] = args.positional;
194
82
  if (!key || rawValue === undefined) die(2, "usage: config set <key> <value>");
195
83
 
196
- if (AUTH_KEYS.has(key)) {
84
+ if (isAuthConfigKey(key)) {
197
85
  die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
198
86
  }
199
- if (!KNOWN_KEYS.has(key)) {
87
+ if (!isWritableConfigKey(key)) {
200
88
  die(2, `unknown config key: "${key}". Run 'ima2 config ls --effective' to see the config structure.`);
201
89
  }
202
90
 
203
- // Parse value: try JSON, fall back to raw string
204
- let value: any = rawValue;
205
- try { value = JSON.parse(rawValue); } catch {}
91
+ const value = parseConfigValue(rawValue);
206
92
 
207
93
  // Warn if env var is overriding this key
208
- const envVar = KEY_TO_ENV[key];
209
- if (envVar && process.env[envVar] !== undefined) {
210
- out(color.yellow(`warning: env ${envVar}=${process.env[envVar]} is currently overriding this value.`));
94
+ const override = envOverrideForKey(key);
95
+ if (override) {
96
+ out(color.yellow(`warning: env ${override.envVar}=${override.value} is currently overriding this value.`));
211
97
  out(`The file change will only apply after unsetting the env var and restarting the server.`);
212
98
  }
213
99
 
214
100
  // Confirm if writing a sensitive key
215
- if ((ALWAYS_REDACT.has(key) || REDACT_PATTERN.test(key)) && !args.yes) {
101
+ if (isSensitiveConfigKey(key) && !args.yes) {
216
102
  const rl = createInterface({ input: process.stdin, output: process.stdout });
217
103
  const ans = await rl.question(`warning: "${key}" is a sensitive credential. Write to config file? [y/N] `);
218
104
  rl.close();
@@ -224,15 +110,15 @@ async function setSub(argv) {
224
110
  saveFileCfg(fileCfg);
225
111
 
226
112
  out(color.green("✓ ") + `wrote ${key}=${JSON.stringify(value)} to ${displayPath(CONFIG_FILE)}`);
227
- out(color.dim("note: server must be restarted to pick up config changes (run `ima2 serve`)"));
113
+ out(color.dim(restartNotice()));
228
114
  }
229
115
 
230
- async function rmSub(argv) {
116
+ async function rmSub(argv: string[]) {
231
117
  const args = parseArgs(argv, { flags: FLAGS });
232
118
  const key = args.positional[0];
233
119
  if (!key) die(2, "key required. Usage: config rm <key>");
234
120
 
235
- if (AUTH_KEYS.has(key)) {
121
+ if (isAuthConfigKey(key)) {
236
122
  die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
237
123
  }
238
124
 
@@ -244,7 +130,7 @@ async function rmSub(argv) {
244
130
  }
245
131
  saveFileCfg(fileCfg);
246
132
  out(color.green("✓ ") + `removed ${key} from ${displayPath(CONFIG_FILE)}`);
247
- out(color.dim("note: server must be restarted to pick up config changes (run `ima2 serve`)"));
133
+ out(color.dim(restartNotice()));
248
134
  }
249
135
 
250
136
  type Sub = (argv: any[]) => Promise<void>;
@@ -256,7 +142,7 @@ const SUB: Record<string, Sub> = {
256
142
  rm: rmSub,
257
143
  };
258
144
 
259
- export default async function configCmd(argv) {
145
+ export default async function configCmd(argv: string[]) {
260
146
  const sub = argv[0];
261
147
  if (!sub || sub === "--help" || sub === "-h") { out(HELP); return; }
262
148
  const handler = SUB[sub];
@@ -0,0 +1,180 @@
1
+ import { config } from "../../config.js";
2
+ import { parseArgs } from "../lib/args.js";
3
+ import { resolveServer, request } from "../lib/client.js";
4
+ import { buildEffectiveConfig, deleteNestedKey, displayPath, envOverrideForKey, getNestedKey, loadFileCfg, restartNotice, saveFileCfg, setNestedKey, CONFIG_FILE, } from "../lib/config-store.js";
5
+ import { color, die, dieWithError, json, out } from "../lib/output.js";
6
+ const MODEL_KEYS = ["imageModels.default", "apiProvider.defaultImageModel"];
7
+ const REASONING_KEYS = ["imageModels.reasoningEffort", "apiProvider.defaultReasoningEffort"];
8
+ const HELP = `
9
+ ima2 defaults [subcommand] [options]
10
+
11
+ Inspect or change persistent model/reasoning defaults.
12
+
13
+ Subcommands:
14
+ ls Show effective defaults
15
+ set model <model> Persist default model for OAuth and API paths
16
+ set reasoning <effort> Persist default reasoning effort for OAuth and API paths
17
+ reset model Remove persisted model defaults
18
+ reset reasoning Remove persisted reasoning defaults
19
+
20
+ Options:
21
+ --json Print JSON
22
+ --local Do not query running server
23
+ --server <url> Query a specific running server for ls/default output
24
+ `;
25
+ const FLAGS = {
26
+ json: { type: "boolean" },
27
+ local: { type: "boolean" },
28
+ server: { type: "string" },
29
+ help: { short: "h", type: "boolean" },
30
+ };
31
+ function localDefaults() {
32
+ const effective = buildEffectiveConfig();
33
+ return {
34
+ ok: true,
35
+ source: "local",
36
+ server: null,
37
+ defaults: {
38
+ oauth: {
39
+ model: getNestedKey(effective, "imageModels.default"),
40
+ reasoningEffort: getNestedKey(effective, "imageModels.reasoningEffort"),
41
+ },
42
+ api: {
43
+ model: getNestedKey(effective, "apiProvider.defaultImageModel"),
44
+ reasoningEffort: getNestedKey(effective, "apiProvider.defaultReasoningEffort"),
45
+ size: getNestedKey(effective, "apiProvider.defaultSize"),
46
+ webSearchEnabled: getNestedKey(effective, "apiProvider.allowWebSearch"),
47
+ },
48
+ },
49
+ };
50
+ }
51
+ async function readDefaults(args) {
52
+ if (args.local)
53
+ return localDefaults();
54
+ try {
55
+ const server = await resolveServer({ serverFlag: args.server });
56
+ const capabilities = await request(server.base, "/api/capabilities", { timeoutMs: 5000 });
57
+ return {
58
+ ok: true,
59
+ source: "server",
60
+ server: server.base,
61
+ defaults: capabilities.defaults,
62
+ };
63
+ }
64
+ catch (error) {
65
+ if (args.server)
66
+ throw error;
67
+ return localDefaults();
68
+ }
69
+ }
70
+ function printDefaults(payload) {
71
+ out(`ima2 defaults (${payload.source})`);
72
+ out(`server: ${payload.server || "none"}`);
73
+ out("");
74
+ out(`oauth model: ${payload.defaults?.oauth?.model}`);
75
+ out(`oauth reasoning: ${payload.defaults?.oauth?.reasoningEffort}`);
76
+ out(`api model: ${payload.defaults?.api?.model}`);
77
+ out(`api reasoning: ${payload.defaults?.api?.reasoningEffort}`);
78
+ if (payload.defaults?.api?.size)
79
+ out(`api size: ${payload.defaults.api.size}`);
80
+ if (payload.defaults?.api?.webSearchEnabled !== undefined) {
81
+ out(`api web search: ${payload.defaults.api.webSearchEnabled ? "enabled" : "disabled"}`);
82
+ }
83
+ }
84
+ function validateModel(value) {
85
+ if (!config.imageModels.valid.has(value)) {
86
+ die(2, `model must be one of: ${Array.from(config.imageModels.valid).join(", ")}`);
87
+ }
88
+ }
89
+ function validateReasoning(value) {
90
+ if (!config.imageModels.validReasoningEfforts.has(value)) {
91
+ die(2, `reasoning must be one of: ${Array.from(config.imageModels.validReasoningEfforts).join(", ")}`);
92
+ }
93
+ }
94
+ function warnOverrides(keys) {
95
+ for (const key of keys) {
96
+ const override = envOverrideForKey(key);
97
+ if (!override)
98
+ continue;
99
+ out(color.yellow(`warning: env ${override.envVar}=${override.value} is currently overriding ${key}.`));
100
+ }
101
+ }
102
+ function setDefaults(keys, value) {
103
+ const fileCfg = loadFileCfg();
104
+ for (const key of keys)
105
+ setNestedKey(fileCfg, key, value);
106
+ saveFileCfg(fileCfg);
107
+ warnOverrides(keys);
108
+ out(color.green("✓ ") + `wrote ${keys.join(", ")}=${JSON.stringify(value)} to ${displayPath(CONFIG_FILE)}`);
109
+ out(color.dim(restartNotice()));
110
+ }
111
+ function resetDefaults(keys) {
112
+ const fileCfg = loadFileCfg();
113
+ let changed = false;
114
+ for (const key of keys)
115
+ changed = deleteNestedKey(fileCfg, key) || changed;
116
+ if (!changed) {
117
+ out(color.dim(`(no persisted defaults found for ${keys.join(", ")})`));
118
+ return;
119
+ }
120
+ saveFileCfg(fileCfg);
121
+ out(color.green("✓ ") + `removed ${keys.join(", ")} from ${displayPath(CONFIG_FILE)}`);
122
+ out(color.dim(restartNotice()));
123
+ }
124
+ async function listSub(argv) {
125
+ const args = parseArgs(argv, { flags: FLAGS });
126
+ try {
127
+ const payload = await readDefaults(args);
128
+ if (args.json)
129
+ json(payload);
130
+ else
131
+ printDefaults(payload);
132
+ }
133
+ catch (error) {
134
+ dieWithError(error);
135
+ }
136
+ }
137
+ async function setSub(argv) {
138
+ const args = parseArgs(argv, { flags: FLAGS });
139
+ const [target, value] = args.positional;
140
+ if (!target || !value)
141
+ die(2, "usage: defaults set <model|reasoning> <value>");
142
+ if (target === "model") {
143
+ validateModel(value);
144
+ setDefaults(MODEL_KEYS, value);
145
+ return;
146
+ }
147
+ if (target === "reasoning") {
148
+ validateReasoning(value);
149
+ setDefaults(REASONING_KEYS, value);
150
+ return;
151
+ }
152
+ die(2, "target must be one of: model, reasoning");
153
+ }
154
+ async function resetSub(argv) {
155
+ const args = parseArgs(argv, { flags: FLAGS });
156
+ const target = args.positional[0];
157
+ if (target === "model") {
158
+ resetDefaults(MODEL_KEYS);
159
+ return;
160
+ }
161
+ if (target === "reasoning") {
162
+ resetDefaults(REASONING_KEYS);
163
+ return;
164
+ }
165
+ die(2, "usage: defaults reset <model|reasoning>");
166
+ }
167
+ export default async function defaultsCmd(argv) {
168
+ const sub = argv[0];
169
+ if (sub === "--help" || sub === "-h") {
170
+ out(HELP);
171
+ return;
172
+ }
173
+ if (!sub || sub === "ls" || sub.startsWith("-"))
174
+ return listSub(sub === "ls" ? argv.slice(1) : argv);
175
+ if (sub === "set")
176
+ return setSub(argv.slice(1));
177
+ if (sub === "reset")
178
+ return resetSub(argv.slice(1));
179
+ die(2, `unknown subcommand: ${sub}\n${HELP}`);
180
+ }