@poodle64/librarian 2026.9.15 → 2026.9.16

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 (2) hide show
  1. package/dist/client.js +42 -19
  2. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -79,8 +79,14 @@ export async function* ask(options) {
79
79
  // A session that expired mid-conversation is the shape this catches: the
80
80
  // POST is redirected to a login page, which answers 200 with HTML, and a
81
81
  // reader whose stream "opened" then waits for frames that can never come.
82
- const kind = response.headers.get('content-type') ?? '';
83
- if (!response.ok || !response.body || !kind.includes('text/event-stream')) {
82
+ //
83
+ // A response that DECLARES something other than an event stream is refused;
84
+ // one that declares nothing is read anyway. A route behind a proxy that
85
+ // drops the header is still streaming, and refusing it here would be this
86
+ // layer breaking a working consumer over a header — the terminal-frame
87
+ // check at the end of this function catches it if it really says nothing.
88
+ const kind = (response.headers.get('content-type') ?? '').toLowerCase();
89
+ if (!response.ok || !response.body || (kind && !kind.includes('text/event-stream'))) {
84
90
  yield { type: 'library_error' };
85
91
  return;
86
92
  }
@@ -92,6 +98,25 @@ export async function* ask(options) {
92
98
  // rather than left to the renderer, which cannot tell a stream that died
93
99
  // from one still arriving.
94
100
  let terminal = false;
101
+ /** One frame's `data:` lines, as the event they carry. */
102
+ function parse(frame) {
103
+ // `data:` only — the type lives inside the payload where Claude Code
104
+ // puts it, so there is no second place to look.
105
+ const data = frame
106
+ .split('\n')
107
+ .filter((l) => l.startsWith('data:'))
108
+ .map((l) => l.slice(5).trim())
109
+ .join('');
110
+ if (!data)
111
+ return null;
112
+ try {
113
+ return JSON.parse(data);
114
+ }
115
+ catch {
116
+ // A partial frame at the end of a stream is not an error.
117
+ return null;
118
+ }
119
+ }
95
120
  for (;;) {
96
121
  let chunk;
97
122
  try {
@@ -111,26 +136,24 @@ export async function* ask(options) {
111
136
  const frame = buffered.slice(0, boundary);
112
137
  buffered = buffered.slice(boundary + 2);
113
138
  boundary = buffered.indexOf('\n\n');
114
- // `data:` only — the type lives inside the payload where Claude Code
115
- // puts it, so there is no second place to look.
116
- const data = frame
117
- .split('\n')
118
- .filter((l) => l.startsWith('data:'))
119
- .map((l) => l.slice(5).trim())
120
- .join('');
121
- if (!data)
139
+ const event = parse(frame);
140
+ if (!event)
122
141
  continue;
123
- try {
124
- const event = JSON.parse(data);
125
- if (event.type === 'result' || event.type === 'library_error')
126
- terminal = true;
127
- yield event;
128
- }
129
- catch {
130
- /* a partial frame at end of stream is not an error */
131
- }
142
+ if (event.type === 'result' || event.type === 'library_error')
143
+ terminal = true;
144
+ yield event;
132
145
  }
133
146
  }
147
+ // The last frame of a stream that closed cleanly without its blank line.
148
+ // It is usually the terminal `result`, carrying the duration and the
149
+ // sources, so dropping it both loses the answer's furniture and makes a
150
+ // finished turn look like one that died.
151
+ const last = parse(buffered);
152
+ if (last) {
153
+ if (last.type === 'result' || last.type === 'library_error')
154
+ terminal = true;
155
+ yield last;
156
+ }
134
157
  if (!terminal && !options.signal?.aborted)
135
158
  yield { type: 'library_error' };
136
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poodle64/librarian",
3
- "version": "2026.9.15",
3
+ "version": "2026.9.16",
4
4
  "description": "An agent's conversation surface as a consumable Svelte 5 package: the stream client, transcript state and self-styling chat components (transcript, composer, markdown) every household app renders instead of rebuilding, speaking as whichever persona the app names.",
5
5
  "type": "module",
6
6
  "license": "MIT",