@poodle64/librarian 2026.9.4 → 2026.9.6

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/README.md CHANGED
@@ -135,4 +135,18 @@ pnpm run test # build + vitest
135
135
 
136
136
  1. Change a component; bump `version` in `package.json` (CalVer).
137
137
  2. `pnpm build`, which runs `svelte-package` then `publint`.
138
- 3. Commit, tag `librarian-v<version>`, push the tag; CI publishes to public npm.
138
+ 3. Commit, tag `librarian-v<version>`, push the tag.
139
+ 4. `.github/workflows/publish.yaml` runs on that push and should publish via
140
+ npm OIDC trusted publishing: check it with
141
+ `gh run list --workflow=publish.yaml`. As of 2026.9.5 every run since
142
+ `ui-v2026.9.2` fails at the `npm publish` step with
143
+ `E404 Not Found - PUT .../@poodle64%2flibrarian` (an OIDC/trusted-publisher
144
+ binding issue, since `Build and test package` passes first). Until that
145
+ is diagnosed and fixed, publish manually from `packages/librarian/`:
146
+ ```bash
147
+ printf '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\n' > .npmrc
148
+ signet exec --identity huginn-claude --broker https://portcullis.godswood.au \
149
+ --credential npm-publish-token --env-var NODE_AUTH_TOKEN -- npm publish
150
+ rm -f .npmrc
151
+ ```
152
+ Confirm with `npm view @poodle64/librarian version`.
package/dist/client.js CHANGED
@@ -23,7 +23,7 @@ export async function* ask(options) {
23
23
  signal: options.signal
24
24
  });
25
25
  if (!response.ok || !response.body) {
26
- yield { type: 'library_error', error: `The agent answered ${response.status}.` };
26
+ yield { type: 'library_error', error: "Milton can't be reached right now." };
27
27
  return;
28
28
  }
29
29
  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
@@ -39,16 +39,16 @@
39
39
  onstop
40
40
  }: Props = $props();
41
41
 
42
- // What the agent is being asked about — chat with this document, this
43
- // collection, or the whole library. Callers that have no narrower scope
44
- // pass neither name, leaving "All collections" the sole entry — and a
45
- // sole entry is not a choice, so `hasChoice` below hides it rather than
46
- // rendering a chip with nothing to switch to.
42
+ // What Milton is being asked about — this document, this collection, or
43
+ // the whole library. Callers that have no narrower scope pass neither
44
+ // name, leaving "The whole library" the sole entry — and a sole entry is
45
+ // not a choice, so `hasChoice` below hides it rather than rendering a
46
+ // chip with nothing to switch to.
47
47
  const choices = $derived(
48
48
  [
49
49
  documentName ? { id: 'document' as const, label: documentName } : null,
50
50
  collectionName ? { id: 'collection' as const, label: collectionName } : null,
51
- { id: 'library' as const, label: 'All collections' }
51
+ { id: 'library' as const, label: 'The whole library' }
52
52
  ].filter((c) => c !== null)
53
53
  );
54
54
 
@@ -79,7 +79,7 @@
79
79
  bind:value
80
80
  onkeydown={keydown}
81
81
  rows="2"
82
- placeholder={running ? 'Queue another message…' : 'Ask anything'}
82
+ placeholder={running ? 'Queue another message…' : 'Ask Milton…'}
83
83
  class="text-foreground placeholder:text-muted-foreground max-h-52 w-full resize-none bg-transparent px-4 pt-3 pb-2 text-base outline-none"
84
84
  ></textarea>
85
85
 
@@ -5,14 +5,13 @@
5
5
  dimmed sub-line. Borders and badge pills make a run of five calls read as
6
6
  five separate events rather than one train of thought.
7
7
 
8
- The row says what the agent is DOING, not what it typed. A reader here is
9
- asking about documents, not reading a terminal, and `Bash ls -1 .` looks
10
- like a leak from the machine room. The raw command is one click away, so
11
- nothing is hidden from anyone who wants it.
8
+ The row says what Milton DID, in a reader's own words never the tool's
9
+ name or the raw command it ran. Expanding a settled row shows what came
10
+ back, never what was typed.
12
11
  -->
13
12
  <script lang="ts">
14
13
  import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
15
- import { describe, summarise, type ToolBlock } from '../../transcript.svelte';
14
+ import { describe, type ToolBlock } from '../../transcript.svelte';
16
15
 
17
16
  interface Props {
18
17
  block: ToolBlock;
@@ -53,17 +52,12 @@
53
52
  <span class="text-muted-foreground truncate">
54
53
  <span class="text-foreground font-medium">{said.verb}</span>
55
54
  {#if said.object}<span class="text-foreground/80">{said.object}</span>{/if}
56
- {#if repeats > 1}<span class="text-muted-foreground">· {repeats} sections</span>{/if}
55
+ {#if repeats > 1}<span class="text-muted-foreground">· {repeats} pages</span>{/if}
57
56
  </span>
58
57
  </button>
59
58
 
60
59
  {#if open}
61
60
  <div class="border-border mt-1 ml-5.5 border-l pl-3">
62
- <!-- The real command, for anyone who wants it. -->
63
- <p class="text-muted-foreground font-mono text-xs break-all">
64
- {block.name}
65
- {summarise(block)}
66
- </p>
67
61
  {#if block.result}
68
62
  <pre
69
63
  class="text-muted-foreground mt-1 max-h-72 overflow-auto font-mono text-xs whitespace-pre-wrap">{block.result}</pre>
@@ -18,15 +18,7 @@
18
18
 
19
19
  let { label }: Props = $props();
20
20
 
21
- const WORDS = [
22
- 'Starting',
23
- 'Reading the shelves',
24
- 'Rummaging',
25
- 'Cross-checking',
26
- 'Thumbing pages',
27
- 'Following a reference',
28
- 'Chasing it down'
29
- ];
21
+ const WORDS = ['Milton is looking', 'Milton is reading'];
30
22
 
31
23
  let tick = $state(0);
32
24
  let elapsed = $state(0);
@@ -34,12 +34,13 @@ export interface Outcome {
34
34
  isError?: boolean;
35
35
  error?: string;
36
36
  }
37
- /** What the agent is DOING, in words a reader who has never seen a shell knows.
37
+ /** What Milton DID, in a reader's own words never the tool's name or the
38
+ * raw command it ran.
38
39
  *
39
40
  * The collapsed row is read by someone asking a question about documents, not
40
41
  * by an engineer: `Bash ls -1 .` tells them nothing and looks like a leak from
41
- * the machine room. The raw command is still one click away on expand, so this
42
- * hides nothing it just stops the transcript opening with jargon.
42
+ * the machine room. Milton never narrates how he searched, so a tool this
43
+ * does not recognise falls back to something that names no mechanism at all.
43
44
  */
44
45
  export declare function describe(block: ToolBlock): {
45
46
  verb: string;
@@ -101,5 +102,9 @@ export type Segment = ActivityGroup | TextBlock;
101
102
  * steps with the same label collapse to one row carrying a count.
102
103
  */
103
104
  export declare function segment(blocks: Block[]): Segment[];
104
- /** One line describing a whole investigation, for the collapsed state. */
105
+ /** One line describing a whole investigation, for the collapsed state.
106
+ *
107
+ * Counts of what Milton did are fine ("1 search · 2 documents read"); how he
108
+ * organises what he knows is not — no collection count, no shelf, no corpus.
109
+ */
105
110
  export declare function summariseActivity(group: ActivityGroup): string;
@@ -6,43 +6,44 @@
6
6
  * deltas that belong to each. An event type this does not know about is kept
7
7
  * verbatim under `other`, so nothing is silently dropped.
8
8
  */
9
- /** What the agent is DOING, in words a reader who has never seen a shell knows.
9
+ /** What Milton DID, in a reader's own words never the tool's name or the
10
+ * raw command it ran.
10
11
  *
11
12
  * The collapsed row is read by someone asking a question about documents, not
12
13
  * by an engineer: `Bash ls -1 .` tells them nothing and looks like a leak from
13
- * the machine room. The raw command is still one click away on expand, so this
14
- * hides nothing it just stops the transcript opening with jargon.
14
+ * the machine room. Milton never narrates how he searched, so a tool this
15
+ * does not recognise falls back to something that names no mechanism at all.
15
16
  */
16
17
  export function describe(block) {
17
18
  const input = parseInput(block);
18
19
  const command = typeof input.command === 'string' ? input.command : '';
19
20
  if (block.name === 'Read') {
20
21
  const path = str(input.file_path ?? input.path);
21
- return { verb: 'Reading', object: documentName(path) };
22
+ return { verb: 'Read', object: documentName(path) };
22
23
  }
23
24
  if (block.name === 'Grep') {
24
- return { verb: 'Searching for', object: str(input.pattern) };
25
+ return { verb: 'Looked for', object: str(input.pattern) };
25
26
  }
26
27
  if (block.name === 'Glob') {
27
- return { verb: 'Looking for files', object: str(input.pattern) };
28
+ return { verb: 'Looked for documents', object: '' };
28
29
  }
29
30
  if (block.name === 'Bash') {
30
31
  if (/\bgrep\b|\brg\b/.test(command)) {
31
32
  const quoted = command.match(/["']([^"']{2,60})["']/);
32
- return { verb: 'Searching for', object: quoted?.[1] ?? 'a phrase' };
33
+ return { verb: 'Looked for', object: quoted?.[1] ?? 'a phrase' };
33
34
  }
34
35
  if (/\bls\b/.test(command))
35
- return { verb: 'Listing', object: listTarget(command) };
36
+ return { verb: 'Looked through the library', object: '' };
36
37
  if (/\bfind\b/.test(command))
37
- return { verb: 'Looking for files', object: '' };
38
+ return { verb: 'Looked for documents', object: '' };
38
39
  if (/\bcat\b|\bhead\b|\bsed\b/.test(command)) {
39
- return { verb: 'Reading', object: documentName(lastPath(command)) };
40
+ return { verb: 'Read', object: documentName(lastPath(command)) };
40
41
  }
41
42
  if (/\bwc\b/.test(command))
42
- return { verb: 'Counting', object: '' };
43
- return { verb: 'Running a command', object: '' };
43
+ return { verb: 'Checked', object: '' };
44
+ return { verb: 'Looked into it', object: '' };
44
45
  }
45
- return { verb: block.name, object: summarise(block) };
46
+ return { verb: 'Looked into it', object: '' };
46
47
  }
47
48
  function parseInput(block) {
48
49
  try {
@@ -64,12 +65,6 @@ function documentName(path) {
64
65
  return prettify(segments.at(-2) ?? '');
65
66
  return prettify(page.replace(/\.md$/, ''));
66
67
  }
67
- function listTarget(command) {
68
- const target = command.trim().split(/\s+/).at(-1) ?? '';
69
- if (!target || target === '.' || target.startsWith('-'))
70
- return 'the collections';
71
- return prettify(target.replace(/\/$/, ''));
72
- }
73
68
  function lastPath(command) {
74
69
  return (command
75
70
  .split(/\s+/)
@@ -155,7 +150,7 @@ export class Transcript {
155
150
  return;
156
151
  }
157
152
  if (event.type === 'library_error') {
158
- this.outcome = { isError: true, error: event.error ?? 'the agent failed' };
153
+ this.outcome = { isError: true, error: event.error ?? "Milton can't be reached right now." };
159
154
  return;
160
155
  }
161
156
  // A tool RESULT arrives as a user message carrying tool_result blocks.
@@ -289,9 +284,9 @@ function sameStep(a, b) {
289
284
  }
290
285
  function tally(group, block) {
291
286
  const { verb } = describe(block);
292
- if (verb === 'Searching for')
287
+ if (verb === 'Looked for')
293
288
  group.searches += 1;
294
- if (verb === 'Reading')
289
+ if (verb === 'Read')
295
290
  group.documents += 1;
296
291
  const collection = collectionOf(block);
297
292
  if (collection && !group.collections.includes(collection))
@@ -303,14 +298,16 @@ function collectionOf(block) {
303
298
  const match = raw.match(/(?:^|["'\s/])([a-z0-9]+(?:-[a-z0-9]+)+)\/local\//);
304
299
  return match?.[1] ?? '';
305
300
  }
306
- /** One line describing a whole investigation, for the collapsed state. */
301
+ /** One line describing a whole investigation, for the collapsed state.
302
+ *
303
+ * Counts of what Milton did are fine ("1 search · 2 documents read"); how he
304
+ * organises what he knows is not — no collection count, no shelf, no corpus.
305
+ */
307
306
  export function summariseActivity(group) {
308
307
  const parts = [];
309
308
  if (group.searches)
310
309
  parts.push(`${group.searches} search${group.searches === 1 ? '' : 'es'}`);
311
310
  if (group.documents)
312
311
  parts.push(`${group.documents} document${group.documents === 1 ? '' : 's'} read`);
313
- if (group.collections.length)
314
- parts.push(`${group.collections.length} collection${group.collections.length === 1 ? '' : 's'}`);
315
- return parts.length ? parts.join(' · ') : 'Worked on it';
312
+ return parts.length ? parts.join(' · ') : 'Looked into it';
316
313
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poodle64/librarian",
3
- "version": "2026.9.4",
3
+ "version": "2026.9.6",
4
4
  "description": "Milton's conversation surface as a consumable Svelte 5 package: the stream client, transcript state and chat components (transcript, composer, markdown) every household app renders instead of rebuilding.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- export {};
@@ -1,40 +0,0 @@
1
- /**
2
- * The scope chip renders only when there is a genuine pick between two or
3
- * more scopes (design-system, the fixed-scope Composer defect). A room with
4
- * one shelf set passes neither `documentName` nor `collectionName`, leaving
5
- * "All collections" as the sole entry — and a sole entry is console
6
- * furniture, not a control.
7
- */
8
- import { describe, it, expect } from 'vitest';
9
- import { render, screen } from '@testing-library/svelte';
10
- import Composer from './composer.svelte';
11
- describe('Composer scope chip', () => {
12
- it('renders nothing when the caller has fixed the scope', () => {
13
- render(Composer, {
14
- props: {
15
- value: '',
16
- running: false,
17
- scope: 'library',
18
- onscope: () => { },
19
- onsubmit: () => { },
20
- onstop: () => { }
21
- }
22
- });
23
- expect(screen.queryByText('All collections')).not.toBeInTheDocument();
24
- });
25
- it('renders chips once a narrower scope is offered', () => {
26
- render(Composer, {
27
- props: {
28
- value: '',
29
- running: false,
30
- scope: 'library',
31
- collectionName: 'household-legal',
32
- onscope: () => { },
33
- onsubmit: () => { },
34
- onstop: () => { }
35
- }
36
- });
37
- expect(screen.getByText('All collections')).toBeInTheDocument();
38
- expect(screen.getByText('household-legal')).toBeInTheDocument();
39
- });
40
- });