@tickernelz/paperclip-pro-server 2026.1014.1 → 2026.1015.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/build-info.json +1 -1
  2. package/dist/services/heartbeat/retries.d.ts.map +1 -1
  3. package/dist/services/heartbeat/retries.js +33 -20
  4. package/dist/services/heartbeat/retries.js.map +1 -1
  5. package/dist/services/heartbeat/workspaces.d.ts +3 -0
  6. package/dist/services/heartbeat/workspaces.d.ts.map +1 -1
  7. package/dist/services/heartbeat/workspaces.js +49 -1
  8. package/dist/services/heartbeat/workspaces.js.map +1 -1
  9. package/dist/services/heartbeat.d.ts.map +1 -1
  10. package/dist/services/heartbeat.js +2 -1
  11. package/dist/services/heartbeat.js.map +1 -1
  12. package/dist/services/model-fallback.d.ts +40 -24
  13. package/dist/services/model-fallback.d.ts.map +1 -1
  14. package/dist/services/model-fallback.js +93 -61
  15. package/dist/services/model-fallback.js.map +1 -1
  16. package/dist/services/model-fallback.test.js +42 -56
  17. package/dist/services/model-fallback.test.js.map +1 -1
  18. package/node_modules/qs/.github/THREAT_MODEL.md +4 -2
  19. package/node_modules/qs/CHANGELOG.md +38 -14
  20. package/node_modules/qs/README.md +31 -3
  21. package/node_modules/qs/dist/qs.js +20 -20
  22. package/node_modules/qs/lib/parse.js +94 -4
  23. package/node_modules/qs/lib/stringify.js +4 -4
  24. package/node_modules/qs/lib/utils.js +130 -6
  25. package/node_modules/qs/package.json +4 -4
  26. package/node_modules/qs/test/parse.js +174 -0
  27. package/node_modules/qs/test/stringify.js +263 -33
  28. package/node_modules/qs/test/utils.js +184 -0
  29. package/package.json +21 -21
  30. package/ui-dist/assets/{Agents.production-B_gOaRfn.js → Agents.production-BHWznw8y.js} +2 -2
  31. package/ui-dist/assets/{CompanyActivity.production-CtbE4gxO.js → CompanyActivity.production-8aUIBuOH.js} +1 -1
  32. package/ui-dist/assets/{CompanyExport-JuOccR7C.js → CompanyExport-wysQLLwi.js} +1 -1
  33. package/ui-dist/assets/{CompanySkills.production-3LEUQ24r.js → CompanySkills.production-CeGKxxeo.js} +1 -1
  34. package/ui-dist/assets/{OrgChart.production-B41A6MKK.js → OrgChart.production-Bv25raI7.js} +1 -1
  35. package/ui-dist/assets/{RoutineDetail.production-rGdQgBL_.js → RoutineDetail.production-BP0s3T7M.js} +1 -1
  36. package/ui-dist/assets/{Routines.production-BCxCN3wv.js → Routines.production-BQL74XW8.js} +1 -1
  37. package/ui-dist/assets/{definition-B8BsU-S8.js → definition-CX59WzhL.js} +1 -1
  38. package/ui-dist/assets/{index-D6WtpzBM.js → index-DvHeLBFO.js} +88 -88
  39. package/ui-dist/index.html +1 -1
  40. package/ui-dist/sw.js +1 -1
@@ -212,63 +212,94 @@ test('stringify()', function (t) {
212
212
  st.end();
213
213
  });
214
214
 
215
- // Characterization of the CURRENT (buggy) behavior: with `allowDots` + `encodeDotInKeys`,
216
- // `stringify` percent-encodes the structural `.` separators that `allowDots` inserts between
217
- // nested keys, not just the literal dots inside a key, which breaks the stringify -> parse
218
- // round-trip. These assertions pin that behavior so it cannot change silently: the fix in
219
- // https://github.com/ljharb/qs/pull/564 changes every output below, so landing that fix MUST
220
- // update this test (that is the point — it forces the behavior change to be explicit).
221
- t.test('[characterization] encodeDotInKeys currently over-encodes structural separator dots (pre-#564)', function (st) {
215
+ t.test('does not encode the structural separator dots between nested keys when encodeDotInKeys and allowDots are provided', function (st) {
222
216
  st.equal(
223
- qs.stringify({ a: { b: { c: 'd' } } }, { allowDots: true, encodeDotInKeys: true }),
224
- 'a%252Eb.c=d',
225
- 'over-encodes the a->b separator (#564: should become a.b.c=d)'
217
+ qs.stringify(
218
+ { a: { b: { c: 'd' } } },
219
+ { allowDots: true, encodeDotInKeys: true }
220
+ ),
221
+ 'a.b.c=d',
222
+ 'no key contains a literal dot, so encodeDotInKeys is a no-op on the separators'
226
223
  );
227
224
  st.equal(
228
- qs.stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true, encodeDotInKeys: true }),
229
- 'a%252Eb%252Ec.d=e',
230
- 'clobbers every separator but the last (#564: should become a.b.c.d=e)'
225
+ qs.stringify(
226
+ { a: { b: { c: { d: 'e' } } } },
227
+ { allowDots: true, encodeDotInKeys: true }
228
+ ),
229
+ 'a.b.c.d=e',
230
+ 'every nesting separator is preserved, not just the last one'
231
231
  );
232
- st.equal(
233
- qs.stringify({ 'a.b': { c: { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
234
- 'a%252Eb%252Ec.d=e',
235
- 'top-level dotted key: deeper separators also get encoded (#564: should become a%252Eb.c.d=e)'
232
+
233
+ st.deepEqual(
234
+ qs.parse(
235
+ qs.stringify({ a: { b: { c: 'd' } } }, { allowDots: true, encodeDotInKeys: true }),
236
+ { allowDots: true, decodeDotInKeys: true }
237
+ ),
238
+ { a: { b: { c: 'd' } } },
239
+ 'stringify -> parse round-trips a deeply nested object'
236
240
  );
241
+
242
+ st.end();
243
+ });
244
+
245
+ t.test('encodeDotInKeys encodes only literal dots inside nested keys, never the structural separators, at any depth', function (st) {
237
246
  st.equal(
238
247
  qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
239
- 'a%252Eb%252Ec.d=e',
240
- 'nested dotted key + object value: the separator around it is encoded (#564: should become a.b%252Ec.d=e)'
248
+ 'a.b%252Ec.d=e',
249
+ 'a nested key with a literal dot and an object value encodes only its own dot'
250
+ );
251
+ st.equal(
252
+ qs.stringify({ 'a.b': { c: { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
253
+ 'a%252Eb.c.d=e',
254
+ 'a top-level dotted key stays encoded while deeper separators stay literal'
241
255
  );
242
256
  st.equal(
243
257
  qs.stringify({ a: { b: { 'c.d': { e: 'f' } } } }, { allowDots: true, encodeDotInKeys: true }),
244
- 'a%252Eb%252Ec%252Ed.e=f',
245
- 'deeply nested dotted key: all separators encoded (#564: should become a.b.c%252Ed.e=f)'
258
+ 'a.b.c%252Ed.e=f',
259
+ 'a literal dot in a deeply nested key is encoded without touching the separators around it'
246
260
  );
247
261
 
248
262
  st.equal(
249
263
  qs.stringify({ a: { b: [1, 2] } }, { allowDots: true, encodeDotInKeys: true }),
250
- 'a%252Eb%5B0%5D=1&a%252Eb%5B1%5D=2',
251
- 'array value under nested keys: separator encoded (#564: should become a.b%5B0%5D=1&a.b%5B1%5D=2)'
264
+ 'a.b%5B0%5D=1&a.b%5B1%5D=2',
265
+ 'separator dots are preserved for an array value nested under keys'
252
266
  );
253
267
  st.equal(
254
268
  qs.stringify({ a: { 'b.c': [1, 2] } }, { allowDots: true, encodeDotInKeys: true }),
255
- 'a%252Eb%252Ec%5B0%5D=1&a%252Eb%252Ec%5B1%5D=2',
256
- 'array value under nested dotted key (#564: should become a.b%252Ec%5B0%5D=1&a.b%252Ec%5B1%5D=2)'
269
+ 'a.b%252Ec%5B0%5D=1&a.b%252Ec%5B1%5D=2',
270
+ 'a nested dotted key with an array value encodes only its own dot'
257
271
  );
258
272
 
259
- st.equal(
260
- qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: false, encodeDotInKeys: true }),
261
- 'a%5Bb%252Ec%5D%5Bd%5D=e',
262
- 'allowDots:false object value encodes the nested-key dot, unlike the primitive-value case (#564: should become a%5Bb.c%5D%5Bd%5D=e)'
273
+ st.deepEqual(
274
+ qs.parse(
275
+ qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
276
+ { allowDots: true, decodeDotInKeys: true }
277
+ ),
278
+ { a: { 'b.c': { d: 'e' } } },
279
+ 'stringify -> parse round-trips a nested dotted key with an object value'
263
280
  );
264
-
265
281
  st.deepEqual(
266
282
  qs.parse(
267
- qs.stringify({ a: { b: { c: 'd' } } }, { allowDots: true, encodeDotInKeys: true }),
283
+ qs.stringify({ 'a.b': { c: { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
268
284
  { allowDots: true, decodeDotInKeys: true }
269
285
  ),
270
- { 'a.b': { c: 'd' } },
271
- 'the over-encoding breaks the round-trip: {a:{b:{c}}} comes back as {"a.b":{c}} (#564 restores it)'
286
+ { 'a.b': { c: { d: 'e' } } },
287
+ 'stringify -> parse round-trips a top-level dotted key with deep nesting'
288
+ );
289
+
290
+ st.end();
291
+ });
292
+
293
+ t.test('with allowDots false, encodeDotInKeys leaves nested-key dots literal, consistently for object and primitive values', function (st) {
294
+ st.equal(
295
+ qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: false, encodeDotInKeys: true }),
296
+ 'a%5Bb.c%5D%5Bd%5D=e',
297
+ 'nested dotted key with an object value'
298
+ );
299
+ st.equal(
300
+ qs.stringify({ a: { 'b.c': 'e' } }, { allowDots: false, encodeDotInKeys: true }),
301
+ 'a%5Bb.c%5D=e',
302
+ 'nested dotted key with a primitive value matches the object-value case'
272
303
  );
273
304
 
274
305
  st.end();
@@ -394,6 +425,41 @@ test('stringify()', function (t) {
394
425
  st.end();
395
426
  });
396
427
 
428
+ t.test('ignores inherited array format names', function (st) {
429
+ var array = { a: ['b', 'c'] };
430
+ var indices = 'a%5B0%5D=b&a%5B1%5D=c';
431
+ var repeat = 'a=b&a=c';
432
+
433
+ st.equal(qs.stringify(array, { arrayFormat: 'unsupported' }), indices, 'unknown format falls back to `indices`');
434
+
435
+ var names = [
436
+ 'constructor',
437
+ 'toString',
438
+ '__proto__',
439
+ 'valueOf',
440
+ 'hasOwnProperty',
441
+ 'isPrototypeOf',
442
+ 'propertyIsEnumerable',
443
+ 'toLocaleString'
444
+ ];
445
+ for (var i = 0; i < names.length; i++) {
446
+ var name = names[i];
447
+ st.equal(qs.stringify(array, { arrayFormat: name }), indices, '`' + name + '` falls back to `indices`');
448
+ st.equal(
449
+ qs.stringify(array, { arrayFormat: name, indices: true }),
450
+ indices,
451
+ '`' + name + '` with `indices: true` falls back to `indices`'
452
+ );
453
+ st.equal(
454
+ qs.stringify(array, { arrayFormat: name, indices: false }),
455
+ repeat,
456
+ '`' + name + '` with `indices: false` falls back to `repeat`'
457
+ );
458
+ }
459
+
460
+ st.end();
461
+ });
462
+
397
463
  t.test('`skipNulls` option', function (st) {
398
464
  st.equal(
399
465
  qs.stringify({ a: 'b', c: null }, { skipNulls: true }),
@@ -569,6 +635,53 @@ test('stringify()', function (t) {
569
635
  st.end();
570
636
  });
571
637
 
638
+ t.test('encodes comma array values with the same encoder arguments as other values, with encodeValuesOnly', function (st) {
639
+ var commaValuesOnly = { arrayFormat: 'comma', encodeValuesOnly: true };
640
+
641
+ st.equal(
642
+ qs.stringify({ a: ['é', 'x'], b: 'é' }, utils.assign({ charset: 'iso-8859-1' }, commaValuesOnly)),
643
+ 'a=%E9,x&b=%E9',
644
+ 'respects charset iso-8859-1'
645
+ );
646
+ st.deepEqual(
647
+ qs.parse(qs.stringify({ a: ['é', 'x'] }, utils.assign({ charset: 'iso-8859-1' }, commaValuesOnly)), { charset: 'iso-8859-1', comma: true }),
648
+ { a: ['é', 'x'] },
649
+ 'round-trips in iso-8859-1'
650
+ );
651
+ st.equal(
652
+ qs.stringify({ a: ['☺'] }, utils.assign({ charset: 'iso-8859-1' }, commaValuesOnly)),
653
+ 'a=%26%239786%3B',
654
+ 'encodes unrepresentable chars as numeric entities in iso-8859-1'
655
+ );
656
+ st.equal(
657
+ qs.stringify({ a: ['(x)', 'y z'], b: '(x)' }, utils.assign({ format: qs.formats.RFC1738 }, commaValuesOnly)),
658
+ 'a=(x),y+z&b=(x)',
659
+ 'respects format RFC1738'
660
+ );
661
+
662
+ var calls = [];
663
+ st.equal(
664
+ qs.stringify({ a: ['b', 'c'] }, utils.assign({
665
+ encoder: function (str, defaultEncoder, charset, kind, format) {
666
+ calls[calls.length] = [str, typeof defaultEncoder, charset, kind, format];
667
+ return defaultEncoder(str, defaultEncoder, charset, kind, format).toUpperCase();
668
+ }
669
+ }, commaValuesOnly)),
670
+ 'a=B,C',
671
+ 'a custom encoder can call defaultEncoder'
672
+ );
673
+ st.deepEqual(
674
+ calls,
675
+ [
676
+ ['b', 'function', 'utf-8', 'value', qs.formats.RFC3986],
677
+ ['c', 'function', 'utf-8', 'value', qs.formats.RFC3986]
678
+ ],
679
+ 'a custom encoder receives defaultEncoder, charset, kind, and format'
680
+ );
681
+
682
+ st.end();
683
+ });
684
+
572
685
  t.test('stringifies comma and empty non-array values', function (st) {
573
686
  st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' }), 'a=,&b=&c=c,d%');
574
687
  st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' }), 'a=,&b=&c=c,d%');
@@ -865,6 +978,112 @@ test('stringify()', function (t) {
865
978
  st.end();
866
979
  });
867
980
 
981
+ t.test('arrayFormat=comma: does not crash on entries with no callable `toString`', function (st) {
982
+ var json = JSON.parse('{ "a": ["b", { "toString": "x" }, "c"] }');
983
+ st.equal(qs.stringify(json, { arrayFormat: 'comma' }), 'a=b%2C%5Bobject%20Object%5D%2Cc', 'JSON own `toString`');
984
+ st.equal(qs.stringify(json, { arrayFormat: 'comma', encode: false }), 'a=b,[object Object],c', 'with encode: false');
985
+ st.equal(
986
+ qs.stringify(json, { arrayFormat: 'comma', encodeValuesOnly: true }),
987
+ 'a=b,%5Bobject%20Object%5D,c',
988
+ 'with encodeValuesOnly'
989
+ );
990
+ st.equal(
991
+ qs.stringify(JSON.parse('{ "a": [{ "toString": "x" }] }'), { arrayFormat: 'comma', commaRoundTrip: true }),
992
+ 'a%5B%5D=%5Bobject%20Object%5D',
993
+ 'with commaRoundTrip'
994
+ );
995
+ st.equal(
996
+ qs.stringify(JSON.parse('{ "a": [{ "toString": "x" }] }'), { arrayFormat: 'comma' }),
997
+ qs.stringify({ a: [{ b: 1 }] }, { arrayFormat: 'comma' }),
998
+ 'renders the same as a plain object entry'
999
+ );
1000
+
1001
+ st.equal(
1002
+ qs.stringify(qs.parse('a[][b]=c', { plainObjects: true }), { arrayFormat: 'comma' }),
1003
+ 'a=%5Bobject%20Object%5D',
1004
+ 'null-prototype entry from `parse` with plainObjects'
1005
+ );
1006
+ st.equal(
1007
+ qs.stringify(qs.parse('a[][b]=c', { plainObjects: true }), { arrayFormat: 'comma', encodeValuesOnly: true }),
1008
+ 'a=%5Bobject%20Object%5D',
1009
+ 'null-prototype entry from `parse` with plainObjects, with encodeValuesOnly'
1010
+ );
1011
+ st.equal(
1012
+ qs.stringify(qs.parse('a[0][toString]=x', { allowPrototypes: true }), { arrayFormat: 'comma' }),
1013
+ 'a=%5Bobject%20Object%5D',
1014
+ 'own `toString` entry from `parse` with allowPrototypes'
1015
+ );
1016
+
1017
+ var nested = JSON.parse('{ "a": [[1, [{ "toString": "x" }]], "y"] }');
1018
+ st.equal(qs.stringify(nested, { arrayFormat: 'comma', encode: false }), 'a=1,[object Object],y', 'nested array');
1019
+ st.equal(
1020
+ qs.stringify(nested, { arrayFormat: 'comma', encodeValuesOnly: true }),
1021
+ 'a=1%2C%5Bobject%20Object%5D,y',
1022
+ 'nested array, with encodeValuesOnly'
1023
+ );
1024
+ st.equal(qs.stringify({ a: [[1, 2], 3] }, { arrayFormat: 'comma', encode: false }), 'a=1,2,3', 'nested array is still flattened');
1025
+
1026
+ var cyclic = [{ toString: 'x' }];
1027
+ cyclic[cyclic.length] = cyclic;
1028
+ st.equal(
1029
+ qs.stringify({ a: cyclic }, { arrayFormat: 'comma', encode: false }),
1030
+ 'a=[object Object],[object Object],',
1031
+ 'cyclic nested array joins its cycle as empty'
1032
+ );
1033
+
1034
+ var customJoin = [{ toString: 'x' }];
1035
+ customJoin.join = function () { return 'j'; };
1036
+ st.equal(qs.stringify({ a: [customJoin, 'y'] }, { arrayFormat: 'comma', encode: false }), 'a=j,y', 'own `join` is still used');
1037
+
1038
+ st.equal(
1039
+ qs.stringify({ a: [{ toString: 'x', valueOf: function () { return 7; } }] }, { arrayFormat: 'comma' }),
1040
+ 'a=7',
1041
+ 'primitive `valueOf` is still used'
1042
+ );
1043
+ st.equal(
1044
+ qs.stringify({ a: [{ toString: 'x', valueOf: function () { return null; } }] }, { arrayFormat: 'comma' }),
1045
+ 'a=null',
1046
+ 'null `valueOf` is still used'
1047
+ );
1048
+ st.equal(
1049
+ qs.stringify({ a: [{ toString: 'x', valueOf: function () { return {}; } }] }, { arrayFormat: 'comma' }),
1050
+ 'a=%5Bobject%20Object%5D',
1051
+ 'non-primitive `valueOf` falls back'
1052
+ );
1053
+
1054
+ st.equal(
1055
+ qs.stringify({ a: [{ toString: function () { return 't'; } }, new Date(0)] }, { arrayFormat: 'comma', encode: false }),
1056
+ 'a=t,1970-01-01T00:00:00.000Z',
1057
+ 'callable `toString` and dates are unaffected'
1058
+ );
1059
+
1060
+ st['throws'](
1061
+ function () {
1062
+ qs.stringify({ a: [{ toString: function () { throw new SyntaxError('mine'); } }] }, { arrayFormat: 'comma' });
1063
+ },
1064
+ SyntaxError,
1065
+ 'a throwing `toString` still throws'
1066
+ );
1067
+ st['throws'](
1068
+ function () {
1069
+ qs.stringify({ a: [{ toString: 'x', valueOf: function () { throw new EvalError('mine'); } }] }, { arrayFormat: 'comma' });
1070
+ },
1071
+ EvalError,
1072
+ 'a throwing `valueOf` still throws'
1073
+ );
1074
+
1075
+ st.end();
1076
+ });
1077
+
1078
+ t.test('arrayFormat=comma: entries with `Symbol.toPrimitive` are unaffected', { skip: !hasSymbols() || typeof Symbol.toPrimitive !== 'symbol' }, function (st) {
1079
+ var entry = { toString: 'x' };
1080
+ entry[Symbol.toPrimitive] = function () { return 'p'; };
1081
+ st.equal(qs.stringify({ a: [entry] }, { arrayFormat: 'comma' }), 'a=p');
1082
+ st.equal(qs.stringify({ a: [entry] }, { arrayFormat: 'comma', encodeValuesOnly: true }), 'a=p');
1083
+
1084
+ st.end();
1085
+ });
1086
+
868
1087
  t.test('stringifies a null object', { skip: !hasProto }, function (st) {
869
1088
  st.equal(qs.stringify({ __proto__: null, a: 'b' }), 'a=b');
870
1089
  st.end();
@@ -1480,6 +1699,17 @@ test('stringify()', function (t) {
1480
1699
  st.end();
1481
1700
  });
1482
1701
 
1702
+ t.test('encodes a literal `+` as `%2B` in iso-8859-1 mode so it round-trips', function (st) {
1703
+ st.equal(qs.stringify({ a: 'b+c' }, { charset: 'iso-8859-1' }), 'a=b%2Bc', 'a `+` in a value is percent-encoded');
1704
+ st.equal(qs.stringify({ 'a+b': 'c' }, { charset: 'iso-8859-1' }), 'a%2Bb=c', 'a `+` in a key is percent-encoded');
1705
+ st.deepEqual(
1706
+ qs.parse(qs.stringify({ a: 'b+c' }, { charset: 'iso-8859-1' }), { charset: 'iso-8859-1' }),
1707
+ { a: 'b+c' },
1708
+ 'the `+` survives a round-trip instead of decoding to a space'
1709
+ );
1710
+ st.end();
1711
+ });
1712
+
1483
1713
  t.test('respects an explicit charset of utf-8 (the default)', function (st) {
1484
1714
  st.equal(qs.stringify({ a: 'æ' }, { charset: 'utf-8' }), 'a=%C3%A6');
1485
1715
  st.end();
@@ -106,6 +106,146 @@ test('merge()', function (t) {
106
106
  s2t.end();
107
107
  });
108
108
 
109
+ st.test('with strictMerge, repeated conflicts append instead of nesting a new wrapper', function (s2t) {
110
+ var opts = { strictMerge: true };
111
+ var wrapped = utils.merge({ foo: 'bar' }, 'baz', opts);
112
+ s2t.deepEqual(wrapped, [{ foo: 'bar' }, 'baz'], 'first conflict wraps in an array');
113
+
114
+ var withObject = utils.merge(wrapped, { qux: 'quux' }, opts);
115
+ s2t.deepEqual(
116
+ withObject,
117
+ { 0: { foo: 'bar' }, 1: 'baz', qux: 'quux' },
118
+ 'merging an object converts the wrapper to an object'
119
+ );
120
+
121
+ var second = utils.merge(withObject, 'corge', opts);
122
+ s2t.deepEqual(
123
+ second,
124
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', qux: 'quux' },
125
+ 'a second conflict appends at the next index'
126
+ );
127
+
128
+ var third = utils.merge(second, 'grault', opts);
129
+ s2t.deepEqual(
130
+ third,
131
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', 3: 'grault', qux: 'quux' },
132
+ 'a third conflict appends again, so nesting depth stays constant'
133
+ );
134
+
135
+ s2t.end();
136
+ });
137
+
138
+ st.test('with strictMerge, a caller-supplied array is still wrapped, not appended to', function (s2t) {
139
+ var merged = utils.merge({ 0: 'a', 1: 'b' }, 'c', { strictMerge: true });
140
+ s2t.deepEqual(merged, [{ 0: 'a', 1: 'b' }, 'c'], 'an unmarked object with numeric keys is wrapped');
141
+ s2t.end();
142
+ });
143
+
144
+ st.test('with strictMerge, throws instead of appending a conflict past arrayLimit with throwOnLimitExceeded', function (s2t) {
145
+ var appendConflict = function (options) {
146
+ var wrapped = utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true });
147
+ var container = utils.merge(wrapped, { qux: 'quux' }, { strictMerge: true });
148
+ return utils.merge(container, 'corge', options);
149
+ };
150
+
151
+ s2t['throws'](
152
+ function () { appendConflict({ strictMerge: true, arrayLimit: 1, throwOnLimitExceeded: true }); },
153
+ new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
154
+ 'appending to a two-element container past arrayLimit throws'
155
+ );
156
+ s2t['throws'](
157
+ function () { appendConflict({ strictMerge: true, arrayLimit: 2, throwOnLimitExceeded: true }); },
158
+ new RangeError('Array limit exceeded. Only 2 elements allowed in an array.'),
159
+ 'uses the plural message when arrayLimit is not 1'
160
+ );
161
+ s2t.deepEqual(
162
+ appendConflict({ strictMerge: true, arrayLimit: 3, throwOnLimitExceeded: true }),
163
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', qux: 'quux' },
164
+ 'appends while the container stays within arrayLimit'
165
+ );
166
+ s2t.deepEqual(
167
+ appendConflict({ strictMerge: true, arrayLimit: 1 }),
168
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', qux: 'quux' },
169
+ 'still appends past arrayLimit without throwOnLimitExceeded, since the container is already an object'
170
+ );
171
+
172
+ s2t.end();
173
+ });
174
+
175
+ st.test('with strictMerge, a conflict appends after numeric keys already in its container', function (s2t) {
176
+ var opts = { strictMerge: true };
177
+
178
+ var container = utils.merge(utils.merge({ foo: 'bar' }, 'baz', opts), { qux: 'quux' }, opts);
179
+ var withIndex = utils.merge(container, { 2: 'keep' }, opts);
180
+ s2t.deepEqual(
181
+ utils.merge(withIndex, 'corge', opts),
182
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'keep', 3: 'corge', qux: 'quux' },
183
+ 'a numeric key merged into the container is not overwritten'
184
+ );
185
+
186
+ var sparse = [];
187
+ sparse[2] = 'keep';
188
+ var extended = utils.merge(utils.merge({ foo: 'bar' }, 'baz', opts), sparse, opts);
189
+ s2t.deepEqual(extended, [{ foo: 'bar' }, 'baz', 'keep'], 'an indexed value extends the container while it is an array');
190
+ var converted = utils.merge(extended, { qux: 'quux' }, opts);
191
+ s2t.deepEqual(
192
+ utils.merge(converted, 'corge', opts),
193
+ { 0: { foo: 'bar' }, 1: 'baz', 2: 'keep', 3: 'corge', qux: 'quux' },
194
+ 'converting the container to an object keeps its last index'
195
+ );
196
+
197
+ s2t.end();
198
+ });
199
+
200
+ st.test('enforces arrayLimit on the two-element array built for colliding values', function (s2t) {
201
+ s2t['throws'](
202
+ function () { utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true, arrayLimit: 1, throwOnLimitExceeded: true }); },
203
+ new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
204
+ 'a strictMerge conflict throws at arrayLimit 1'
205
+ );
206
+ s2t['throws'](
207
+ function () { utils.merge('a', 'b', { arrayLimit: 0, throwOnLimitExceeded: true }); },
208
+ new RangeError('Array limit exceeded. Only 0 elements allowed in an array.'),
209
+ 'two colliding primitives throw at arrayLimit 0'
210
+ );
211
+
212
+ var conflict = utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true, arrayLimit: 1 });
213
+ s2t.ok(utils.isOverflow(conflict), 'without throwOnLimitExceeded, a strictMerge conflict converts to an overflow object');
214
+ s2t.deepEqual(conflict, { 0: { foo: 'bar' }, 1: 'baz' }, 'holding both values');
215
+
216
+ var pair = utils.merge('a', 'b', { arrayLimit: 1 });
217
+ s2t.ok(utils.isOverflow(pair), 'without throwOnLimitExceeded, two colliding primitives convert to an overflow object');
218
+ s2t.deepEqual(pair, { 0: 'a', 1: 'b' }, 'holding both values');
219
+
220
+ s2t.deepEqual(
221
+ utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true, arrayLimit: 2, throwOnLimitExceeded: true }),
222
+ [{ foo: 'bar' }, 'baz'],
223
+ 'a strictMerge conflict within arrayLimit is still an array'
224
+ );
225
+ s2t.deepEqual(
226
+ utils.merge('a', 'b', { arrayLimit: 2, throwOnLimitExceeded: true }),
227
+ ['a', 'b'],
228
+ 'two colliding primitives within arrayLimit are still an array'
229
+ );
230
+
231
+ s2t.end();
232
+ });
233
+
234
+ st.test('seeds an overflow container index from the keys it already holds', function (s2t) {
235
+ var overflow = utils.combine(['a'], 'b', 0, false);
236
+ s2t.ok(utils.isOverflow(overflow), 'the source is an overflow object');
237
+
238
+ var merged = utils.merge({ 2: 'keep', z: 'zz' }, overflow);
239
+ s2t.ok(utils.isOverflow(merged), 'the result is marked as overflow');
240
+ s2t.deepEqual(
241
+ utils.merge(merged, 'c'),
242
+ { 0: 'a', 1: 'b', 2: 'keep', 3: 'c', z: 'zz' },
243
+ 'the next value lands after the highest numeric key the container already held'
244
+ );
245
+
246
+ s2t.end();
247
+ });
248
+
109
249
  st.test('merges overflow object into primitive', function (s2t) {
110
250
  // Create an overflow object via combine: 2 elements (indices 0-1) with limit 0
111
251
  var overflow = utils.combine(['a'], 'b', 0, false);
@@ -441,6 +581,50 @@ test('decode', function (t) {
441
581
  'decodes a string in iso-8859-1'
442
582
  );
443
583
 
584
+ t.test('strings with no percent sign', function (st) {
585
+ st.equal(utils.decode(''), '', 'empty string');
586
+ st.equal(utils.decode('abc'), 'abc', 'returns a plain string unchanged');
587
+ st.equal(utils.decode('abc', null, 'iso-8859-1'), 'abc', 'returns a plain string unchanged in iso-8859-1');
588
+ st.equal(utils.decode('a++b+'), 'a b ', 'decodes every + to a space');
589
+ st.equal(utils.decode('a++b+', null, 'iso-8859-1'), 'a b ', 'decodes every + to a space in iso-8859-1');
590
+ st.equal(utils.decode('\uD800x\uDC00'), '\uD800x\uDC00', 'leaves lone surrogates unchanged');
591
+ st.equal(utils.decode('😀+'), '😀 ', 'leaves surrogate pairs unchanged');
592
+ st.equal(utils.decode('a=b&c'), 'a=b&c', 'leaves delimiters unchanged');
593
+
594
+ st.end();
595
+ });
596
+
597
+ t.test('strings with a percent sign', function (st) {
598
+ st.equal(utils.decode('a+%20b'), 'a b', 'decodes both + and an escape');
599
+ st.equal(utils.decode('%E9+x', null, 'iso-8859-1'), 'é x', 'decodes both + and an escape in iso-8859-1');
600
+ st.equal(utils.decode('a+%'), 'a %', 'decodes + even when the escape is malformed');
601
+ st.equal(utils.decode('a+%E0%A4%A'), 'a %E0%A4%A', 'decodes + even when the escape is truncated');
602
+
603
+ st.end();
604
+ });
605
+
606
+ t.test('non-strings', function (st) {
607
+ forEach(v.nullPrimitives, function (nullish) {
608
+ st['throws'](
609
+ function () { utils.decode(nullish); },
610
+ TypeError,
611
+ inspect(nullish) + ' is not a string'
612
+ );
613
+ });
614
+ st['throws'](
615
+ function () { utils.decode(['a']); },
616
+ TypeError,
617
+ 'an array is not a string'
618
+ );
619
+
620
+ /* eslint no-new-wrappers: 0 */
621
+ st.equal(utils.decode(new String('ab')), 'ab', 'a boxed string decodes to a primitive');
622
+ st.equal(utils.decode(new String('a+b')), 'a b', 'a boxed string with + decodes to a primitive');
623
+ st.equal(utils.decode(new String('a%20b')), 'a b', 'a boxed string with an escape decodes to a primitive');
624
+
625
+ st.end();
626
+ });
627
+
444
628
  t.end();
445
629
  });
446
630
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tickernelz/paperclip-pro-server",
3
- "version": "2026.1014.1",
3
+ "version": "2026.1015.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://github.com/tickernelz/paperclip-pro",
6
6
  "bugs": {
@@ -54,25 +54,25 @@
54
54
  "@openrouter/ai-sdk-provider": "3.1.0",
55
55
  "@opentelemetry/api": "^1.9.0",
56
56
  "@photon-ai/advanced-imessage": "2.1.0",
57
- "@tickernelz/paperclip-pro-adapter-claude-local": "2026.1014.1",
58
- "@tickernelz/paperclip-pro-adapter-codex-local": "2026.1014.1",
59
- "@tickernelz/paperclip-pro-adapter-cursor-cloud": "2026.1014.1",
60
- "@tickernelz/paperclip-pro-adapter-cursor-local": "2026.1014.1",
61
- "@tickernelz/paperclip-pro-adapter-gemini-local": "2026.1014.1",
62
- "@tickernelz/paperclip-pro-adapter-grok-local": "2026.1014.1",
63
- "@tickernelz/paperclip-pro-adapter-kimi-local": "2026.1014.1",
64
- "@tickernelz/paperclip-pro-adapter-omp-local": "2026.1014.1",
65
- "@tickernelz/paperclip-pro-adapter-openclaw-gateway": "2026.1014.1",
66
- "@tickernelz/paperclip-pro-adapter-opencode-local": "2026.1014.1",
67
- "@tickernelz/paperclip-pro-adapter-pi-local": "2026.1014.1",
68
- "@tickernelz/paperclip-pro-adapter-utils": "2026.1014.1",
69
- "@tickernelz/paperclip-pro-db": "2026.1014.1",
70
- "@tickernelz/paperclip-pro-hermes-paperclip-adapter": "2026.1014.1",
71
- "@tickernelz/paperclip-pro-mcp-server": "2026.1014.1",
72
- "@tickernelz/paperclip-pro-plugin-sdk": "2026.1014.1",
73
- "@tickernelz/paperclip-pro-shared": "2026.1014.1",
74
- "@tickernelz/paperclip-pro-skills-catalog": "2026.1014.1",
75
- "@tickernelz/paperclip-pro-teams-catalog": "2026.1014.1",
57
+ "@tickernelz/paperclip-pro-adapter-claude-local": "2026.1015.0",
58
+ "@tickernelz/paperclip-pro-adapter-codex-local": "2026.1015.0",
59
+ "@tickernelz/paperclip-pro-adapter-cursor-cloud": "2026.1015.0",
60
+ "@tickernelz/paperclip-pro-adapter-cursor-local": "2026.1015.0",
61
+ "@tickernelz/paperclip-pro-adapter-gemini-local": "2026.1015.0",
62
+ "@tickernelz/paperclip-pro-adapter-grok-local": "2026.1015.0",
63
+ "@tickernelz/paperclip-pro-adapter-kimi-local": "2026.1015.0",
64
+ "@tickernelz/paperclip-pro-adapter-omp-local": "2026.1015.0",
65
+ "@tickernelz/paperclip-pro-adapter-openclaw-gateway": "2026.1015.0",
66
+ "@tickernelz/paperclip-pro-adapter-opencode-local": "2026.1015.0",
67
+ "@tickernelz/paperclip-pro-adapter-pi-local": "2026.1015.0",
68
+ "@tickernelz/paperclip-pro-adapter-utils": "2026.1015.0",
69
+ "@tickernelz/paperclip-pro-db": "2026.1015.0",
70
+ "@tickernelz/paperclip-pro-hermes-paperclip-adapter": "2026.1015.0",
71
+ "@tickernelz/paperclip-pro-mcp-server": "2026.1015.0",
72
+ "@tickernelz/paperclip-pro-plugin-sdk": "2026.1015.0",
73
+ "@tickernelz/paperclip-pro-shared": "2026.1015.0",
74
+ "@tickernelz/paperclip-pro-skills-catalog": "2026.1015.0",
75
+ "@tickernelz/paperclip-pro-teams-catalog": "2026.1015.0",
76
76
  "@vercel/connect": "2.3.3",
77
77
  "acorn": "8.18.0",
78
78
  "acpx": "0.13.1",
@@ -128,7 +128,7 @@
128
128
  "acpx"
129
129
  ],
130
130
  "devDependencies": {
131
- "@tickernelz/paperclip-pro-paperclip-runner": "2026.1014.1",
131
+ "@tickernelz/paperclip-pro-paperclip-runner": "2026.1015.0",
132
132
  "@types/compression": "^1.8.1",
133
133
  "@types/express": "^5.0.0",
134
134
  "@types/express-serve-static-core": "^5.1.3",