@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 +41 -3
- package/index.ts +9 -3
- package/package.json +6 -4
- package/skills/outline-doc-writing/SKILL.md +25 -6
- package/skills/outline-knowledge-capture/SKILL.md +14 -6
- package/skills/outline-wiki/SKILL.md +40 -12
- package/src/formatting/formatters.ts +84 -4
- package/src/tools/outline-search.ts +91 -21
- package/src/tools/outline-setup.ts +7 -0
- package/src/types.ts +7 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
33
|
-
"typebox": "
|
|
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
|
|
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
|
|
15
|
-
always better than adding a second one.**
|
|
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
|
|
78
|
-
one line what you put there. If you created a draft, say that
|
|
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. **
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
4. **Otherwise create.** `outline_collections` -> `outline_structure` ->
|
|
35
41
|
`outline_create` under the right parent.
|
|
36
|
-
|
|
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.
|
|
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. **
|
|
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
|
-
|
|
30
|
-
`outline_read` on the one or two most promising ids
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
dates
|
|
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
|
|
53
|
-
output - never invent one
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|