ima2-gen 2.0.1 → 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.
- package/CHANGELOG.md +151 -0
- package/README.md +10 -1
- package/bin/commands/backfillThumbs.js +6 -0
- package/bin/commands/gen.js +13 -5
- package/bin/commands/multimode.js +7 -5
- package/bin/commands/node.js +2 -1
- package/bin/ima2.js +14 -10
- package/config.js +8 -1
- package/docs/API.md +133 -9
- package/docs/CLI.md +6 -5
- package/docs/FAQ.ko.md +16 -0
- package/docs/FAQ.md +30 -0
- package/docs/README.ko.md +7 -3
- package/docs/migration/runtime-test-inventory.md +25 -1
- package/lib/agentCommandParser.js +10 -6
- package/lib/agentGenerationPlanner.js +96 -19
- package/lib/agentImageVideoGen.js +281 -0
- package/lib/agentPlannerModel.js +172 -0
- package/lib/agentQuestionResponder.js +9 -3
- package/lib/agentQueueStore.js +42 -0
- package/lib/agentQueueWorker.js +57 -7
- package/lib/agentRuntime.js +103 -274
- package/lib/agentSettings.js +9 -6
- package/lib/agentToolManifest.js +90 -0
- package/lib/agentTypes.js +1 -0
- package/lib/agyCli.js +31 -0
- package/lib/agyImageAdapter.js +104 -12
- package/lib/capabilities.js +7 -5
- package/lib/configKeys.js +6 -0
- package/lib/errorClassify.js +8 -7
- package/lib/eventBus.js +71 -0
- package/lib/geminiApiImageAdapter.js +24 -21
- package/lib/generationErrors.js +3 -1
- package/lib/generationRequestLog.js +25 -0
- package/lib/grokImageAdapter.js +76 -144
- package/lib/grokImageCore.js +153 -0
- package/lib/grokMultimodeAdapter.js +7 -4
- package/lib/grokVideoAdapter.js +3 -2
- package/lib/grokVideoCanvas.js +13 -0
- package/lib/grokVideoPlannerPrompt.js +52 -12
- package/lib/historyList.js +1 -0
- package/lib/inflight.js +54 -17
- package/lib/multimodeHelpers.js +11 -0
- package/lib/nodeHelpers.js +59 -0
- package/lib/oauthProxy/generators.js +6 -5
- package/lib/oauthProxy/prompts.js +60 -41
- package/lib/oauthProxy/streams.js +1 -1
- package/lib/promptBuilder/systemPrompt.js +2 -5
- package/lib/promptSafetyPolicy.js +1 -5
- package/lib/responsesDoctor.js +3 -3
- package/lib/responsesFallback.js +29 -15
- package/lib/responsesImageAdapter.js +11 -7
- package/lib/routeHelpers.js +44 -0
- package/lib/ssePublish.js +12 -0
- package/lib/storyboardPrefix.js +28 -0
- package/lib/thumbBackfill.js +16 -5
- package/node_modules/qs/CHANGELOG.md +10 -0
- package/node_modules/qs/README.md +1 -1
- package/node_modules/qs/dist/qs.js +15 -15
- package/node_modules/qs/eslint.config.mjs +1 -0
- package/node_modules/qs/lib/parse.js +52 -22
- package/node_modules/qs/lib/stringify.js +11 -4
- package/node_modules/qs/package.json +2 -2
- package/node_modules/qs/test/parse.js +49 -0
- package/node_modules/qs/test/stringify.js +129 -0
- package/package.json +4 -1
- package/routes/agent.js +19 -2
- package/routes/agy.js +48 -0
- package/routes/auth.js +6 -2
- package/routes/edit.js +7 -1
- package/routes/events.js +78 -0
- package/routes/generate.js +111 -128
- package/routes/generationRequestLog.js +16 -0
- package/routes/index.js +6 -0
- package/routes/keys.js +22 -2
- package/routes/multimode.js +100 -57
- package/routes/nodes.js +64 -108
- package/routes/video.js +100 -17
- package/skills/ima2/SKILL.md +98 -21
- package/ui/dist/.vite/manifest.json +22 -12
- package/ui/dist/assets/AgentWorkspace-qEMrATBP.js +3 -0
- package/ui/dist/assets/{CardNewsWorkspace-Dqyc1WZ1.js → CardNewsWorkspace-DoJnVHmn.js} +1 -1
- package/ui/dist/assets/GenerationRequestLogPanel-BDWRRLNV.js +1 -0
- package/ui/dist/assets/{NodeCanvas-ChEXzQbb.js → NodeCanvas-DIMo45sp.js} +3 -3
- package/ui/dist/assets/{PromptBuilderPanel-B95ZufnR.js → PromptBuilderPanel-BF3lcSJD.js} +2 -2
- package/ui/dist/assets/{PromptImportDialog-DGOwFQET.js → PromptImportDialog-Cy6ZymKG.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-CgvdnR49.js → PromptImportDiscoverySection-CzuW-8P2.js} +1 -1
- package/ui/dist/assets/PromptImportFolderSection-DP5ywnsv.js +1 -0
- package/ui/dist/assets/{PromptLibraryPanel-B9kndPw1.js → PromptLibraryPanel-CAsKr7CV.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-_PdPVsxi.js +1 -0
- package/ui/dist/assets/index-BUinlX2n.js +4 -0
- package/ui/dist/assets/index-CX3fge8X.css +1 -0
- package/ui/dist/assets/index-ygo6nfqx.js +23 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AgentWorkspace-CYv84Rus.js +0 -3
- package/ui/dist/assets/PromptImportFolderSection-CfUye9J8.js +0 -1
- package/ui/dist/assets/SettingsWorkspace-B3tgLrmF.js +0 -1
- package/ui/dist/assets/index-BhcvL0g-.js +0 -1
- package/ui/dist/assets/index-BtK3YhJc.js +0 -39
- package/ui/dist/assets/index-ClOLOjnA.css +0 -1
|
@@ -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.
|
|
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": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"cli"
|
|
40
40
|
],
|
|
41
41
|
"license": "MIT",
|
|
42
|
+
"homepage": "https://lidge-jun.github.io/ima2-gen/",
|
|
42
43
|
"repository": {
|
|
43
44
|
"type": "git",
|
|
44
45
|
"url": "git+https://github.com/lidge-jun/ima2-gen.git"
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
"assets/card-news/templates/",
|
|
58
59
|
".env.example",
|
|
59
60
|
"README.md",
|
|
61
|
+
"CHANGELOG.md",
|
|
60
62
|
"LICENSE",
|
|
61
63
|
"server.js",
|
|
62
64
|
"config.js"
|
|
@@ -65,6 +67,7 @@
|
|
|
65
67
|
"node": ">=20"
|
|
66
68
|
},
|
|
67
69
|
"dependencies": {
|
|
70
|
+
"@openai/codex": "latest",
|
|
68
71
|
"better-sqlite3": "^12.9.0",
|
|
69
72
|
"dotenv": "^17.4.2",
|
|
70
73
|
"express": "^5.1.0",
|
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
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { buildAgyPathEnv, resolveAgyBin } from "../lib/agyCli.js";
|
|
3
|
+
// Detect whether the Antigravity CLI (`agy`) is installed, using the same
|
|
4
|
+
// spawn-and-catch style as lib/agyImageAdapter.ts (no shell `which`/`where`).
|
|
5
|
+
// Login state cannot be probed — agy has no status command — so we only
|
|
6
|
+
// report installation here.
|
|
7
|
+
function isAgyInstalled() {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
let settled = false;
|
|
10
|
+
const done = (value) => {
|
|
11
|
+
if (settled)
|
|
12
|
+
return;
|
|
13
|
+
settled = true;
|
|
14
|
+
resolve(value);
|
|
15
|
+
};
|
|
16
|
+
try {
|
|
17
|
+
const child = spawn(resolveAgyBin(), ["--version"], {
|
|
18
|
+
stdio: "ignore",
|
|
19
|
+
env: { ...process.env, PATH: buildAgyPathEnv() },
|
|
20
|
+
});
|
|
21
|
+
child.on("error", () => done(false)); // ENOENT when not on PATH
|
|
22
|
+
child.on("exit", (code) => done(code === 0));
|
|
23
|
+
// Safety timeout so a hung binary never blocks the request.
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
try {
|
|
26
|
+
if (!child.killed)
|
|
27
|
+
child.kill();
|
|
28
|
+
}
|
|
29
|
+
catch { /* ignore */ }
|
|
30
|
+
done(false);
|
|
31
|
+
}, 3000).unref?.();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
done(false);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function registerAgyRoutes(app) {
|
|
39
|
+
app.get("/api/agy/status", async (_req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const installed = await isAgyInstalled();
|
|
42
|
+
res.json({ installed });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
res.json({ installed: false });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
package/routes/auth.js
CHANGED
|
@@ -2,11 +2,15 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { writeFileSync, renameSync, mkdirSync, existsSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
const GROK_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
7
8
|
const GROK_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
8
9
|
const GROK_TOKEN_URL = "https://auth.x.ai/oauth2/token";
|
|
9
10
|
const CODEX_DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
11
|
+
// Bundled @openai/codex binary (npm dependency), resolved relative to this
|
|
12
|
+
// module so device-auth login works even when `codex` is not on the user's PATH.
|
|
13
|
+
const CODEX_BIN = join(dirname(fileURLToPath(import.meta.url)), "..", "node_modules", ".bin", process.platform === "win32" ? "codex.cmd" : "codex");
|
|
10
14
|
const MAX_CONCURRENT_SESSIONS = 20;
|
|
11
15
|
const sessions = new Map();
|
|
12
16
|
function sid() {
|
|
@@ -125,7 +129,7 @@ function startCodexDeviceCode() {
|
|
|
125
129
|
for (const k of ["OPENAI_API_KEY", "XAI_API_KEY", "GEMINI_API_KEY", "ANTHROPIC_API_KEY", "VERTEX_SERVICE_ACCOUNT_JSON"]) {
|
|
126
130
|
delete childEnv[k];
|
|
127
131
|
}
|
|
128
|
-
const child = spawn(
|
|
132
|
+
const child = spawn(CODEX_BIN, ["login", "--device-auth"], {
|
|
129
133
|
stdio: ["ignore", "pipe", "pipe"],
|
|
130
134
|
env: childEnv,
|
|
131
135
|
});
|
package/routes/edit.js
CHANGED
|
@@ -169,6 +169,7 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
169
169
|
let revisedPrompt;
|
|
170
170
|
let webSearchCalls = 0;
|
|
171
171
|
let resultMimeFromProvider;
|
|
172
|
+
let providerUrl = null;
|
|
172
173
|
if (activeProvider === "gemini-api") {
|
|
173
174
|
const r = await generateViaGeminiApi(`Edit this image: ${prompt}`, requireRuntimeContext(ctx), {
|
|
174
175
|
model: imageModel,
|
|
@@ -206,6 +207,7 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
206
207
|
directApiKey,
|
|
207
208
|
});
|
|
208
209
|
resultB64 = r.b64;
|
|
210
|
+
providerUrl = r.providerUrl ?? null;
|
|
209
211
|
usage = r.usage;
|
|
210
212
|
revisedPrompt = r.revisedPrompt;
|
|
211
213
|
webSearchCalls = r.webSearchCalls;
|
|
@@ -237,6 +239,7 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
237
239
|
const editFilePath = join(ctx.config.storage.generatedDir, filename);
|
|
238
240
|
await writeFile(editFilePath, editBuffer);
|
|
239
241
|
generateImageThumbnailFromBuffer(editBuffer, editFilePath).catch(() => { });
|
|
242
|
+
const createdAt = Date.now();
|
|
240
243
|
const meta = {
|
|
241
244
|
prompt,
|
|
242
245
|
userPrompt: prompt,
|
|
@@ -252,10 +255,11 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
252
255
|
provider: activeProvider,
|
|
253
256
|
kind: "edit",
|
|
254
257
|
requestId,
|
|
255
|
-
createdAt
|
|
258
|
+
createdAt,
|
|
256
259
|
usage: usage || null,
|
|
257
260
|
webSearchCalls,
|
|
258
261
|
webSearchEnabled,
|
|
262
|
+
...(providerUrl ? { providerUrl } : {}),
|
|
259
263
|
};
|
|
260
264
|
await safeWriteSidecar(join(ctx.config.storage.generatedDir, filename + ".json"), meta);
|
|
261
265
|
invalidateHistoryIndex();
|
|
@@ -281,6 +285,8 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
281
285
|
promptMode: normalizedPromptMode,
|
|
282
286
|
webSearchCalls,
|
|
283
287
|
webSearchEnabled,
|
|
288
|
+
providerUrl,
|
|
289
|
+
createdAt,
|
|
284
290
|
});
|
|
285
291
|
}
|
|
286
292
|
catch (e) {
|
package/routes/events.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { subscribe, replaySince, hasReplayGap, replayOldestId, MAX_SSE_LISTENERS } from "../lib/eventBus.js";
|
|
2
|
+
let activeConnections = 0;
|
|
3
|
+
function safeWrite(res, chunk) {
|
|
4
|
+
if (res.writableEnded || res.destroyed)
|
|
5
|
+
return false;
|
|
6
|
+
try {
|
|
7
|
+
res.write(chunk);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function formatSse(ev) {
|
|
15
|
+
return `id: ${ev.id}\nevent: ${ev.event}\ndata: ${JSON.stringify({ ...ev.data, jobId: ev.jobId })}\n\n`;
|
|
16
|
+
}
|
|
17
|
+
export function registerEventsRoute(app, _ctx) {
|
|
18
|
+
app.get("/api/events", (req, res) => {
|
|
19
|
+
if (activeConnections >= MAX_SSE_LISTENERS) {
|
|
20
|
+
return res.status(503).json({
|
|
21
|
+
error: { code: "SSE_CAPACITY", message: "Too many event stream connections" },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
25
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
26
|
+
res.setHeader("Connection", "keep-alive");
|
|
27
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
28
|
+
res.flushHeaders?.();
|
|
29
|
+
activeConnections++;
|
|
30
|
+
const headerLastId = parseInt(req.headers["last-event-id"], 10);
|
|
31
|
+
const queryLastId = parseInt(String(req.query.lastEventId ?? ""), 10);
|
|
32
|
+
const lastId = !Number.isNaN(headerLastId) ? headerLastId : queryLastId;
|
|
33
|
+
if (!Number.isNaN(lastId)) {
|
|
34
|
+
if (hasReplayGap(lastId)) {
|
|
35
|
+
const gapPayload = JSON.stringify({
|
|
36
|
+
lastEventId: lastId,
|
|
37
|
+
oldestAvailableId: replayOldestId(),
|
|
38
|
+
});
|
|
39
|
+
if (!safeWrite(res, `event: replay-gap\ndata: ${gapPayload}\n\n`)) {
|
|
40
|
+
activeConnections = Math.max(0, activeConnections - 1);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const ev of replaySince(lastId)) {
|
|
45
|
+
if (!safeWrite(res, formatSse(ev)))
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let cleaned = false;
|
|
50
|
+
const unsub = subscribe((ev) => {
|
|
51
|
+
if (!safeWrite(res, formatSse(ev)))
|
|
52
|
+
cleanup();
|
|
53
|
+
});
|
|
54
|
+
const heartbeat = setInterval(() => {
|
|
55
|
+
if (!safeWrite(res, ": ping\n\n"))
|
|
56
|
+
cleanup();
|
|
57
|
+
}, 15_000);
|
|
58
|
+
function cleanup() {
|
|
59
|
+
if (cleaned)
|
|
60
|
+
return;
|
|
61
|
+
cleaned = true;
|
|
62
|
+
unsub();
|
|
63
|
+
clearInterval(heartbeat);
|
|
64
|
+
activeConnections = Math.max(0, activeConnections - 1);
|
|
65
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
66
|
+
try {
|
|
67
|
+
res.end();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* socket already torn down */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
req.on("close", cleanup);
|
|
75
|
+
res.on("close", cleanup);
|
|
76
|
+
res.on("error", cleanup);
|
|
77
|
+
});
|
|
78
|
+
}
|