ima2-gen 2.0.2 → 2.0.3

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 (79) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/bin/commands/gen.js +7 -5
  3. package/bin/commands/multimode.js +7 -5
  4. package/bin/commands/node.js +2 -1
  5. package/config.js +8 -1
  6. package/docs/API.md +5 -4
  7. package/docs/CLI.md +4 -4
  8. package/docs/FAQ.ko.md +1 -1
  9. package/docs/FAQ.md +1 -1
  10. package/docs/migration/runtime-test-inventory.md +11 -1
  11. package/lib/agentCommandParser.js +10 -6
  12. package/lib/agentGenerationPlanner.js +96 -19
  13. package/lib/agentImageVideoGen.js +26 -6
  14. package/lib/agentPlannerModel.js +172 -0
  15. package/lib/agentQuestionResponder.js +9 -3
  16. package/lib/agentQueueStore.js +42 -0
  17. package/lib/agentQueueWorker.js +57 -7
  18. package/lib/agentRuntime.js +96 -12
  19. package/lib/agentSettings.js +9 -6
  20. package/lib/agentToolManifest.js +90 -0
  21. package/lib/agentTypes.js +1 -0
  22. package/lib/agyCli.js +31 -0
  23. package/lib/agyImageAdapter.js +73 -8
  24. package/lib/capabilities.js +7 -5
  25. package/lib/configKeys.js +6 -0
  26. package/lib/geminiApiImageAdapter.js +11 -4
  27. package/lib/generationRequestLog.js +25 -0
  28. package/lib/grokImageAdapter.js +27 -34
  29. package/lib/grokMultimodeAdapter.js +2 -1
  30. package/lib/grokVideoAdapter.js +3 -2
  31. package/lib/grokVideoPlannerPrompt.js +18 -25
  32. package/lib/inflight.js +1 -1
  33. package/lib/multimodeHelpers.js +3 -2
  34. package/lib/oauthProxy/generators.js +6 -5
  35. package/lib/oauthProxy/prompts.js +41 -16
  36. package/lib/oauthProxy/streams.js +1 -1
  37. package/lib/promptSafetyPolicy.js +1 -1
  38. package/lib/responsesDoctor.js +3 -3
  39. package/lib/responsesFallback.js +27 -14
  40. package/lib/responsesImageAdapter.js +11 -7
  41. package/node_modules/qs/CHANGELOG.md +10 -0
  42. package/node_modules/qs/README.md +1 -1
  43. package/node_modules/qs/dist/qs.js +15 -15
  44. package/node_modules/qs/eslint.config.mjs +1 -0
  45. package/node_modules/qs/lib/parse.js +52 -22
  46. package/node_modules/qs/lib/stringify.js +11 -4
  47. package/node_modules/qs/package.json +2 -2
  48. package/node_modules/qs/test/parse.js +49 -0
  49. package/node_modules/qs/test/stringify.js +129 -0
  50. package/package.json +1 -1
  51. package/routes/agent.js +19 -2
  52. package/routes/agy.js +5 -1
  53. package/routes/generate.js +12 -1
  54. package/routes/generationRequestLog.js +16 -0
  55. package/routes/index.js +2 -0
  56. package/routes/keys.js +22 -2
  57. package/routes/multimode.js +1 -1
  58. package/routes/nodes.js +5 -5
  59. package/ui/dist/.vite/manifest.json +22 -12
  60. package/ui/dist/assets/AgentWorkspace-qEMrATBP.js +3 -0
  61. package/ui/dist/assets/{CardNewsWorkspace-Dav3K5CT.js → CardNewsWorkspace-DoJnVHmn.js} +1 -1
  62. package/ui/dist/assets/GenerationRequestLogPanel-BDWRRLNV.js +1 -0
  63. package/ui/dist/assets/{NodeCanvas-C4ifFzB1.js → NodeCanvas-DIMo45sp.js} +3 -3
  64. package/ui/dist/assets/{PromptBuilderPanel-CEcyU9PL.js → PromptBuilderPanel-BF3lcSJD.js} +2 -2
  65. package/ui/dist/assets/{PromptImportDialog-CgQ94Gth.js → PromptImportDialog-Cy6ZymKG.js} +2 -2
  66. package/ui/dist/assets/{PromptImportDiscoverySection-CuzyzbNI.js → PromptImportDiscoverySection-CzuW-8P2.js} +1 -1
  67. package/ui/dist/assets/PromptImportFolderSection-DP5ywnsv.js +1 -0
  68. package/ui/dist/assets/{PromptLibraryPanel-BOe18we8.js → PromptLibraryPanel-CAsKr7CV.js} +2 -2
  69. package/ui/dist/assets/SettingsWorkspace-_PdPVsxi.js +1 -0
  70. package/ui/dist/assets/index-BUinlX2n.js +4 -0
  71. package/ui/dist/assets/index-CX3fge8X.css +1 -0
  72. package/ui/dist/assets/index-ygo6nfqx.js +23 -0
  73. package/ui/dist/index.html +2 -2
  74. package/ui/dist/assets/AgentWorkspace-Dth6YijN.js +0 -3
  75. package/ui/dist/assets/PromptImportFolderSection-DHLGlO6l.js +0 -1
  76. package/ui/dist/assets/SettingsWorkspace-Cdgnm4Wa.js +0 -1
  77. package/ui/dist/assets/index-C5PSahkr.js +0 -1
  78. package/ui/dist/assets/index-Dn2AhL6d.css +0 -1
  79. package/ui/dist/assets/index-Tjqx6wUV.js +0 -23
@@ -214,9 +214,12 @@ var parseObject = function (chain, val, options, valuesParsed) {
214
214
  return leaf;
215
215
  };
216
216
 
217
- var splitKeyIntoSegments = function splitKeyIntoSegments(givenKey, options) {
218
- var key = options.allowDots ? givenKey.replace(/\.([^.[]+)/g, '[$1]') : givenKey;
217
+ // Split a key like "a[b][c[]]" into ['a', '[b]', '[c[]]'] while preserving
218
+ // qs parse semantics for depth/prototype guards.
219
+ var splitKeyIntoSegments = function splitKeyIntoSegments(originalKey, options) {
220
+ var key = options.allowDots ? originalKey.replace(/\.([^.[]+)/g, '[$1]') : originalKey;
219
221
 
222
+ // depth <= 0 keeps the whole key as one segment
220
223
  if (options.depth <= 0) {
221
224
  if (!options.plainObjects && has.call(Object.prototype, key)) {
222
225
  if (!options.allowPrototypes) {
@@ -227,14 +230,11 @@ var splitKeyIntoSegments = function splitKeyIntoSegments(givenKey, options) {
227
230
  return [key];
228
231
  }
229
232
 
230
- var brackets = /(\[[^[\]]*])/;
231
- var child = /(\[[^[\]]*])/g;
232
-
233
- var segment = brackets.exec(key);
234
- var parent = segment ? key.slice(0, segment.index) : key;
235
-
236
- var keys = [];
233
+ var segments = [];
237
234
 
235
+ // parent before the first '[' (may be empty if key starts with '[')
236
+ var first = key.indexOf('[');
237
+ var parent = first >= 0 ? key.slice(0, first) : key;
238
238
  if (parent) {
239
239
  if (!options.plainObjects && has.call(Object.prototype, parent)) {
240
240
  if (!options.allowPrototypes) {
@@ -242,32 +242,62 @@ var splitKeyIntoSegments = function splitKeyIntoSegments(givenKey, options) {
242
242
  }
243
243
  }
244
244
 
245
- keys[keys.length] = parent;
245
+ segments[segments.length] = parent;
246
246
  }
247
247
 
248
- var i = 0;
249
- while ((segment = child.exec(key)) !== null && i < options.depth) {
250
- i += 1;
251
-
252
- var segmentContent = segment[1].slice(1, -1);
253
- if (!options.plainObjects && has.call(Object.prototype, segmentContent)) {
254
- if (!options.allowPrototypes) {
255
- return;
248
+ var n = key.length;
249
+ var open = first;
250
+ var collected = 0;
251
+
252
+ while (open >= 0 && collected < options.depth) {
253
+ var level = 1;
254
+ var i = open + 1;
255
+ var close = -1;
256
+
257
+ // balance nested '[' and ']' inside this bracket group using a nesting level counter
258
+ while (i < n && close < 0) {
259
+ var cu = key.charCodeAt(i);
260
+ if (cu === 0x5B) { // '['
261
+ level += 1;
262
+ } else if (cu === 0x5D) { // ']'
263
+ level -= 1;
264
+ if (level === 0) {
265
+ close = i; // found matching close; loop will exit by condition
266
+ }
256
267
  }
268
+ i += 1;
257
269
  }
258
270
 
259
- keys[keys.length] = segment[1];
271
+ if (close < 0) {
272
+ // Unterminated group: wrap the raw remainder in one bracket pair so it stays
273
+ // a single literal segment (e.g. "[[]b" -> "[[]b]"); we do not infer missing ']'.
274
+ segments[segments.length] = '[' + key.slice(open) + ']';
275
+ return segments;
276
+ }
277
+
278
+ var seg = key.slice(open, close + 1);
279
+ // prototype guard for the content of this group
280
+ var content = seg.slice(1, -1);
281
+ if (!options.plainObjects && has.call(Object.prototype, content) && !options.allowPrototypes) {
282
+ return;
283
+ }
284
+
285
+ segments[segments.length] = seg;
286
+ collected += 1;
287
+
288
+ // find the next '[' after this balanced group
289
+ open = key.indexOf('[', close + 1);
260
290
  }
261
291
 
262
- if (segment) {
292
+ if (open >= 0) {
263
293
  if (options.strictDepth === true) {
264
294
  throw new RangeError('Input depth exceeded depth option of ' + options.depth + ' and strictDepth is true');
265
295
  }
266
296
 
267
- keys[keys.length] = '[' + key.slice(segment.index) + ']';
297
+ segments[segments.length] = '[' + key.slice(open) + ']';
268
298
  }
269
299
 
270
- return keys;
300
+ return segments;
271
301
  };
272
302
 
273
303
  var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesParsed) {
@@ -118,7 +118,7 @@ var stringify = function stringify(
118
118
 
119
119
  if (obj === null) {
120
120
  if (strictNullHandling) {
121
- return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key', format) : prefix;
121
+ return formatter(encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key', format) : prefix);
122
122
  }
123
123
 
124
124
  obj = '';
@@ -142,7 +142,9 @@ var stringify = function stringify(
142
142
  if (generateArrayPrefix === 'comma' && isArray(obj)) {
143
143
  // we need to join elements in
144
144
  if (encodeValuesOnly && encoder) {
145
- obj = utils.maybeMap(obj, encoder);
145
+ obj = utils.maybeMap(obj, function (v) {
146
+ return v == null ? v : encoder(v);
147
+ });
146
148
  }
147
149
  objKeys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }];
148
150
  } else if (isArray(filter)) {
@@ -312,6 +314,11 @@ module.exports = function (object, opts) {
312
314
  var sideChannel = getSideChannel();
313
315
  for (var i = 0; i < objKeys.length; ++i) {
314
316
  var key = objKeys[i];
317
+
318
+ if (typeof key === 'undefined' || key === null) {
319
+ continue;
320
+ }
321
+
315
322
  var value = obj[key];
316
323
 
317
324
  if (options.skipNulls && value === null) {
@@ -345,10 +352,10 @@ module.exports = function (object, opts) {
345
352
  if (options.charsetSentinel) {
346
353
  if (options.charset === 'iso-8859-1') {
347
354
  // encodeURIComponent('&#10003;'), the "numeric entity" representation of a checkmark
348
- prefix += 'utf8=%26%2310003%3B&';
355
+ prefix += 'utf8=%26%2310003%3B' + options.delimiter;
349
356
  } else {
350
357
  // encodeURIComponent('✓')
351
- prefix += 'utf8=%E2%9C%93&';
358
+ prefix += 'utf8=%E2%9C%93' + options.delimiter;
352
359
  }
353
360
  }
354
361
 
@@ -2,7 +2,7 @@
2
2
  "name": "qs",
3
3
  "description": "A querystring parser that supports nesting and arrays, with a depth limit",
4
4
  "homepage": "https://github.com/ljharb/qs",
5
- "version": "6.15.1",
5
+ "version": "6.15.2",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/ljharb/qs.git"
@@ -36,7 +36,7 @@
36
36
  "devDependencies": {
37
37
  "@browserify/envify": "^6.0.0",
38
38
  "@browserify/uglifyify": "^6.0.0",
39
- "@ljharb/eslint-config": "^22.2.2",
39
+ "@ljharb/eslint-config": "^22.2.3",
40
40
  "browserify": "^16.5.2",
41
41
  "bundle-collapser": "^1.4.0",
42
42
  "common-shakeify": "~1.0.0",
@@ -210,6 +210,9 @@ test('parse()', function (t) {
210
210
  t.test('uses original key when depth = 0', function (st) {
211
211
  st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: 0 }), { 'a[0]': 'b', 'a[1]': 'c' });
212
212
  st.deepEqual(qs.parse('a[0][0]=b&a[0][1]=c&a[1]=d&e=2', { depth: 0 }), { 'a[0][0]': 'b', 'a[0][1]': 'c', 'a[1]': 'd', e: '2' });
213
+ st.deepEqual(qs.parse('a.b=c', { depth: 0, allowDots: true }), { 'a[b]': 'c' }, 'normalizes dots before applying depth-0 behavior');
214
+ st.deepEqual(qs.parse('toString=foo', { depth: 0 }), {}, 'respects prototype guard at depth 0');
215
+ st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' }, 'allows prototypes at depth 0 when enabled');
213
216
  st.end();
214
217
  });
215
218
 
@@ -263,6 +266,52 @@ test('parse()', function (t) {
263
266
  st.end();
264
267
  });
265
268
 
269
+ t.test('parses keys with literal [] inside a bracket group (#493)', function (st) {
270
+ // A bracket pair inside a bracket group should be treated literally as part of the key
271
+ st.deepEqual(
272
+ qs.parse('search[withbracket[]]=foobar'),
273
+ { search: { 'withbracket[]': 'foobar' } },
274
+ 'treats inner [] literally when inside a bracket group'
275
+ );
276
+
277
+ // Single-level variant
278
+ st.deepEqual(
279
+ qs.parse('a[b[]]=c'),
280
+ { a: { 'b[]': 'c' } },
281
+ 'keeps "b[]" as a literal key'
282
+ );
283
+
284
+ // Nested with an array push on the outer level
285
+ st.deepEqual(
286
+ qs.parse('list[][x[]]=y'),
287
+ { list: [{ 'x[]': 'y' }] },
288
+ 'preserves inner [] while still treating outer [] as array push'
289
+ );
290
+
291
+ // Multiple nested bracket pairs: inner [] remains literal as part of the key
292
+ st.deepEqual(
293
+ qs.parse('a[b[c[]]]=d'),
294
+ { a: { 'b[c[]]': 'd' } },
295
+ 'treats "b[c[]]" as a literal key inside the bracket group'
296
+ );
297
+
298
+ // Depth limits with literal brackets: preserve inner [] while limiting bracket-group parsing
299
+ st.deepEqual(
300
+ qs.parse('a[b[c[]]][d]=e', { depth: 1 }),
301
+ { a: { 'b[c[]]': { '[d]': 'e' } } },
302
+ 'respects depth: 1 and preserves literal inner [] in the parsed key'
303
+ );
304
+
305
+ // Unterminated inner bracket group is wrapped as a literal remainder segment
306
+ st.deepEqual(
307
+ qs.parse('a[[]b=c'),
308
+ { a: { '[[]b': 'c' } },
309
+ 'handles unterminated inner bracket groups without throwing'
310
+ );
311
+
312
+ st.end();
313
+ });
314
+
266
315
  t.test('allows to specify array indices', function (st) {
267
316
  st.deepEqual(qs.parse('a[1]=c&a[0]=b&a[2]=d'), { a: ['b', 'c', 'd'] });
268
317
  st.deepEqual(qs.parse('a[1]=c&a[0]=b'), { a: ['b', 'c'] });
@@ -651,6 +651,49 @@ test('stringify()', function (t) {
651
651
  st.end();
652
652
  });
653
653
 
654
+ t.test('does not crash on null/undefined entries in arrayFormat=comma with encodeValuesOnly', function (st) {
655
+ st.doesNotThrow(
656
+ function () { qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
657
+ 'does not pass a raw null array entry to the encoder'
658
+ );
659
+ st.doesNotThrow(
660
+ function () { qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
661
+ 'does not pass a raw undefined array entry to the encoder'
662
+ );
663
+ st.doesNotThrow(
664
+ function () { qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
665
+ 'does not crash on a single-null array'
666
+ );
667
+
668
+ st.equal(
669
+ qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
670
+ 'a=,b',
671
+ 'null entry joins as empty, comma stays unencoded under encodeValuesOnly'
672
+ );
673
+ st.equal(
674
+ qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
675
+ 'a=,b',
676
+ 'undefined entry joins as empty, comma stays unencoded under encodeValuesOnly'
677
+ );
678
+ st.equal(
679
+ qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
680
+ 'a=',
681
+ 'single-null array stringifies as empty value'
682
+ );
683
+ st.equal(
684
+ qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, strictNullHandling: true }),
685
+ 'a',
686
+ 'strictNullHandling drops the equals sign for a single-null array'
687
+ );
688
+ st.equal(
689
+ qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, skipNulls: true }),
690
+ '',
691
+ 'skipNulls drops a single-null array entirely'
692
+ );
693
+
694
+ st.end();
695
+ });
696
+
654
697
  t.test('stringifies a null object', { skip: !hasProto }, function (st) {
655
698
  st.equal(qs.stringify({ __proto__: null, a: 'b' }), 'a=b');
656
699
  st.end();
@@ -825,6 +868,35 @@ test('stringify()', function (t) {
825
868
  st.end();
826
869
  });
827
870
 
871
+ t.test('skips null/undefined entries in filter=array', function (st) {
872
+ st.doesNotThrow(
873
+ function () { qs.stringify({ a: 'b', undefined: 'x' }, { filter: ['a', undefined] }); },
874
+ 'does not pass a raw undefined filter entry to the encoder'
875
+ );
876
+ st.doesNotThrow(
877
+ function () { qs.stringify({ a: 'b', 'null': 'x' }, { filter: ['a', null] }); },
878
+ 'does not pass a raw null filter entry to the encoder'
879
+ );
880
+
881
+ st.equal(
882
+ qs.stringify({ a: 'b', undefined: 'x', c: 'd' }, { filter: ['a', undefined, 'c'] }),
883
+ 'a=b&c=d',
884
+ 'undefined filter entry is skipped, remaining keys are kept'
885
+ );
886
+ st.equal(
887
+ qs.stringify({ a: 'b', 'null': 'x', c: 'd' }, { filter: ['a', null, 'c'] }),
888
+ 'a=b&c=d',
889
+ 'null filter entry is skipped, remaining keys are kept'
890
+ );
891
+ st.equal(
892
+ qs.stringify({ a: 'b', 'null': 'x' }, { filter: [null] }),
893
+ '',
894
+ 'filter array containing only null yields empty string'
895
+ );
896
+
897
+ st.end();
898
+ });
899
+
828
900
  t.test('supports custom representations when filter=function', function (st) {
829
901
  var calls = 0;
830
902
  var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } };
@@ -1111,6 +1183,28 @@ test('stringify()', function (t) {
1111
1183
  st.end();
1112
1184
  });
1113
1185
 
1186
+ t.test('strictNullHandling: applies the formatter to the encoded key (RFC1738)', function (st) {
1187
+ st.equal(
1188
+ qs.stringify(
1189
+ { 'a b': null, 'c d': 'e f' },
1190
+ { strictNullHandling: false, format: 'RFC1738' }
1191
+ ),
1192
+ 'a+b=&c+d=e+f',
1193
+ 'without: as expected'
1194
+ );
1195
+
1196
+ st.equal(
1197
+ qs.stringify(
1198
+ { 'a b': null, 'c d': 'e f' },
1199
+ { strictNullHandling: true, format: 'RFC1738' }
1200
+ ),
1201
+ 'a+b&c+d=e+f',
1202
+ 'with: as expected'
1203
+ );
1204
+
1205
+ st.end();
1206
+ });
1207
+
1114
1208
  t.test('throws if an invalid charset is specified', function (st) {
1115
1209
  st['throws'](function () {
1116
1210
  qs.stringify({ a: 'b' }, { charset: 'foobar' });
@@ -1146,6 +1240,12 @@ test('stringify()', function (t) {
1146
1240
  'adds the right sentinel when instructed to and the charset is iso-8859-1'
1147
1241
  );
1148
1242
 
1243
+ st.equal(
1244
+ qs.stringify({ a: 1, b: 2 }, { charsetSentinel: true, delimiter: ';' }),
1245
+ 'utf8=%E2%9C%93;a=1;b=2',
1246
+ 'uses the configured delimiter after the sentinel'
1247
+ );
1248
+
1149
1249
  st.end();
1150
1250
  });
1151
1251
 
@@ -1316,4 +1416,33 @@ test('stringifies empty keys', function (t) {
1316
1416
 
1317
1417
  st.end();
1318
1418
  });
1419
+
1420
+ t.test('round-trips keys containing percent-encoded bracket text', function (st) {
1421
+ var cases = [
1422
+ { 'a%5Bb': 'c' },
1423
+ { 'a%5Db': 'c' },
1424
+ { 'a%255Bb': 'c' },
1425
+ { 'a%255Db': 'c' },
1426
+ { a: { 'b%5Bc': 'd' } },
1427
+ { a: { 'b%255Bc': 'd' } },
1428
+ { 'a%5B%255Bb': 'c' }
1429
+ ];
1430
+ for (var i = 0; i < cases.length; i++) {
1431
+ st.deepEqual(
1432
+ qs.parse(qs.stringify(cases[i])),
1433
+ cases[i],
1434
+ 'round-trips ' + JSON.stringify(cases[i])
1435
+ );
1436
+ }
1437
+
1438
+ st.end();
1439
+ });
1440
+
1441
+ t.test('parses input containing percent-encoded bracket text without mangling', function (st) {
1442
+ st.deepEqual(qs.parse('a%25255Bb=c'), { 'a%255Bb': 'c' }, 'a%25255Bb decodes to a%255Bb, not a%5Bb');
1443
+ st.deepEqual(qs.parse('a%25255Db=c'), { 'a%255Db': 'c' }, 'a%25255Db decodes to a%255Db, not a%5Db');
1444
+ st.deepEqual(qs.parse('a%5Bb%25255Bc%5D=d'), { a: { 'b%255Bc': 'd' } }, 'nested %25255B decodes to %255B inside segment, not %5B');
1445
+
1446
+ st.end();
1447
+ });
1319
1448
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Local OAuth image generation studio with classic and node workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/routes/agent.js CHANGED
@@ -1,5 +1,5 @@
1
- import { appendAgentTurn, compactAgentSession, createAgentSession, deleteAgentSession, getAgentGenerationSettings, getAgentSession, getAgentWorkspacePayload, renameAgentSession, setAgentCurrentImage, setAgentGenerationSettings, setAgentLocks, setAgentWebSearch, } from "../lib/agentStore.js";
2
- import { cancelAgentQueueItem, createAgentQueueItem, getAgentQueueItem, listAgentQueueItems, retryAgentQueueItem, } from "../lib/agentQueueStore.js";
1
+ import { appendAgentTurn, compactAgentSession, createAgentSession, deleteAgentSession, getAgentGenerationSettings, getAgentSession, getAgentWorkspacePayload, importAgentImage, renameAgentSession, setAgentCurrentImage, setAgentGenerationSettings, setAgentLocks, setAgentWebSearch, } from "../lib/agentStore.js";
2
+ import { cancelAgentQueueItem, createAgentQueueItem, getAgentGenerationErrors, getAgentQueueItem, listAgentQueueItems, retryAgentQueueItem, } from "../lib/agentQueueStore.js";
3
3
  import { ensureAgentQueueWorker, tickAgentQueueWorker } from "../lib/agentQueueWorker.js";
4
4
  import { parseAgentSlashCommand, formatAgentQuestionReply, formatAgentSlashHelp } from "../lib/agentCommandParser.js";
5
5
  import { requestAgentQuestionAnswer } from "../lib/agentQuestionResponder.js";
@@ -46,6 +46,11 @@ export function registerAgentRoutes(app, ctxRaw) {
46
46
  if (Object.prototype.hasOwnProperty.call(body, "generationSettings")) {
47
47
  setAgentGenerationSettings(req.params.sessionId, body.generationSettings);
48
48
  }
49
+ if (Object.prototype.hasOwnProperty.call(body, "currentImage")) {
50
+ const image = normalizeCurrentImage(body.currentImage);
51
+ if (image)
52
+ importAgentImage(req.params.sessionId, image);
53
+ }
49
54
  if (Object.prototype.hasOwnProperty.call(body, "currentImageId")) {
50
55
  const ok = setAgentCurrentImage(req.params.sessionId, body.currentImageId);
51
56
  if (!ok)
@@ -105,6 +110,18 @@ export function registerAgentRoutes(app, ctxRaw) {
105
110
  app.get("/api/agent/queue", (_req, res) => {
106
111
  res.json({ queue: listAgentQueueItems() });
107
112
  });
113
+ app.get("/api/agent/sessions/:sessionId/errors", (req, res) => {
114
+ try {
115
+ if (!getAgentSession(req.params.sessionId))
116
+ throw notFound(req.params.sessionId);
117
+ const limitRaw = Number(req.query.limit);
118
+ const limit = Number.isFinite(limitRaw) ? limitRaw : 10;
119
+ res.json({ errors: getAgentGenerationErrors(req.params.sessionId, limit) });
120
+ }
121
+ catch (error) {
122
+ sendError(res, error);
123
+ }
124
+ });
108
125
  app.get("/api/agent/sessions/:sessionId/queue", (req, res) => {
109
126
  if (!getAgentSession(req.params.sessionId))
110
127
  return sendError(res, notFound(req.params.sessionId));
package/routes/agy.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { buildAgyPathEnv, resolveAgyBin } from "../lib/agyCli.js";
2
3
  // Detect whether the Antigravity CLI (`agy`) is installed, using the same
3
4
  // spawn-and-catch style as lib/agyImageAdapter.ts (no shell `which`/`where`).
4
5
  // Login state cannot be probed — agy has no status command — so we only
@@ -13,7 +14,10 @@ function isAgyInstalled() {
13
14
  resolve(value);
14
15
  };
15
16
  try {
16
- const child = spawn("agy", ["--version"], { stdio: "ignore" });
17
+ const child = spawn(resolveAgyBin(), ["--version"], {
18
+ stdio: "ignore",
19
+ env: { ...process.env, PATH: buildAgyPathEnv() },
20
+ });
17
21
  child.on("error", () => done(false)); // ENOENT when not on PATH
18
22
  child.on("exit", (code) => done(code === 0));
19
23
  // Safety timeout so a hung binary never blocks the request.
@@ -5,6 +5,7 @@ import { randomBytes } from "crypto";
5
5
  import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
6
6
  import { generateImageThumbnailFromBuffer } from "../lib/imageThumb.js";
7
7
  import { classifyUpstreamError } from "../lib/errorClassify.js";
8
+ import { appendGenerationRequestLog } from "../lib/generationRequestLog.js";
8
9
  import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
9
10
  import { resolveProviderOptions } from "../lib/providerOptions.js";
10
11
  import { generateViaResponses } from "../lib/responsesImageAdapter.js";
@@ -83,7 +84,8 @@ export function registerGenerateRoutes(app, ctxRaw) {
83
84
  const moderationCheck = validateModeration(ctx, moderation);
84
85
  if (moderationCheck.error)
85
86
  return fail(400, { error: moderationCheck.error });
86
- const count = Math.min(Math.max(parseInt(n) || 1, 1), 8);
87
+ const maxGeneratedImages = Math.max(1, Math.trunc(Number(ctx.config.limits.maxGeneratedImages) || 1));
88
+ const count = Math.min(Math.max(parseInt(n) || 1, 1), maxGeneratedImages);
87
89
  const referencePayload = summarizeReferencePayload(references);
88
90
  const refCheckResult = validateAndNormalizeRefs(references);
89
91
  if (refCheckResult.error) {
@@ -469,6 +471,15 @@ export function registerGenerateRoutes(app, ctxRaw) {
469
471
  errorCode: finishErrorCode,
470
472
  meta: finishMeta,
471
473
  });
474
+ appendGenerationRequestLog(ctx.config.storage.generationRequestLogFile, {
475
+ id: randomBytes(8).toString("hex"),
476
+ requestId,
477
+ createdAt: Date.now(),
478
+ prompt: typeof req.body?.prompt === "string" ? req.body.prompt : "",
479
+ requested: parseInt(req.body?.n) || 1,
480
+ succeeded: finishStatus === "completed" ? (finishMeta.imageCount ?? 1) : 0,
481
+ error: finishStatus === "error" ? (finishErrorCode ?? "unknown") : null,
482
+ }).catch(() => { });
472
483
  }
473
484
  });
474
485
  }
@@ -0,0 +1,16 @@
1
+ import { listGenerationRequestLog } from "../lib/generationRequestLog.js";
2
+ import { requireRuntimeContext } from "../lib/runtimeContext.js";
3
+ export function registerGenerationRequestLogRoutes(app, ctxRaw) {
4
+ const ctx = requireRuntimeContext(ctxRaw);
5
+ app.get("/api/generation-requests", async (_req, res) => {
6
+ try {
7
+ const items = await listGenerationRequestLog(ctx.config.storage.generationRequestLogFile);
8
+ res.json({ items });
9
+ }
10
+ catch (error) {
11
+ res.status(500).json({
12
+ error: error instanceof Error ? error.message : "Could not read generation request log",
13
+ });
14
+ }
15
+ });
16
+ }
package/routes/index.js CHANGED
@@ -25,6 +25,7 @@ import { registerVideoExtendedRoutes } from "./videoExtended.js";
25
25
  import { registerQuotaRoutes } from "./quota.js";
26
26
  import { registerAuthRoutes } from "./auth.js";
27
27
  import { mountKeyRoutes } from "./keys.js";
28
+ import { registerGenerationRequestLogRoutes } from "./generationRequestLog.js";
28
29
  import { requireRuntimeContext } from "../lib/runtimeContext.js";
29
30
  export function configureRoutes(app, ctxRaw) {
30
31
  const ctx = requireRuntimeContext(ctxRaw);
@@ -56,4 +57,5 @@ export function configureRoutes(app, ctxRaw) {
56
57
  registerQuotaRoutes(app, ctx);
57
58
  registerAuthRoutes(app);
58
59
  mountKeyRoutes(app, ctx);
60
+ registerGenerationRequestLogRoutes(app, ctx);
59
61
  }
package/routes/keys.js CHANGED
@@ -62,8 +62,28 @@ export function mountKeyRoutes(app, ctx) {
62
62
  valid: !!vertexJson,
63
63
  maskedKey: ctx.vertexProjectId ? `project: ${ctx.vertexProjectId}` : null,
64
64
  };
65
+ status.geminiAuthMode = ctx.geminiAuthMode
66
+ || (vertexJson && !ctx.geminiApiKey ? "vertex" : "apikey");
65
67
  res.json(status);
66
68
  });
69
+ // Persist the Gemini auth mode chosen in the settings dropdown, so reopening
70
+ // settings (or restarting the server) keeps the user's selection.
71
+ app.put("/api/keys/gemini-auth-mode", async (req, res) => {
72
+ const { mode } = req.body;
73
+ if (mode !== "apikey" && mode !== "vertex") {
74
+ return res.status(400).json({ ok: false, error: "mode must be apikey|vertex", code: "INVALID_MODE" });
75
+ }
76
+ const cfgPath = ctx.config.storage.configFile;
77
+ let existing = {};
78
+ try {
79
+ existing = JSON.parse(await readFile(cfgPath, "utf-8"));
80
+ }
81
+ catch { /* new file */ }
82
+ existing.geminiAuthMode = mode;
83
+ await writeConfigAtomic(cfgPath, existing);
84
+ ctx.geminiAuthMode = mode;
85
+ return res.json({ ok: true, geminiAuthMode: mode });
86
+ });
67
87
  // Vertex JSON — dedicated route (before generic :provider)
68
88
  app.put("/api/keys/vertex", async (req, res) => {
69
89
  const { serviceAccountJson } = req.body;
@@ -187,6 +207,8 @@ export function mountKeyRoutes(app, ctx) {
187
207
  }
188
208
  catch { /* new file */ }
189
209
  existing[CONFIG_KEY_MAP[provider]] = trimmed;
210
+ if (provider === "gemini")
211
+ existing.geminiAuthMode = "apikey";
190
212
  await writeConfigAtomic(cfgPath, existing);
191
213
  // Hot-update runtime context
192
214
  if (provider === "openai") {
@@ -209,8 +231,6 @@ export function mountKeyRoutes(app, ctx) {
209
231
  ctx.geminiApiKeySource = "config";
210
232
  ctx.hasGeminiApiKey = true;
211
233
  ctx.geminiAuthMode = "apikey";
212
- existing[CONFIG_KEY_MAP[provider]] = trimmed;
213
- existing.geminiAuthMode = "apikey";
214
234
  }
215
235
  return res.json({ ok: true, provider, source: "config", valid: true });
216
236
  });
@@ -82,7 +82,7 @@ export function registerMultimodeRoutes(app, ctxRaw) {
82
82
  const { prompt, quality: rawQuality = "medium", size = "1024x1024", format = "png", moderation = "low", provider = "auto", references = [], mode: promptMode = "auto", model: rawModel, reasoningEffort: rawReasoningEffort, webSearchEnabled: rawWebSearchEnabled = true, } = req.body;
83
83
  const composerPrompt = normalizeComposerPrompt(req.body?.composerPrompt);
84
84
  const composerInsertedPrompts = normalizeComposerInsertedPrompts(req.body?.composerInsertedPrompts);
85
- const maxImages = normalizeMaxImages(req.body?.maxImages);
85
+ const maxImages = normalizeMaxImages(req.body?.maxImages, ctx.config.limits.maxGeneratedImages);
86
86
  const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
87
87
  const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
88
88
  const providerOptions = resolveProviderOptions(ctx, {
package/routes/nodes.js CHANGED
@@ -204,9 +204,9 @@ export function registerNodeRoutes(app, ctxRaw) {
204
204
  let b64, usage, webSearchCalls = 0, revisedPrompt = null;
205
205
  const grokDirectApiKey = activeProvider === "grok-api" ? ctx.xaiApiKey : undefined;
206
206
  let resultFormat = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" ? "jpeg" : format;
207
- const MAX_RETRIES = 1;
207
+ const maxAttempts = inputImageCount > 0 ? 1 : 2;
208
208
  let lastErr = null;
209
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
209
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
210
210
  try {
211
211
  logEvent("node", "attempt", {
212
212
  requestId,
@@ -297,7 +297,7 @@ export function registerNodeRoutes(app, ctxRaw) {
297
297
  if (isNonRetryableGenerationError(lastErr))
298
298
  break;
299
299
  }
300
- if (attempt < MAX_RETRIES) {
300
+ if (attempt + 1 < maxAttempts) {
301
301
  logEvent("node", "retry", {
302
302
  requestId,
303
303
  attempt: attempt + 1,
@@ -312,7 +312,7 @@ export function registerNodeRoutes(app, ctxRaw) {
312
312
  }
313
313
  if (!b64) {
314
314
  const finalErr = normalizeGenerationFailure(lastErr, {
315
- safetyMessage: lastErr?.message || "Empty response after retry",
315
+ safetyMessage: lastErr?.message || "Empty response after generation attempt",
316
316
  });
317
317
  finishStatus = "error";
318
318
  finishHttpStatus = finalErr.status || 500;
@@ -327,7 +327,7 @@ export function registerNodeRoutes(app, ctxRaw) {
327
327
  diagnosticReason: lastErr?.diagnosticReason,
328
328
  retryKind: lastErr?.retryKind,
329
329
  referencesDroppedOnRetry: lastErr?.referencesDroppedOnRetry,
330
- attempts: MAX_RETRIES + 1,
330
+ attempts: maxAttempts,
331
331
  outerHttpAlreadyCommitted: res.headersSent,
332
332
  sseErrorSent: streamResponse,
333
333
  });