switchroom 0.16.38 → 0.16.46
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/dist/agent-scheduler/index.js +88 -82
- package/dist/auth-broker/index.js +87 -81
- package/dist/cli/autoaccept-poll.js +8 -8
- package/dist/cli/drive-write-pretool.mjs +10 -10
- package/dist/cli/notion-write-pretool.mjs +89 -83
- package/dist/cli/skill-validate-pretool.mjs +91 -91
- package/dist/cli/switchroom.js +1621 -737
- package/dist/cli/ui/index.html +877 -214
- package/dist/host-control/main.js +271 -239
- package/dist/vault/approvals/kernel-server.js +90 -84
- package/dist/vault/broker/server.js +91 -85
- package/examples/minimal.yaml +1 -1
- package/examples/switchroom.yaml +1 -1
- package/package.json +2 -2
- package/profiles/_shared/reply-discipline.md.hbs +9 -0
- package/skills/switchroom-status/SKILL.md +1 -1
- package/telegram-plugin/bridge/bridge.ts +2 -1
- package/telegram-plugin/card-format.ts +7 -1
- package/telegram-plugin/dist/bridge/bridge.js +132 -114
- package/telegram-plugin/dist/gateway/gateway.js +2090 -1046
- package/telegram-plugin/dist/server.js +180 -162
- package/telegram-plugin/format.ts +305 -31
- package/telegram-plugin/gateway/gateway.ts +262 -63
- package/telegram-plugin/gateway/model-command.ts +173 -19
- package/telegram-plugin/hooks/tool-label-pretool.d.mts +12 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +54 -16
- package/telegram-plugin/package.json +1 -1
- package/telegram-plugin/session-tail.ts +47 -1
- package/telegram-plugin/stream-reply-handler.ts +19 -1
- package/telegram-plugin/tests/always-allow-grant.test.ts +34 -2
- package/telegram-plugin/tests/card-format.test.ts +28 -0
- package/telegram-plugin/tests/claude-code-event-contract.test.ts +151 -0
- package/telegram-plugin/tests/format-consistency.test.ts +223 -0
- package/telegram-plugin/tests/formatting-parse-regression.test.ts +272 -0
- package/telegram-plugin/tests/formatting-torture-set.ts +218 -0
- package/telegram-plugin/tests/model-command.test.ts +213 -47
- package/telegram-plugin/tests/paragraph-normalizer.test.ts +203 -21
- package/telegram-plugin/tests/rich-markdown-oracle.ts +469 -0
- package/telegram-plugin/tests/session-tail.test.ts +91 -0
- package/telegram-plugin/tests/status-vocabulary-unification.test.ts +125 -0
- package/telegram-plugin/tests/telegram-format.test.ts +33 -8
- package/telegram-plugin/tests/text-voice-scrub.test.ts +142 -22
- package/telegram-plugin/tests/tool-activity-summary.test.ts +6 -1
- package/telegram-plugin/tests/tts-normalize.test.ts +242 -0
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +24 -0
- package/telegram-plugin/tests/voice-ondemand.test.ts +99 -2
- package/telegram-plugin/tests/voice-presynth.test.ts +437 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +49 -0
- package/telegram-plugin/text-voice-scrub.ts +68 -18
- package/telegram-plugin/tool-activity-summary.ts +20 -108
- package/telegram-plugin/tts-normalize.ts +377 -0
- package/telegram-plugin/uat/driver.ts +472 -22
- package/telegram-plugin/uat/scenarios/jtbd-model-litellm-sr-dm.test.ts +34 -14
- package/telegram-plugin/uat/scenarios/jtbd-multipart-render-dm.test.ts +169 -0
- package/telegram-plugin/uat/scenarios/jtbd-narration-intent-dm.test.ts +134 -0
- package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +254 -0
- package/telegram-plugin/uat/scenarios/jtbd-status-phase-transitions-dm.test.ts +109 -0
- package/telegram-plugin/uat/uat-driver.test.ts +297 -0
- package/telegram-plugin/voice-ondemand.ts +161 -10
- package/telegram-plugin/voice-presynth.ts +242 -0
- package/telegram-plugin/worker-activity-feed.ts +9 -1
|
@@ -48,11 +48,71 @@ describe('normalizeParagraphBreaks', () => {
|
|
|
48
48
|
expect(normalizeParagraphBreaks(input)).toBe(input)
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
test('
|
|
52
|
-
|
|
51
|
+
test('promotes every interior break in a vertical stat card (#2750)', () => {
|
|
52
|
+
// The gymbro food-log card: lines end in digits/letters (no terminator),
|
|
53
|
+
// but each is a standalone `label: value` stat line, so every interior
|
|
54
|
+
// break must become a GFM hard break instead of collapsing to one wall.
|
|
55
|
+
const input = 'Calories: 1800\nProtein: 120g\nCarbs: 200g'
|
|
56
|
+
expect(normalizeParagraphBreaks(input)).toBe(
|
|
57
|
+
'Calories: 1800 \nProtein: 120g \nCarbs: 200g',
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('promotes a key-value line followed by prose (#2750)', () => {
|
|
62
|
+
const input = 'Status: active\nEverything is running smoothly.'
|
|
63
|
+
expect(normalizeParagraphBreaks(input)).toBe(
|
|
64
|
+
'Status: active \nEverything is running smoothly.',
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('promotes bold-label stat cards — colon inside AND outside the emphasis (#2750)', () => {
|
|
69
|
+
// The fleet HOUSE style uses markdown-bold labels. Both `**Calories:** 1800`
|
|
70
|
+
// (colon inside the bold) and `**Calories**: 1800` (colon outside) must have
|
|
71
|
+
// every interior break promoted.
|
|
72
|
+
const inside = '**Calories:** 1800\n**Protein:** 120g\n**Carbs:** 200g'
|
|
73
|
+
expect(normalizeParagraphBreaks(inside)).toBe(
|
|
74
|
+
'**Calories:** 1800 \n**Protein:** 120g \n**Carbs:** 200g',
|
|
75
|
+
)
|
|
76
|
+
const outside = '**Calories**: 1800\n**Protein**: 120g'
|
|
77
|
+
expect(normalizeParagraphBreaks(outside)).toBe('**Calories**: 1800 \n**Protein**: 120g')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('regression guard: a mid-sentence colon soft-wrap still does NOT promote (#2750)', () => {
|
|
81
|
+
// A wrapped sentence that happens to contain a colon must NOT be mistaken
|
|
82
|
+
// for a stat line: the next line continues the sentence in lowercase.
|
|
83
|
+
const input = 'He made one point: the plan was sound and\nthe timing was right too'
|
|
84
|
+
expect(normalizeParagraphBreaks(input)).toBe(input)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('regression guard: a long colon-clause with a CAPITAL continuation does NOT promote (#2750)', () => {
|
|
88
|
+
// The lowercase-continuation guard alone would miss this: the wrapped clause
|
|
89
|
+
// continues with a capitalised proper noun. The label before the colon is a
|
|
90
|
+
// long multi-word run (not a short stat label), so the word/length cap keeps
|
|
91
|
+
// it from being mistaken for a stat line.
|
|
92
|
+
const input = 'The deal has one catch: the buyer wants it and\nToronto lawyers must sign off'
|
|
53
93
|
expect(normalizeParagraphBreaks(input)).toBe(input)
|
|
54
94
|
})
|
|
55
95
|
|
|
96
|
+
test('lowercase-label stat cards are an ACCEPTED false-negative (not promoted) (#2750)', () => {
|
|
97
|
+
// `calories: 1800\nprotein: 120g` — the lowercase-started next line is
|
|
98
|
+
// indistinguishable from a mid-sentence soft-wrap continuation, so the
|
|
99
|
+
// conservative guard leaves it alone. Documented, not fixed: models are
|
|
100
|
+
// steered toward capitalised / bold labels, and a false-negative (un-promoted
|
|
101
|
+
// break) is the module's preferred failure mode over a false-positive.
|
|
102
|
+
const input = 'calories: 1800\nprotein: 120g'
|
|
103
|
+
expect(normalizeParagraphBreaks(input)).toBe(input)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('list glued to prose gains a block-boundary blank line; interior stays tight', () => {
|
|
107
|
+
// Uniform-block-spacing: a list start glued to prose by a single `\n`
|
|
108
|
+
// gets a blank line (so the spacer pass can see the transition); the
|
|
109
|
+
// list's own single-`\n` interior is never touched.
|
|
110
|
+
const input = 'Here are the steps.\n- first\n- second\n- third'
|
|
111
|
+
expect(normalizeParagraphBreaks(input)).toBe(
|
|
112
|
+
'Here are the steps.\n\n- first\n- second\n- third',
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
56
116
|
test('does NOT promote between list items (ordered or unordered)', () => {
|
|
57
117
|
const ul = '- alpha.\n- beta.\n- gamma.'
|
|
58
118
|
const ol = '1. first.\n2. second.\n3. third.'
|
|
@@ -275,6 +335,37 @@ describe('normalizeParagraphBreaks', () => {
|
|
|
275
335
|
const input = '- first item\n continued text of the first item'
|
|
276
336
|
expect(normalizeParagraphBreaks(input)).toBe(input)
|
|
277
337
|
})
|
|
338
|
+
|
|
339
|
+
test('F2: prose with an interior ` | ` gets normal paragraph-break treatment (not table)', () => {
|
|
340
|
+
// `isMarkerLine` used to treat any line containing ` | ` as a table row and
|
|
341
|
+
// suppress the paragraph-break promotion. A sentence like "choose A | B" is
|
|
342
|
+
// prose, not a table — the lone `\n` after a terminated prose line must
|
|
343
|
+
// still be promoted to a GFM hard break.
|
|
344
|
+
const input = 'Pick one option, choose A | B here.\nThen continue below.'
|
|
345
|
+
expect(normalizeParagraphBreaks(input)).toBe(
|
|
346
|
+
'Pick one option, choose A | B here. \nThen continue below.',
|
|
347
|
+
)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
test('F2: a genuine table (header + delimiter) is still detected as structural', () => {
|
|
351
|
+
// The tightened check must still recognise a real GFM table: a blank line
|
|
352
|
+
// is inserted before the header and the rows stay contiguous.
|
|
353
|
+
const input = 'Here is data.\n| a | b |\n| --- | --- |\n| 1 | 2 |'
|
|
354
|
+
expect(normalizeParagraphBreaks(input)).toBe(
|
|
355
|
+
'Here is data.\n\n| a | b |\n| --- | --- |\n| 1 | 2 |',
|
|
356
|
+
)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('F4: blank line inserted after a closed code fence before glued prose', () => {
|
|
360
|
+
// Prose glued directly onto a fence close (single `\n`) can be swallowed /
|
|
361
|
+
// mis-parsed. A blank line after a closed fence is always CommonMark-safe.
|
|
362
|
+
const input = 'Intro.\n\n```\nconst x = 1\n```\nThen the prose continues.'
|
|
363
|
+
const out = normalizeParagraphBreaks(input)
|
|
364
|
+
// The fence interior is untouched...
|
|
365
|
+
expect(out).toContain('```\nconst x = 1\n```')
|
|
366
|
+
// ...and a blank line now separates the fence close from the following prose.
|
|
367
|
+
expect(out).toContain('```\n\nThen the prose continues.')
|
|
368
|
+
})
|
|
278
369
|
})
|
|
279
370
|
|
|
280
371
|
// ---------------------------------------------------------------------------
|
|
@@ -383,40 +474,48 @@ describe('addParagraphSpacers', () => {
|
|
|
383
474
|
expect(addParagraphSpacers(input)).toBe(input)
|
|
384
475
|
})
|
|
385
476
|
|
|
386
|
-
test('
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
expect(addParagraphSpacers(
|
|
477
|
+
test('spaces prose↔list transitions (uniform block spacing), list interior tight', () => {
|
|
478
|
+
expect(addParagraphSpacers('Here are the steps.\n\n- first\n- second')).toBe(
|
|
479
|
+
`Here are the steps.${gap}- first\n- second`,
|
|
480
|
+
)
|
|
481
|
+
expect(addParagraphSpacers('- first\n- second\n\nClosing prose after the list.')).toBe(
|
|
482
|
+
`- first\n- second${gap}Closing prose after the list.`,
|
|
483
|
+
)
|
|
391
484
|
})
|
|
392
485
|
|
|
393
|
-
test('
|
|
394
|
-
const
|
|
395
|
-
expect(addParagraphSpacers(
|
|
486
|
+
test('spaces a prose→table boundary; table rows stay contiguous', () => {
|
|
487
|
+
const table = '| a | b |\n| --- | --- |\n| 1 | 2 |'
|
|
488
|
+
expect(addParagraphSpacers(`Intro.\n\n${table}`)).toBe(`Intro.${gap}${table}`)
|
|
396
489
|
})
|
|
397
490
|
|
|
398
|
-
test('
|
|
491
|
+
test('spaces prose↔fence boundaries; fence interior untouched', () => {
|
|
399
492
|
const input = 'Look here.\n\n```js\nconst a = 1;\n```\n\nDone.'
|
|
400
|
-
|
|
401
|
-
|
|
493
|
+
expect(addParagraphSpacers(input)).toBe(
|
|
494
|
+
`Look here.${gap}\`\`\`js\nconst a = 1;\n\`\`\`${gap}Done.`,
|
|
495
|
+
)
|
|
402
496
|
})
|
|
403
497
|
|
|
404
|
-
test('
|
|
498
|
+
test('spaces heading→anything and blockquote boundaries', () => {
|
|
405
499
|
expect(addParagraphSpacers('Intro prose.\n\n## Section\n\nBody prose.')).toBe(
|
|
406
|
-
|
|
500
|
+
`Intro prose.${gap}## Section${gap}Body prose.`,
|
|
407
501
|
)
|
|
408
502
|
expect(addParagraphSpacers('Said.\n\n> a quote\n\nAfter.')).toBe(
|
|
409
|
-
|
|
503
|
+
`Said.${gap}> a quote${gap}After.`,
|
|
410
504
|
)
|
|
411
505
|
})
|
|
412
506
|
|
|
507
|
+
test('does NOT space between items of the same loose list / same-kind blocks', () => {
|
|
508
|
+
const looseList = '- first\n\n- second\n\n- third'
|
|
509
|
+
expect(addParagraphSpacers(looseList)).toBe(looseList)
|
|
510
|
+
const quotes = '> one\n\n> two'
|
|
511
|
+
expect(addParagraphSpacers(quotes)).toBe(quotes)
|
|
512
|
+
})
|
|
513
|
+
|
|
413
514
|
test('never reaches inside a fenced block (interior blank line untouched)', () => {
|
|
414
515
|
const input = 'Before.\n\n```\nline 1\n\nline 2\n```\n\nAfter.'
|
|
415
516
|
const out = addParagraphSpacers(input)
|
|
416
517
|
// The fence interior — including its own blank line — is byte-for-byte intact.
|
|
417
518
|
expect(out).toContain('```\nline 1\n\nline 2\n```')
|
|
418
|
-
// And no spacer paragraph was injected anywhere (both gaps touch the fence).
|
|
419
|
-
expect(out).not.toContain(PARAGRAPH_SPACER)
|
|
420
519
|
})
|
|
421
520
|
|
|
422
521
|
test('a body with no paragraph gap is returned unchanged', () => {
|
|
@@ -435,9 +534,9 @@ describe('addParagraphSpacers', () => {
|
|
|
435
534
|
const out = addParagraphSpacers(normalizeParagraphBreaks(input))
|
|
436
535
|
// The two prose paragraphs gain a visible spacer between them.
|
|
437
536
|
expect(out).toContain(`Summary of the change.${gap}It does two things.`)
|
|
438
|
-
// The list
|
|
439
|
-
|
|
440
|
-
expect(out).
|
|
537
|
+
// The prose→list transition gains a spacer too (uniform block spacing),
|
|
538
|
+
// but the list INTERIOR stays tight.
|
|
539
|
+
expect(out).toContain(`It does two things.${gap}- adds a normalizer\n- lifts the cap`)
|
|
441
540
|
})
|
|
442
541
|
})
|
|
443
542
|
|
|
@@ -470,3 +569,86 @@ describe('normalizeParagraphBreaks — inline bullet split integration', () => {
|
|
|
470
569
|
expect(twice).toBe(once)
|
|
471
570
|
})
|
|
472
571
|
})
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Regression guard for the stray whitespace-only blank line between paragraphs
|
|
575
|
+
* (the "\n\n \n\n" seen in a real reply). A model (or an upstream transform)
|
|
576
|
+
* that authors a blank line whose only content is ASCII whitespace leaves a
|
|
577
|
+
* ragged / oversized gap: CommonMark discards it so it buys no visible space,
|
|
578
|
+
* but it reads as noise in the raw text. Step 1 of normalizeParagraphBreaks now
|
|
579
|
+
* collapses any run of blank lines — including whitespace-only interior lines —
|
|
580
|
+
* to exactly one clean `\n\n`, WITHOUT ever eating the deliberate U+00A0
|
|
581
|
+
* paragraph spacer that addParagraphSpacers (#2692) adds later for a visible
|
|
582
|
+
* gap on the rich-message path.
|
|
583
|
+
*/
|
|
584
|
+
describe('normalizeParagraphBreaks — whitespace-only blank-line collapse (Bug 2)', () => {
|
|
585
|
+
const NBSP = ' '
|
|
586
|
+
|
|
587
|
+
test('a lone-space blank line between paragraphs collapses to a clean `\\n\\n` (real string)', () => {
|
|
588
|
+
const input =
|
|
589
|
+
"Here's what was fixed and how it validates.\n\n \n\n" +
|
|
590
|
+
'One loose end: a stray thing.'
|
|
591
|
+
expect(normalizeParagraphBreaks(input)).toBe(
|
|
592
|
+
"Here's what was fixed and how it validates.\n\n" +
|
|
593
|
+
'One loose end: a stray thing.',
|
|
594
|
+
)
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
test('a single whitespace-only blank line (`A\\n \\nB`) collapses to `\\n\\n`', () => {
|
|
598
|
+
expect(normalizeParagraphBreaks('Alpha para.\n \nBravo para.')).toBe(
|
|
599
|
+
'Alpha para.\n\nBravo para.',
|
|
600
|
+
)
|
|
601
|
+
expect(normalizeParagraphBreaks('Alpha para.\n\t\nBravo para.')).toBe(
|
|
602
|
+
'Alpha para.\n\nBravo para.',
|
|
603
|
+
)
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
test('multiple stray whitespace-only lines in one gap collapse to a single `\\n\\n`', () => {
|
|
607
|
+
expect(normalizeParagraphBreaks('A.\n\n \n\n \n\nB.')).toBe('A.\n\nB.')
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
test('a legitimate single `\\n\\n` paragraph break is preserved exactly', () => {
|
|
611
|
+
expect(normalizeParagraphBreaks('Alpha para.\n\nBravo para.')).toBe(
|
|
612
|
+
'Alpha para.\n\nBravo para.',
|
|
613
|
+
)
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
test('never EMITS a whitespace-only line between two prose paragraphs', () => {
|
|
617
|
+
const out = normalizeParagraphBreaks('First.\n\n \n\nSecond.')
|
|
618
|
+
for (const line of out.split('\n')) {
|
|
619
|
+
// Every line is either empty or has real (non-ASCII-whitespace) content.
|
|
620
|
+
expect(line === '' || /\S/.test(line) || line.includes(NBSP)).toBe(true)
|
|
621
|
+
}
|
|
622
|
+
expect(out).toBe('First.\n\nSecond.')
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
test('a blank line inside a fenced code block (masked) is NOT collapsed', () => {
|
|
626
|
+
const input = '```\ncode line 1\n \ncode line 2\n```'
|
|
627
|
+
// The whitespace-only line lives inside a masked fence and must survive.
|
|
628
|
+
expect(normalizeParagraphBreaks(input)).toBe(input)
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
test('trailing hard-break spaces on a CONTENT line are not eaten', () => {
|
|
632
|
+
// "line. \n\nNext." — the two trailing spaces are a GFM hard break on a
|
|
633
|
+
// content line, not a blank line; they must survive.
|
|
634
|
+
const input = 'Trailing spaces on line. \n\nNext.'
|
|
635
|
+
expect(normalizeParagraphBreaks(input)).toBe(input)
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
test('the deliberate U+00A0 spacer from addParagraphSpacers is preserved (no #2692 regression)', () => {
|
|
639
|
+
const normalized = normalizeParagraphBreaks('Alpha para.\n\nBravo para.')
|
|
640
|
+
const spaced = addParagraphSpacers(normalized)
|
|
641
|
+
// addParagraphSpacers wedges a U+00A0-only line to force a visible gap.
|
|
642
|
+
expect(spaced).toContain('\n\n' + PARAGRAPH_SPACER + '\n\n')
|
|
643
|
+
expect(spaced.split('\n')).toContain(NBSP)
|
|
644
|
+
// And re-running the normalizer must NOT eat that intentional spacer.
|
|
645
|
+
expect(normalizeParagraphBreaks(spaced)).toBe(spaced)
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
test('idempotent: collapsing a stray gap twice is stable', () => {
|
|
649
|
+
const once = normalizeParagraphBreaks('A.\n\n \n\nB.')
|
|
650
|
+
const twice = normalizeParagraphBreaks(once)
|
|
651
|
+
expect(twice).toBe(once)
|
|
652
|
+
expect(once).toBe('A.\n\nB.')
|
|
653
|
+
})
|
|
654
|
+
})
|