@patimweb/pi-outline-wiki 1.0.0 → 1.1.1

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
@@ -29,6 +29,7 @@ outline_setup:
29
29
  name: work
30
30
  url: https://acme.getoutline.com
31
31
  apiKey: ol_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
32
+ description: internal engineering wiki
32
33
  ```
33
34
 
34
35
  `outline_setup` verifies the connection immediately and reports the workspace and the authenticated user, so a typo in the URL or key surfaces right away.
@@ -41,10 +42,10 @@ The `url` is forgiving: a bare host, a trailing slash, a pasted `/api` base or t
41
42
  |------|-------------|
42
43
  | `outline_setup` | Configure workspace URL and API key. Must be called first. |
43
44
  | `outline_status` | Show configured profiles and verify the active key still works. |
44
- | `outline_profile` | List, switch, or delete workspace profiles (multi-workspace support). |
45
+ | `outline_profile` | List, switch, or delete workspace profiles, with what each wiki holds. |
45
46
  | `outline_collections` | List collections with their ids. |
46
47
  | `outline_structure` | Show the nested document tree of one collection. |
47
- | `outline_search` | Full-text search with optional collection, status and date filters. |
48
+ | `outline_search` | Full-text search with optional collection, status and date filters; can search all workspaces at once. |
48
49
  | `outline_list` | Browse documents: recent, by collection, by parent, or your drafts. |
49
50
  | `outline_read` | Read a document's markdown body, optionally with children or saved to a file. |
50
51
  | `outline_create` | Create a document in a collection or under a parent document. |
@@ -70,6 +71,16 @@ outline_search:
70
71
 
71
72
  Archived documents are excluded unless you ask for them via `statusFilter`.
72
73
 
74
+ With several workspaces configured, `allProfiles: true` searches every one of them and labels each hit with the workspace it came from:
75
+
76
+ ```yaml
77
+ outline_search:
78
+ query: postgres failover
79
+ allProfiles: true
80
+ ```
81
+
82
+ `allProfiles` cannot be combined with `profile`, `collectionId` or `documentId` -- those are specific to a single workspace, and the tool rejects the combination rather than silently returning nothing.
83
+
73
84
  ### Reading
74
85
 
75
86
  `outline_read` takes the document id (uuid) or the slug from an Outline URL. Long pages are truncated at 20,000 characters by default so a single wiki page cannot eat the whole context window.
@@ -174,7 +185,34 @@ Credentials are persisted to `~/.pi/outline-config.json`, written atomically wit
174
185
 
175
186
  ### Multiple workspaces
176
187
 
177
- Every workspace is stored under a profile name. The first profile becomes active automatically; use `outline_profile` to list, switch, or delete profiles. Every read and write tool also accepts a one-off `profile` parameter.
188
+ Several Outline wikis can be configured side by side -- call `outline_setup` once per wiki with a different `name`. The first profile becomes active automatically; later ones do not steal the active slot.
189
+
190
+ ```yaml
191
+ outline_setup:
192
+ name: firma
193
+ url: https://acme.getoutline.com
194
+ apiKey: ol_api_xxx
195
+ description: internal engineering wiki
196
+
197
+ outline_setup:
198
+ name: kunde-a
199
+ url: https://wiki.kunde-a.de
200
+ apiKey: ol_api_yyy
201
+ description: ACME client project docs
202
+ ```
203
+
204
+ **Give every workspace a `description`.** It is shown by `outline_profile` and `outline_status`, and it is what lets the agent decide which wiki to search instead of guessing from a profile name. The bundled skills instruct the agent to check that list before searching, and to ask you rather than guess when a *write* could land in the wrong wiki.
205
+
206
+ | Need | How |
207
+ |------|-----|
208
+ | See what is configured | `outline_profile` |
209
+ | Switch the default | `outline_profile: { action: use, name: kunde-a }` |
210
+ | One call against another wiki | any tool + `profile: kunde-a` |
211
+ | Search everything at once | `outline_search` + `allProfiles: true` |
212
+
213
+ Document ids are scoped to their workspace. A hit that a fan-out search reports as `profile kunde-a` must be read with `outline_read` and `profile: kunde-a` -- the same id means nothing in another wiki.
214
+
215
+ If one workspace is unreachable or its key has expired, a fan-out search still returns the results from the others and reports the failure separately.
178
216
 
179
217
  ### Self-hosted instances
180
218
 
package/index.ts CHANGED
@@ -91,7 +91,12 @@ export default function (pi: ExtensionAPI) {
91
91
  }
92
92
 
93
93
  pi.sendUserMessage(
94
- `Search our Outline wiki for "${query}" using outline_search, read the most relevant document(s) with outline_read, and summarise what the wiki says. Cite each document by title and id, and say so plainly if the wiki has nothing on it.`,
94
+ [
95
+ `Search our Outline wiki for "${query}".`,
96
+ "If outline_profile shows more than one workspace, pick the one whose description fits, or search them all with outline_search allProfiles: true.",
97
+ "Then read the most relevant document(s) with outline_read, passing the same profile the hit came from, and summarise what the wiki says.",
98
+ "Cite each document by title and id (and workspace, if several are in play). Say so plainly if the wiki has nothing on it.",
99
+ ].join(" "),
95
100
  { deliverAs: "steer" },
96
101
  );
97
102
  ctx.ui.notify(`Searching the wiki for "${query}"...`, "info");
@@ -112,11 +117,12 @@ export default function (pi: ExtensionAPI) {
112
117
  [
113
118
  "Capture the durable knowledge from this session in our Outline wiki.",
114
119
  hint ? `Focus on: ${hint}.` : "",
115
- "First use outline_search to check whether a page on this already exists.",
120
+ "If outline_profile shows more than one workspace, decide which one this belongs in first and pass profile explicitly on every call; ask me if it is not obvious.",
121
+ "Then use outline_search to check whether a page on this already exists.",
116
122
  "If it does, extend it with outline_update (mode append) instead of creating a duplicate.",
117
123
  "If it does not, use outline_collections to pick the right collection and create the page with outline_create.",
118
124
  "Write what a colleague would need in six months: the decision, why, and what was ruled out -- not a transcript.",
119
- "Show me the page title and URL when you are done.",
125
+ "Show me the workspace, page title and URL when you are done.",
120
126
  ]
121
127
  .filter(Boolean)
122
128
  .join(" "),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patimweb/pi-outline-wiki",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Outline wiki extension for the pi coding agent. Search, read, write, organise and comment on Outline documents from your pi session.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -29,15 +29,17 @@
29
29
  "test:watch": "node --experimental-test-module-mocks --test --watch 'tests/**/*.test.ts'",
30
30
  "test:coverage": "node --experimental-test-module-mocks --experimental-test-coverage --test 'tests/**/*.test.ts'"
31
31
  },
32
- "dependencies": {
33
- "typebox": "^1.2.8"
32
+ "peerDependencies": {
33
+ "typebox": "*"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@semantic-release/git": "^10.0.0",
40
- "semantic-release": "^25.0.0"
40
+ "semantic-release": "^25.0.0",
41
+ "typebox": "^1.3.25",
42
+ "yaml": "^2.9.0"
41
43
  },
42
44
  "pi": {
43
45
  "extensions": [
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: outline-doc-writing
3
- description: Write and edit documents in the Outline wiki so they are findable, correct and safe to change. Use whenever creating a new wiki page, updating an existing one, restructuring a collection, or when the user asks to "document this in the wiki", "write it up in Outline", "update the runbook" or "add a page". Covers where a page belongs, how to structure it, how to update without destroying other people's work, and when to comment instead of edit.
4
- allowed-tools: outline_collections, outline_structure, outline_search, outline_list, outline_read, outline_create, outline_update, outline_move, outline_archive, outline_comment
3
+ description: Write and edit documents in the Outline wiki so they are findable, correct and safe to change. Use whenever creating a new wiki page, updating an existing one, restructuring a collection, or when the user asks to "document this in the wiki", "write it up in Outline", "update the runbook" or "add a page". Covers which workspace and collection a page belongs in, how to structure it, how to update without destroying other people's work, and when to comment instead of edit.
4
+ allowed-tools: outline_profile, outline_collections, outline_structure, outline_search, outline_list, outline_read, outline_create, outline_update, outline_move, outline_archive, outline_comment
5
5
  ---
6
6
 
7
7
  # Writing in the Outline wiki
@@ -9,10 +9,28 @@ allowed-tools: outline_collections, outline_structure, outline_search, outline_l
9
9
  A wiki page you write is read by people who were not in this session. Write
10
10
  for them.
11
11
 
12
+ ## Which workspace
13
+
14
+ Writing into the wrong wiki is worse than reading the wrong one: it is
15
+ visible to the wrong people and it is not obvious to anyone that it happened.
16
+ So when more than one workspace is configured, settle this **before** the
17
+ first write.
18
+
19
+ 1. `outline_profile` lists every workspace with what it holds, and marks the
20
+ active one.
21
+ 2. Pass `profile: <name>` explicitly on every write call. Do not rely on
22
+ whichever workspace happens to be active - that is a setting from an
23
+ earlier session.
24
+ 3. If it is not obvious which workspace the content belongs to, **ask the
25
+ user**. Client material in an internal wiki, or one client's material in
26
+ another client's wiki, is a confidentiality problem, not a filing mistake.
27
+ 4. Say which workspace you wrote to when you report back.
28
+
12
29
  ## Before you create anything
13
30
 
14
- 1. `outline_search` for the topic. **Extending an existing page is almost
15
- always better than adding a second one.** Duplicates are how wikis die.
31
+ 1. `outline_search` for the topic - in the workspace you settled on. **Extending
32
+ an existing page is almost always better than adding a second one.**
33
+ Duplicates are how wikis die.
16
34
  2. If nothing exists, `outline_collections` then `outline_structure` on the
17
35
  likely collection, so the new page lands where people will look for it.
18
36
  3. If the right home is ambiguous, ask the user which collection rather than
@@ -74,5 +92,6 @@ the content is fine but the location is wrong.
74
92
 
75
93
  ## Finish the job
76
94
 
77
- After writing, tell the user the page title, its id and its URL, and say in
78
- one line what you put there. If you created a draft, say that too.
95
+ After writing, tell the user the workspace, the page title, its id and its
96
+ URL, and say in one line what you put there. If you created a draft, say that
97
+ too.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: outline-knowledge-capture
3
3
  description: Turn what was worked out in a coding session into a durable Outline wiki page - architecture decisions, debugging findings, incident write-ups, setup and runbook steps, or gotchas that cost real time. Use when the user says "write this up", "document this decision", "capture this in the wiki", "add this to our docs", "so we don't hit this again", or after any session that produced knowledge which is not obvious from the code alone.
4
- allowed-tools: outline_collections, outline_structure, outline_search, outline_read, outline_create, outline_update, outline_comment
4
+ allowed-tools: outline_profile, outline_collections, outline_structure, outline_search, outline_read, outline_create, outline_update, outline_comment
5
5
  ---
6
6
 
7
7
  # Capturing session knowledge in Outline
@@ -26,14 +26,20 @@ what you tried, or anything you would have to invent details for.
26
26
 
27
27
  ## Procedure
28
28
 
29
- 1. **Check for an existing home.** `outline_search` with the system or
29
+ 1. **Pick the workspace.** If `outline_profile` shows more than one, decide
30
+ which wiki this belongs in before writing anything, and pass `profile`
31
+ explicitly on every call. Work done for a client belongs in that client's
32
+ workspace - never in the internal wiki or another client's, even when the
33
+ technical finding is general. If the session touched more than one context
34
+ and you cannot tell, ask.
35
+ 2. **Check for an existing home.** `outline_search` with the system or
30
36
  component name. A decision about the payments service belongs on or under
31
37
  the payments page, not in a new orphan document.
32
- 2. **Extend before you create.** If a relevant page exists, `outline_read` it
38
+ 3. **Extend before you create.** If a relevant page exists, `outline_read` it
33
39
  and add a dated section with `outline_update` `mode: append`.
34
- 3. **Otherwise create.** `outline_collections` -> `outline_structure` ->
40
+ 4. **Otherwise create.** `outline_collections` -> `outline_structure` ->
35
41
  `outline_create` under the right parent.
36
- 4. **Report back** with the title, id and URL.
42
+ 5. **Report back** with the workspace, title, id and URL.
37
43
 
38
44
  ## What to write
39
45
 
@@ -48,7 +54,9 @@ Answer the four questions a future colleague will actually have:
48
54
  true.
49
55
 
50
56
  Include the concrete artefacts: the exact error message, the command that
51
- reproduces it, the file paths, the ticket or PR link. A page that says "we
57
+ reproduces it, the file paths, the ticket or PR link. Leave out credentials,
58
+ tokens and customer data - a wiki page is readable by everyone in that
59
+ workspace. A page that says "we
52
60
  fixed a timeout issue" helps nobody.
53
61
 
54
62
  ## Templates
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: outline-wiki
3
- description: Research the team's Outline wiki before answering questions about internal systems, conventions, decisions, runbooks or onboarding. Use whenever a question is about "how we do it here" rather than about general knowledge or the code in front of you - for example architecture decisions, deployment steps, naming conventions, ownership, incident history, or anything the user refers to as "the wiki", "our docs", "Confluence-style notes" or Outline. Also use it to check whether the wiki already covers something before writing a new page.
4
- allowed-tools: outline_status, outline_collections, outline_structure, outline_search, outline_list, outline_read, outline_comments, outline_export
3
+ description: Research the team's Outline wiki - or wikis - before answering questions about internal systems, conventions, decisions, runbooks or onboarding. Use whenever a question is about "how we do it here" rather than about general knowledge or the code in front of you - for example architecture decisions, deployment steps, naming conventions, ownership, incident history, or anything the user refers to as "the wiki", "our docs", "Confluence-style notes" or Outline. Also use it to check whether the wiki already covers something before writing a new page.
4
+ allowed-tools: outline_status, outline_profile, outline_collections, outline_structure, outline_search, outline_list, outline_read, outline_comments, outline_export
5
5
  ---
6
6
 
7
7
  # Researching the Outline wiki
@@ -22,18 +22,43 @@ Use the wiki when the answer depends on this organisation:
22
22
  Do **not** use it for general programming questions, or for facts that the
23
23
  repository in front of you answers directly.
24
24
 
25
+ ## Which wiki
26
+
27
+ More than one Outline workspace can be configured, and they are separate
28
+ worlds: a document id from one is meaningless in another.
29
+
30
+ **Start with `outline_profile`** when you have not already seen the list this
31
+ session. It shows every configured workspace with a one-line description of
32
+ what it holds, and marks the active one. Then decide:
33
+
34
+ - The question clearly belongs to one workspace (the description names the
35
+ client, product or team): search that one with `profile: <name>`.
36
+ - You do not know which workspace holds the answer: `outline_search` with
37
+ `allProfiles: true` searches all of them at once and labels each hit with
38
+ the workspace it came from.
39
+ - Only one workspace is configured: ignore all of this and just search.
40
+
41
+ After a fan-out, **carry the profile through**. A hit reported as
42
+ `profile kunde-a` must be read with `outline_read` and `profile: "kunde-a"`,
43
+ or you will get "document not found" from the wrong wiki.
44
+
45
+ If a workspace has no description, say so once and suggest the user re-run
46
+ `outline_setup` with one - without it you are guessing from the name.
47
+
25
48
  ## The loop
26
49
 
27
- 1. **Search, do not browse.** `outline_search` with two or three distinctive
50
+ 1. **Pick the wiki** (above), if more than one is configured.
51
+ 2. **Search, do not browse.** `outline_search` with two or three distinctive
28
52
  nouns. "postgres failover" beats "how does our database failover work".
29
- 2. **Read the top hits.** The search snippet is not the answer.
30
- `outline_read` on the one or two most promising ids.
31
- 3. **Follow the structure.** If a page looks like an index, use
53
+ 3. **Read the top hits.** The search snippet is not the answer.
54
+ `outline_read` on the one or two most promising ids - with the same
55
+ `profile` the hit came from.
56
+ 4. **Follow the structure.** If a page looks like an index, use
32
57
  `outline_read` with `includeChildren: true`, or `outline_structure` on its
33
58
  collection, to see what sits under it.
34
- 4. **Answer with citations.** Name the document title and id you used, so the
35
- user can open it. If several pages disagree, say so and give the update
36
- dates - wikis rot.
59
+ 5. **Answer with citations.** Name the document title, id and - when several
60
+ workspaces are in play - the workspace you took it from. If several pages
61
+ disagree, say so and give the update dates: wikis rot.
37
62
 
38
63
  ## Search that actually finds things
39
64
 
@@ -43,14 +68,17 @@ repository in front of you answers directly.
43
68
  form before concluding the wiki is silent. Teams write "k8s" and
44
69
  "Kubernetes" in different pages.
45
70
  - Recent-work questions ("what changed lately?") are `outline_list`, not
46
- `outline_search`.
71
+ `outline_search`. `outline_list` works on one workspace at a time.
72
+ - `allProfiles` cannot be combined with `collectionId` or `documentId`: those
73
+ ids only exist in one workspace. Narrow the workspace first, then filter.
47
74
  - Something that existed but is gone is usually archived, not deleted:
48
75
  re-run the search with `statusFilter: ["published", "archived"]`.
49
76
 
50
77
  ## Ids
51
78
 
52
- Every tool takes a document id. Ids come from search, list or structure
53
- output - never invent one. An Outline URL like
79
+ Every tool takes a document id, and ids are scoped to one workspace. They
80
+ come from search, list or structure output - never invent one, and never
81
+ carry one from one workspace to another. An Outline URL like
54
82
  `https://acme.getoutline.com/doc/runbook-deploy-aBc123XyZ` also works: pass
55
83
  the trailing `runbook-deploy-aBc123XyZ` slug as `id`.
56
84
 
@@ -59,19 +59,99 @@ export function formatProfileStatus(
59
59
  return "No Outline workspace configured. Use outline_setup with your workspace URL and an API key from Settings -> API & Apps.";
60
60
  }
61
61
 
62
- const lines = names.map((name) => {
62
+ const lines: string[] = [];
63
+ for (const name of names) {
63
64
  const config = profiles[name];
64
65
  const marker = name === active ? "*" : " ";
65
66
  const insecure = config.insecureTls ? " (TLS validation off)" : "";
66
- return `${marker} ${name}: ${config.url} key ${maskApiKey(config.apiKey)}${insecure}`;
67
- });
67
+ lines.push(
68
+ `${marker} ${name}: ${config.url} key ${maskApiKey(config.apiKey)}${insecure}`,
69
+ );
70
+ // The description is what tells the agent which wiki holds what, so it
71
+ // gets its own line rather than being truncated onto the end of the URL.
72
+ if (config.description) lines.push(` ${config.description}`);
73
+ }
74
+
75
+ const footer =
76
+ names.length > 1
77
+ ? [
78
+ "",
79
+ "Target one workspace with the profile parameter, or set allProfiles: true on outline_search to search all of them at once.",
80
+ ]
81
+ : [];
68
82
 
69
83
  return [
70
- `Outline profiles (${names.length}), * marks the active one:`,
84
+ `Outline workspaces (${names.length}), * marks the active one:`,
71
85
  ...lines,
86
+ ...footer,
72
87
  ].join("\n");
73
88
  }
74
89
 
90
+ /** Search results gathered from several workspaces in one call. */
91
+ export interface ProfileSearchResult {
92
+ readonly profile: string;
93
+ readonly description?: string;
94
+ readonly hits: ReadonlyArray<SearchHit>;
95
+ readonly error?: string;
96
+ }
97
+
98
+ export function formatMultiSearchResults(
99
+ results: ReadonlyArray<ProfileSearchResult>,
100
+ query: string,
101
+ ): string {
102
+ const withHits = results.filter((result) => result.hits.length > 0);
103
+ const failed = results.filter((result) => result.error);
104
+ const total = withHits.reduce((sum, result) => sum + result.hits.length, 0);
105
+
106
+ const sections: string[] = [];
107
+
108
+ if (total === 0) {
109
+ sections.push(
110
+ `No documents match "${query}" in any of the ${results.length} configured workspace(s).`,
111
+ );
112
+ } else {
113
+ sections.push(
114
+ `Search "${query}" across ${results.length} workspace(s) -- ${total} result(s).`,
115
+ "Ids are only valid within their own workspace: pass that workspace as the profile parameter to outline_read.",
116
+ );
117
+ }
118
+
119
+ for (const result of withHits) {
120
+ const label = result.description
121
+ ? `${result.profile} (${result.description})`
122
+ : result.profile;
123
+ const lines = result.hits.map((hit, index) => {
124
+ const context = hit.context.replace(/\s+/g, " ").trim();
125
+ return [
126
+ ` ${index + 1}. ${hit.document.title}${statusMarkers(hit.document)}`,
127
+ ` id ${hit.document.id} | profile ${result.profile} | updated ${shortDate(hit.document.updatedAt)}`,
128
+ context ? ` ${context.slice(0, 300)}` : "",
129
+ ]
130
+ .filter(Boolean)
131
+ .join("\n");
132
+ });
133
+ sections.push(`\n[${label}] ${result.hits.length} result(s):\n${lines.join("\n")}`);
134
+ }
135
+
136
+ const empty = results.filter(
137
+ (result) => !result.error && result.hits.length === 0,
138
+ );
139
+ if (empty.length > 0 && total > 0) {
140
+ sections.push(
141
+ `\nNo matches in: ${empty.map((result) => result.profile).join(", ")}.`,
142
+ );
143
+ }
144
+
145
+ if (failed.length > 0) {
146
+ sections.push(
147
+ `\n${failed.length} workspace(s) could not be searched:`,
148
+ ...failed.map((result) => `- ${result.profile}: ${result.error}`),
149
+ );
150
+ }
151
+
152
+ return sections.join("\n");
153
+ }
154
+
75
155
  export function formatWorkspaceInfo(info: WorkspaceInfo): string {
76
156
  const lines = [
77
157
  `Workspace: ${info.teamName ?? "(unknown)"}`,
@@ -3,9 +3,13 @@
3
3
  */
4
4
 
5
5
  import { Type } from "typebox";
6
- import { resolveConfig } from "../config.ts";
6
+ import { getProfiles, resolveConfig } from "../config.ts";
7
7
  import { searchDocuments } from "../clients/outline-client.ts";
8
- import { formatSearchResults } from "../formatting/formatters.ts";
8
+ import {
9
+ formatMultiSearchResults,
10
+ formatSearchResults,
11
+ type ProfileSearchResult,
12
+ } from "../formatting/formatters.ts";
9
13
 
10
14
  const VALID_STATUS = new Set(["draft", "archived", "published"]);
11
15
  const VALID_DATE = new Set(["day", "week", "month", "year"]);
@@ -14,7 +18,7 @@ export const OutlineSearchTool = {
14
18
  name: "outline_search",
15
19
  label: "Outline Search",
16
20
  description:
17
- "Full-text search across the Outline wiki. Returns matching documents with a snippet and their ids; read the full text with outline_read. Prefer two or three distinctive keywords over long sentences.",
21
+ "Full-text search across the Outline wiki. Returns matching documents with a snippet and their ids; read the full text with outline_read. Prefer two or three distinctive keywords over long sentences. With several workspaces configured, target one with 'profile' or set allProfiles to search all of them at once.",
18
22
  parameters: Type.Object({
19
23
  query: Type.String({
20
24
  description: "Search keywords. Distinctive nouns work better than full questions.",
@@ -45,6 +49,13 @@ export const OutlineSearchTool = {
45
49
  profile: Type.Optional(
46
50
  Type.String({ description: "Outline profile to use. Defaults to the active one." }),
47
51
  ),
52
+ allProfiles: Type.Optional(
53
+ Type.Boolean({
54
+ description:
55
+ "Search every configured workspace instead of just one, and report which workspace each hit came from. Use it when you do not know which wiki holds the answer. Default false.",
56
+ default: false,
57
+ }),
58
+ ),
48
59
  }),
49
60
 
50
61
  async execute(
@@ -58,6 +69,7 @@ export const OutlineSearchTool = {
58
69
  statusFilter?: string[];
59
70
  dateFilter?: string;
60
71
  profile?: string;
72
+ allProfiles?: boolean;
61
73
  },
62
74
  signal: AbortSignal,
63
75
  ) {
@@ -78,30 +90,88 @@ export const OutlineSearchTool = {
78
90
  );
79
91
  }
80
92
 
93
+ const query = params.query.trim();
94
+ const criteria = {
95
+ query,
96
+ limit: params.limit,
97
+ offset: params.offset,
98
+ collectionId: params.collectionId,
99
+ documentId: params.documentId,
100
+ statusFilter: params.statusFilter,
101
+ dateFilter: params.dateFilter,
102
+ };
103
+
104
+ if (params.allProfiles) {
105
+ if (params.profile) {
106
+ throw new Error(
107
+ "profile and allProfiles contradict each other: pick one workspace, or search all of them.",
108
+ );
109
+ }
110
+ // Collection and document filters are ids that only exist in one
111
+ // workspace, so a fan-out with them set would silently return nothing
112
+ // for every other wiki.
113
+ if (params.collectionId || params.documentId) {
114
+ throw new Error(
115
+ "collectionId and documentId are specific to one workspace and cannot be combined with allProfiles. Search that workspace with the profile parameter instead.",
116
+ );
117
+ }
118
+
119
+ const profiles = getProfiles();
120
+ const names = Object.keys(profiles);
121
+ if (names.length === 0) {
122
+ throw new Error(
123
+ "No Outline workspace configured. Use outline_setup first.",
124
+ );
125
+ }
126
+
127
+ // One slow or broken workspace must not sink the whole search, so each
128
+ // failure is reported alongside the results from the others.
129
+ const results: ProfileSearchResult[] = await Promise.all(
130
+ names.map(async (name) => {
131
+ try {
132
+ const { hits } = await searchDocuments(profiles[name], criteria, signal);
133
+ return { profile: name, description: profiles[name].description, hits };
134
+ } catch (err) {
135
+ return {
136
+ profile: name,
137
+ description: profiles[name].description,
138
+ hits: [],
139
+ error: (err as Error).message,
140
+ };
141
+ }
142
+ }),
143
+ );
144
+
145
+ return {
146
+ content: [
147
+ { type: "text" as const, text: formatMultiSearchResults(results, query) },
148
+ ],
149
+ details: {
150
+ query,
151
+ searchedProfiles: names,
152
+ count: results.reduce((sum, result) => sum + result.hits.length, 0),
153
+ failedProfiles: results.filter((r) => r.error).map((r) => r.profile),
154
+ documents: results.flatMap((result) =>
155
+ result.hits.map((hit) => ({
156
+ id: hit.document.id,
157
+ title: hit.document.title,
158
+ profile: result.profile,
159
+ })),
160
+ ),
161
+ },
162
+ };
163
+ }
164
+
81
165
  const config = resolveConfig(params.profile);
82
- const { hits, pagination } = await searchDocuments(
83
- config,
84
- {
85
- query: params.query.trim(),
86
- limit: params.limit,
87
- offset: params.offset,
88
- collectionId: params.collectionId,
89
- documentId: params.documentId,
90
- statusFilter: params.statusFilter,
91
- dateFilter: params.dateFilter,
92
- },
93
- signal,
94
- );
166
+ const { hits, pagination } = await searchDocuments(config, criteria, signal);
95
167
 
96
168
  return {
97
169
  content: [
98
- {
99
- type: "text" as const,
100
- text: formatSearchResults(hits, params.query.trim(), pagination),
101
- },
170
+ { type: "text" as const, text: formatSearchResults(hits, query, pagination) },
102
171
  ],
103
172
  details: {
104
- query: params.query.trim(),
173
+ query,
174
+ profile: params.profile ?? null,
105
175
  count: hits.length,
106
176
  documents: hits.map((hit) => ({
107
177
  id: hit.document.id,
@@ -29,6 +29,12 @@ export const OutlineSetupTool = {
29
29
  apiKey: Type.String({
30
30
  description: "Outline API key from Settings -> API & Apps (starts with ol_api_).",
31
31
  }),
32
+ description: Type.Optional(
33
+ Type.String({
34
+ description:
35
+ "One line on what this wiki contains, e.g. 'internal engineering wiki' or 'ACME client project docs'. Strongly recommended when several workspaces are configured: it is how the agent decides which wiki to search.",
36
+ }),
37
+ ),
32
38
  insecureTls: Type.Optional(
33
39
  Type.Boolean({
34
40
  description:
@@ -41,6 +47,7 @@ export const OutlineSetupTool = {
41
47
  const config: OutlineConfig = {
42
48
  url: normalizeBaseUrl(params.url),
43
49
  apiKey: params.apiKey.trim(),
50
+ ...(params.description && { description: params.description.trim() }),
44
51
  ...(params.insecureTls !== undefined && { insecureTls: params.insecureTls }),
45
52
  };
46
53
 
package/src/types.ts CHANGED
@@ -15,6 +15,12 @@ export interface OutlineConfig {
15
15
  readonly url: string;
16
16
  /** Personal API key, starts with `ol_api_`. */
17
17
  readonly apiKey: string;
18
+ /**
19
+ * What this wiki is for, in one line ("internal engineering wiki",
20
+ * "client ACME project docs"). With several workspaces configured this is
21
+ * what lets the agent pick the right one instead of guessing from a name.
22
+ */
23
+ readonly description?: string;
18
24
  /**
19
25
  * Skip TLS certificate validation. Only for self-hosted instances behind a
20
26
  * self-signed certificate. Disables validation process-wide, so it is off
@@ -126,6 +132,7 @@ export interface SetupParams {
126
132
  readonly name: string;
127
133
  readonly url: string;
128
134
  readonly apiKey: string;
135
+ readonly description?: string;
129
136
  readonly insecureTls?: boolean;
130
137
  }
131
138