@patimweb/pi-outline-wiki 1.0.0
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/.node-version +1 -0
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/index.ts +128 -0
- package/package.json +51 -0
- package/release.config.cjs +16 -0
- package/screenshot.png +0 -0
- package/skills/outline-doc-writing/SKILL.md +78 -0
- package/skills/outline-knowledge-capture/SKILL.md +92 -0
- package/skills/outline-wiki/SKILL.md +70 -0
- package/src/clients/outline-client.ts +632 -0
- package/src/config.ts +218 -0
- package/src/formatting/formatters.ts +258 -0
- package/src/tools/outline-archive.ts +70 -0
- package/src/tools/outline-collections.ts +52 -0
- package/src/tools/outline-comment.ts +65 -0
- package/src/tools/outline-comments.ts +52 -0
- package/src/tools/outline-create.ts +99 -0
- package/src/tools/outline-delete.ts +60 -0
- package/src/tools/outline-export.ts +146 -0
- package/src/tools/outline-list.ts +110 -0
- package/src/tools/outline-move.ts +99 -0
- package/src/tools/outline-profile.ts +82 -0
- package/src/tools/outline-read.ts +104 -0
- package/src/tools/outline-search.ts +114 -0
- package/src/tools/outline-setup.ts +71 -0
- package/src/tools/outline-status.ts +64 -0
- package/src/tools/outline-structure.ts +63 -0
- package/src/tools/outline-update.ts +106 -0
- package/src/types.ts +169 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outline_read tool -- Read one document's markdown body.
|
|
3
|
+
*
|
|
4
|
+
* Long wiki pages can blow a context window, so the body is truncated by
|
|
5
|
+
* default and the caller decides how much to pull in.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import { resolveConfig } from "../config.ts";
|
|
10
|
+
import {
|
|
11
|
+
getDocument,
|
|
12
|
+
listDocuments,
|
|
13
|
+
slugifyTitle,
|
|
14
|
+
writeMarkdownFile,
|
|
15
|
+
} from "../clients/outline-client.ts";
|
|
16
|
+
import { formatDocument, formatDocumentList } from "../formatting/formatters.ts";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_MAX_CHARS = 20_000;
|
|
19
|
+
|
|
20
|
+
export const OutlineReadTool = {
|
|
21
|
+
name: "outline_read",
|
|
22
|
+
label: "Outline Read Document",
|
|
23
|
+
description:
|
|
24
|
+
"Read a document's full markdown body by id (the id or urlId from outline_search, outline_list or outline_structure). Optionally list its child documents or save it to a local markdown file.",
|
|
25
|
+
parameters: Type.Object({
|
|
26
|
+
id: Type.String({
|
|
27
|
+
description: "Document id (uuid) or urlId from an Outline URL.",
|
|
28
|
+
}),
|
|
29
|
+
maxChars: Type.Optional(
|
|
30
|
+
Type.Number({
|
|
31
|
+
description:
|
|
32
|
+
"Truncate the body after this many characters. Default 20000. Use 0 for no limit.",
|
|
33
|
+
default: DEFAULT_MAX_CHARS,
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
includeChildren: Type.Optional(
|
|
37
|
+
Type.Boolean({
|
|
38
|
+
description: "Also list the direct child documents. Default false.",
|
|
39
|
+
default: false,
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
saveDir: Type.Optional(
|
|
43
|
+
Type.String({
|
|
44
|
+
description:
|
|
45
|
+
"Directory to save the document as a .md file in. Absolute paths are safest.",
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
profile: Type.Optional(
|
|
49
|
+
Type.String({ description: "Outline profile to use. Defaults to the active one." }),
|
|
50
|
+
),
|
|
51
|
+
}),
|
|
52
|
+
|
|
53
|
+
async execute(
|
|
54
|
+
_toolCallId: string,
|
|
55
|
+
params: {
|
|
56
|
+
id: string;
|
|
57
|
+
maxChars?: number;
|
|
58
|
+
includeChildren?: boolean;
|
|
59
|
+
saveDir?: string;
|
|
60
|
+
profile?: string;
|
|
61
|
+
},
|
|
62
|
+
signal: AbortSignal,
|
|
63
|
+
) {
|
|
64
|
+
const config = resolveConfig(params.profile);
|
|
65
|
+
const doc = await getDocument(config, params.id, signal);
|
|
66
|
+
const maxChars = params.maxChars ?? DEFAULT_MAX_CHARS;
|
|
67
|
+
|
|
68
|
+
const sections = [formatDocument(doc, { maxChars })];
|
|
69
|
+
|
|
70
|
+
let savedPath: string | undefined;
|
|
71
|
+
if (params.saveDir) {
|
|
72
|
+
savedPath = writeMarkdownFile(
|
|
73
|
+
params.saveDir,
|
|
74
|
+
`${slugifyTitle(doc.title)}.md`,
|
|
75
|
+
`# ${doc.title}\n\n${doc.text}\n`,
|
|
76
|
+
);
|
|
77
|
+
sections.push(`\nSaved full document to ${savedPath}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let childCount = 0;
|
|
81
|
+
if (params.includeChildren) {
|
|
82
|
+
const { documents } = await listDocuments(
|
|
83
|
+
config,
|
|
84
|
+
{ parentDocumentId: doc.id, limit: 50 },
|
|
85
|
+
signal,
|
|
86
|
+
);
|
|
87
|
+
childCount = documents.length;
|
|
88
|
+
sections.push(`\n${formatDocumentList(documents, "Child documents")}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: "text" as const, text: sections.join("\n") }],
|
|
93
|
+
details: {
|
|
94
|
+
id: doc.id,
|
|
95
|
+
title: doc.title,
|
|
96
|
+
collectionId: doc.collectionId,
|
|
97
|
+
length: doc.text.length,
|
|
98
|
+
truncated: maxChars > 0 && doc.text.length > maxChars,
|
|
99
|
+
childCount,
|
|
100
|
+
savedPath,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outline_search tool -- Full-text search across the workspace.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { resolveConfig } from "../config.ts";
|
|
7
|
+
import { searchDocuments } from "../clients/outline-client.ts";
|
|
8
|
+
import { formatSearchResults } from "../formatting/formatters.ts";
|
|
9
|
+
|
|
10
|
+
const VALID_STATUS = new Set(["draft", "archived", "published"]);
|
|
11
|
+
const VALID_DATE = new Set(["day", "week", "month", "year"]);
|
|
12
|
+
|
|
13
|
+
export const OutlineSearchTool = {
|
|
14
|
+
name: "outline_search",
|
|
15
|
+
label: "Outline Search",
|
|
16
|
+
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.",
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
query: Type.String({
|
|
20
|
+
description: "Search keywords. Distinctive nouns work better than full questions.",
|
|
21
|
+
}),
|
|
22
|
+
limit: Type.Optional(
|
|
23
|
+
Type.Number({ description: "How many results to return. Default 15.", default: 15 }),
|
|
24
|
+
),
|
|
25
|
+
offset: Type.Optional(
|
|
26
|
+
Type.Number({ description: "Pagination offset. Default 0.", default: 0 }),
|
|
27
|
+
),
|
|
28
|
+
collectionId: Type.Optional(
|
|
29
|
+
Type.String({ description: "Restrict the search to one collection." }),
|
|
30
|
+
),
|
|
31
|
+
documentId: Type.Optional(
|
|
32
|
+
Type.String({ description: "Restrict the search to one document and its children." }),
|
|
33
|
+
),
|
|
34
|
+
statusFilter: Type.Optional(
|
|
35
|
+
Type.Array(Type.String(), {
|
|
36
|
+
description:
|
|
37
|
+
"Restrict to document states: any of draft, archived, published. Defaults to published documents.",
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
dateFilter: Type.Optional(
|
|
41
|
+
Type.String({
|
|
42
|
+
description: "Only documents updated within: day, week, month or year.",
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
profile: Type.Optional(
|
|
46
|
+
Type.String({ description: "Outline profile to use. Defaults to the active one." }),
|
|
47
|
+
),
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
async execute(
|
|
51
|
+
_toolCallId: string,
|
|
52
|
+
params: {
|
|
53
|
+
query: string;
|
|
54
|
+
limit?: number;
|
|
55
|
+
offset?: number;
|
|
56
|
+
collectionId?: string;
|
|
57
|
+
documentId?: string;
|
|
58
|
+
statusFilter?: string[];
|
|
59
|
+
dateFilter?: string;
|
|
60
|
+
profile?: string;
|
|
61
|
+
},
|
|
62
|
+
signal: AbortSignal,
|
|
63
|
+
) {
|
|
64
|
+
if (!params.query || !params.query.trim()) {
|
|
65
|
+
throw new Error("query must not be empty.");
|
|
66
|
+
}
|
|
67
|
+
if (params.statusFilter) {
|
|
68
|
+
const invalid = params.statusFilter.filter((s) => !VALID_STATUS.has(s));
|
|
69
|
+
if (invalid.length > 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Invalid statusFilter value(s): ${invalid.join(", ")}. Use draft, archived or published.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (params.dateFilter && !VALID_DATE.has(params.dateFilter)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Invalid dateFilter "${params.dateFilter}". Use day, week, month or year.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
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
|
+
);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text" as const,
|
|
100
|
+
text: formatSearchResults(hits, params.query.trim(), pagination),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
details: {
|
|
104
|
+
query: params.query.trim(),
|
|
105
|
+
count: hits.length,
|
|
106
|
+
documents: hits.map((hit) => ({
|
|
107
|
+
id: hit.document.id,
|
|
108
|
+
title: hit.document.title,
|
|
109
|
+
})),
|
|
110
|
+
pagination,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outline_setup tool -- Configure an Outline workspace.
|
|
3
|
+
*
|
|
4
|
+
* Stores a named profile. A profile is automatically set as active
|
|
5
|
+
* if it's the first one. Use outline_status to see all profiles and
|
|
6
|
+
* outline_profile to switch between them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Type } from "typebox";
|
|
10
|
+
import { saveProfile, normalizeBaseUrl, resolveConfig } from "../config.ts";
|
|
11
|
+
import { getWorkspaceInfo } from "../clients/outline-client.ts";
|
|
12
|
+
import { formatWorkspaceInfo } from "../formatting/formatters.ts";
|
|
13
|
+
import type { OutlineConfig, SetupParams } from "../types.ts";
|
|
14
|
+
|
|
15
|
+
export const OutlineSetupTool = {
|
|
16
|
+
name: "outline_setup",
|
|
17
|
+
label: "Outline Setup",
|
|
18
|
+
description:
|
|
19
|
+
"Configure an Outline workspace (URL + API key). Call this first before using any other outline tool. The API key is created in Outline under Settings -> API & Apps and starts with 'ol_api_'. Credentials are stored in ~/.pi/outline-config.json (readable only by you). Works with both cloud (yourteam.getoutline.com) and self-hosted installations.",
|
|
20
|
+
parameters: Type.Object({
|
|
21
|
+
name: Type.String({
|
|
22
|
+
description:
|
|
23
|
+
"Profile name, e.g. 'work', 'company-wiki'. Use a short, memorable name.",
|
|
24
|
+
}),
|
|
25
|
+
url: Type.String({
|
|
26
|
+
description:
|
|
27
|
+
"Workspace URL, e.g. https://acme.getoutline.com or https://wiki.example.com. A trailing /api or /mcp is stripped automatically.",
|
|
28
|
+
}),
|
|
29
|
+
apiKey: Type.String({
|
|
30
|
+
description: "Outline API key from Settings -> API & Apps (starts with ol_api_).",
|
|
31
|
+
}),
|
|
32
|
+
insecureTls: Type.Optional(
|
|
33
|
+
Type.Boolean({
|
|
34
|
+
description:
|
|
35
|
+
"Skip TLS certificate validation. Only for self-hosted instances with a self-signed certificate; this disables validation for the whole pi process.",
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
async execute(_toolCallId: string, params: SetupParams, signal: AbortSignal) {
|
|
41
|
+
const config: OutlineConfig = {
|
|
42
|
+
url: normalizeBaseUrl(params.url),
|
|
43
|
+
apiKey: params.apiKey.trim(),
|
|
44
|
+
...(params.insecureTls !== undefined && { insecureTls: params.insecureTls }),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
saveProfile(params.name, config);
|
|
48
|
+
|
|
49
|
+
// Verify immediately: a typo in the URL or key is much cheaper to find
|
|
50
|
+
// here than inside the first real document call.
|
|
51
|
+
let verification: string;
|
|
52
|
+
let verified = false;
|
|
53
|
+
try {
|
|
54
|
+
const info = await getWorkspaceInfo(resolveConfig(params.name), signal);
|
|
55
|
+
verification = formatWorkspaceInfo(info);
|
|
56
|
+
verified = true;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
verification = `Saved, but the connection test failed: ${(err as Error).message}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: "text" as const,
|
|
65
|
+
text: `Outline profile "${params.name}" saved and set as active (${config.url}).\n\n${verification}`,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
details: { profileName: params.name, url: config.url, verified },
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outline_status tool -- Show configuration status and verify the connection.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { getProfiles, getActiveProfile, resolveConfig } from "../config.ts";
|
|
7
|
+
import { getWorkspaceInfo, isTlsValidationDisabled } from "../clients/outline-client.ts";
|
|
8
|
+
import { formatProfileStatus, formatWorkspaceInfo } from "../formatting/formatters.ts";
|
|
9
|
+
|
|
10
|
+
export const OutlineStatusTool = {
|
|
11
|
+
name: "outline_status",
|
|
12
|
+
label: "Outline Status",
|
|
13
|
+
description:
|
|
14
|
+
"Show which Outline workspaces are configured and verify that the active API key still works.",
|
|
15
|
+
parameters: Type.Object({
|
|
16
|
+
profile: Type.Optional(
|
|
17
|
+
Type.String({ description: "Profile to verify. Defaults to the active one." }),
|
|
18
|
+
),
|
|
19
|
+
}),
|
|
20
|
+
|
|
21
|
+
async execute(
|
|
22
|
+
_toolCallId: string,
|
|
23
|
+
params: { profile?: string },
|
|
24
|
+
signal: AbortSignal,
|
|
25
|
+
) {
|
|
26
|
+
const allProfiles = getProfiles();
|
|
27
|
+
const active = getActiveProfile();
|
|
28
|
+
const overview = formatProfileStatus(allProfiles, active);
|
|
29
|
+
|
|
30
|
+
if (Object.keys(allProfiles).length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text" as const, text: overview }],
|
|
33
|
+
details: { configured: false, profileCount: 0, activeProfile: null },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let connection: string;
|
|
38
|
+
let reachable = false;
|
|
39
|
+
try {
|
|
40
|
+
const info = await getWorkspaceInfo(resolveConfig(params.profile), signal);
|
|
41
|
+
connection = formatWorkspaceInfo(info);
|
|
42
|
+
reachable = true;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
connection = `Connection test failed: ${(err as Error).message}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const warning = isTlsValidationDisabled()
|
|
48
|
+
? "\n\nWarning: TLS certificate validation is disabled in this pi process."
|
|
49
|
+
: "";
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{ type: "text" as const, text: `${overview}\n\n${connection}${warning}` },
|
|
54
|
+
],
|
|
55
|
+
details: {
|
|
56
|
+
configured: true,
|
|
57
|
+
profileCount: Object.keys(allProfiles).length,
|
|
58
|
+
activeProfile: active,
|
|
59
|
+
profiles: Object.keys(allProfiles),
|
|
60
|
+
reachable,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outline_structure tool -- Show the nested document tree of a collection.
|
|
3
|
+
*
|
|
4
|
+
* Cheaper and more useful than listing documents flat: it reveals how the
|
|
5
|
+
* wiki is organised, which is what you need before filing a new document.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import { resolveConfig } from "../config.ts";
|
|
10
|
+
import { getCollectionStructure, listCollections } from "../clients/outline-client.ts";
|
|
11
|
+
import { formatCollectionStructure } from "../formatting/formatters.ts";
|
|
12
|
+
|
|
13
|
+
export const OutlineStructureTool = {
|
|
14
|
+
name: "outline_structure",
|
|
15
|
+
label: "Outline Collection Structure",
|
|
16
|
+
description:
|
|
17
|
+
"Show the nested document tree of one collection (titles and ids, parents and children). Use it to understand how a wiki section is organised before creating or moving documents.",
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
collectionId: Type.String({
|
|
20
|
+
description: "Collection id. Get it from outline_collections.",
|
|
21
|
+
}),
|
|
22
|
+
profile: Type.Optional(
|
|
23
|
+
Type.String({ description: "Outline profile to use. Defaults to the active one." }),
|
|
24
|
+
),
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
async execute(
|
|
28
|
+
_toolCallId: string,
|
|
29
|
+
params: { collectionId: string; profile?: string },
|
|
30
|
+
signal: AbortSignal,
|
|
31
|
+
) {
|
|
32
|
+
const config = resolveConfig(params.profile);
|
|
33
|
+
const nodes = await getCollectionStructure(config, params.collectionId, signal);
|
|
34
|
+
|
|
35
|
+
// The tree endpoint returns no collection name, so resolve it for output.
|
|
36
|
+
let collectionName: string | undefined;
|
|
37
|
+
try {
|
|
38
|
+
const { collections } = await listCollections(config, { limit: 100 }, signal);
|
|
39
|
+
collectionName = collections.find((c) => c.id === params.collectionId)?.name;
|
|
40
|
+
} catch {
|
|
41
|
+
/* naming is cosmetic -- never fail the call over it */
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function countNodes(items: ReadonlyArray<{ children: ReadonlyArray<any> }>): number {
|
|
45
|
+
return items.reduce((total, node) => total + 1 + countNodes(node.children), 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text" as const,
|
|
52
|
+
text: formatCollectionStructure(nodes, collectionName),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
details: {
|
|
56
|
+
collectionId: params.collectionId,
|
|
57
|
+
collectionName,
|
|
58
|
+
topLevelCount: nodes.length,
|
|
59
|
+
totalCount: countNodes(nodes),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outline_update tool -- Change an existing document.
|
|
3
|
+
*
|
|
4
|
+
* `replace` overwrites the body, so append/prepend exist to add to a page
|
|
5
|
+
* (changelog, decision log, FAQ) without having to read and re-send it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import { resolveConfig } from "../config.ts";
|
|
10
|
+
import { getDocument, updateDocument } from "../clients/outline-client.ts";
|
|
11
|
+
import { formatDocumentSaved } from "../formatting/formatters.ts";
|
|
12
|
+
|
|
13
|
+
const VALID_MODES = new Set(["replace", "append", "prepend"]);
|
|
14
|
+
|
|
15
|
+
export const OutlineUpdateTool = {
|
|
16
|
+
name: "outline_update",
|
|
17
|
+
label: "Outline Update Document",
|
|
18
|
+
description:
|
|
19
|
+
"Update an existing document: replace the body, append or prepend a section, rename it, or publish a draft. Read the document first when replacing, so nothing is lost.",
|
|
20
|
+
parameters: Type.Object({
|
|
21
|
+
id: Type.String({ description: "Document id or urlId." }),
|
|
22
|
+
text: Type.Optional(Type.String({ description: "Markdown to write, append or prepend." })),
|
|
23
|
+
mode: Type.Optional(
|
|
24
|
+
Type.String({
|
|
25
|
+
description:
|
|
26
|
+
"How to apply text: replace (default, overwrites the whole body), append (add at the end), prepend (add at the top).",
|
|
27
|
+
default: "replace",
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
title: Type.Optional(Type.String({ description: "New title." })),
|
|
31
|
+
icon: Type.Optional(Type.String({ description: "New emoji or icon." })),
|
|
32
|
+
publish: Type.Optional(
|
|
33
|
+
Type.Boolean({ description: "Publish a draft document so others can see it." }),
|
|
34
|
+
),
|
|
35
|
+
profile: Type.Optional(
|
|
36
|
+
Type.String({ description: "Outline profile to use. Defaults to the active one." }),
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
async execute(
|
|
41
|
+
_toolCallId: string,
|
|
42
|
+
params: {
|
|
43
|
+
id: string;
|
|
44
|
+
text?: string;
|
|
45
|
+
mode?: string;
|
|
46
|
+
title?: string;
|
|
47
|
+
icon?: string;
|
|
48
|
+
publish?: boolean;
|
|
49
|
+
profile?: string;
|
|
50
|
+
},
|
|
51
|
+
signal: AbortSignal,
|
|
52
|
+
) {
|
|
53
|
+
const mode = (params.mode ?? "replace").toLowerCase();
|
|
54
|
+
if (!VALID_MODES.has(mode)) {
|
|
55
|
+
throw new Error(`Invalid mode "${params.mode}". Use replace, append or prepend.`);
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
params.text === undefined &&
|
|
59
|
+
params.title === undefined &&
|
|
60
|
+
params.icon === undefined &&
|
|
61
|
+
params.publish === undefined
|
|
62
|
+
) {
|
|
63
|
+
throw new Error("Nothing to update: pass at least one of text, title, icon or publish.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const config = resolveConfig(params.profile);
|
|
67
|
+
|
|
68
|
+
let text = params.text;
|
|
69
|
+
if (text !== undefined && mode !== "replace") {
|
|
70
|
+
const current = await getDocument(config, params.id, signal);
|
|
71
|
+
const existing = current.text ?? "";
|
|
72
|
+
text =
|
|
73
|
+
mode === "append"
|
|
74
|
+
? `${existing.replace(/\s*$/, "")}\n\n${text}\n`
|
|
75
|
+
: `${text}\n\n${existing.replace(/^\s*/, "")}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const doc = await updateDocument(
|
|
79
|
+
config,
|
|
80
|
+
{
|
|
81
|
+
id: params.id,
|
|
82
|
+
text,
|
|
83
|
+
title: params.title,
|
|
84
|
+
icon: params.icon,
|
|
85
|
+
publish: params.publish,
|
|
86
|
+
},
|
|
87
|
+
signal,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: "text" as const,
|
|
94
|
+
text: `${formatDocumentSaved(doc, "updated")}\nMode: ${mode}`,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
details: {
|
|
98
|
+
id: doc.id,
|
|
99
|
+
title: doc.title,
|
|
100
|
+
url: doc.url,
|
|
101
|
+
mode,
|
|
102
|
+
length: doc.text.length,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data types for the pi Outline Wiki extension.
|
|
3
|
+
*
|
|
4
|
+
* All domain data is represented as plain immutable-shaped interfaces.
|
|
5
|
+
* No behavior, no classes, no inheritance -- just data (errors excepted).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Configuration
|
|
9
|
+
|
|
10
|
+
export interface OutlineConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Workspace base URL without a trailing slash and without the `/api`
|
|
13
|
+
* suffix, e.g. `https://acme.getoutline.com` or `https://wiki.example.com`.
|
|
14
|
+
*/
|
|
15
|
+
readonly url: string;
|
|
16
|
+
/** Personal API key, starts with `ol_api_`. */
|
|
17
|
+
readonly apiKey: string;
|
|
18
|
+
/**
|
|
19
|
+
* Skip TLS certificate validation. Only for self-hosted instances behind a
|
|
20
|
+
* self-signed certificate. Disables validation process-wide, so it is off
|
|
21
|
+
* unless explicitly requested.
|
|
22
|
+
*/
|
|
23
|
+
readonly insecureTls?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface OutlineProfiles {
|
|
27
|
+
readonly profiles: Record<string, OutlineConfig>;
|
|
28
|
+
readonly activeProfile: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Domain
|
|
32
|
+
|
|
33
|
+
export interface UserRef {
|
|
34
|
+
readonly id?: string;
|
|
35
|
+
readonly name?: string;
|
|
36
|
+
readonly email?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CollectionSummary {
|
|
40
|
+
readonly id: string;
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly description?: string | null;
|
|
43
|
+
readonly icon?: string | null;
|
|
44
|
+
readonly color?: string | null;
|
|
45
|
+
readonly permission?: string | null;
|
|
46
|
+
readonly updatedAt?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DocumentSummary {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly urlId?: string;
|
|
52
|
+
readonly title: string;
|
|
53
|
+
readonly url?: string;
|
|
54
|
+
readonly icon?: string | null;
|
|
55
|
+
readonly collectionId?: string | null;
|
|
56
|
+
readonly parentDocumentId?: string | null;
|
|
57
|
+
readonly template?: boolean;
|
|
58
|
+
readonly fullWidth?: boolean;
|
|
59
|
+
readonly createdAt?: string;
|
|
60
|
+
readonly updatedAt?: string;
|
|
61
|
+
readonly archivedAt?: string | null;
|
|
62
|
+
readonly deletedAt?: string | null;
|
|
63
|
+
readonly publishedAt?: string | null;
|
|
64
|
+
readonly createdBy?: UserRef;
|
|
65
|
+
readonly updatedBy?: UserRef;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DocumentDetail extends DocumentSummary {
|
|
69
|
+
/** Markdown body. */
|
|
70
|
+
readonly text: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SearchHit {
|
|
74
|
+
readonly ranking?: number;
|
|
75
|
+
/** Snippet around the match, may contain markdown highlight markers. */
|
|
76
|
+
readonly context: string;
|
|
77
|
+
readonly document: DocumentSummary;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface NavigationNode {
|
|
81
|
+
readonly id: string;
|
|
82
|
+
readonly title: string;
|
|
83
|
+
readonly url?: string;
|
|
84
|
+
readonly children: ReadonlyArray<NavigationNode>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CommentEntry {
|
|
88
|
+
readonly id: string;
|
|
89
|
+
readonly documentId?: string;
|
|
90
|
+
readonly parentCommentId?: string | null;
|
|
91
|
+
/** Plain text flattened from the stored rich-text document. */
|
|
92
|
+
readonly text: string;
|
|
93
|
+
readonly createdAt?: string;
|
|
94
|
+
readonly updatedAt?: string;
|
|
95
|
+
readonly resolvedAt?: string | null;
|
|
96
|
+
readonly createdBy?: UserRef;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface WorkspaceInfo {
|
|
100
|
+
readonly userName?: string;
|
|
101
|
+
readonly userEmail?: string;
|
|
102
|
+
readonly teamName?: string;
|
|
103
|
+
readonly teamUrl?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface Pagination {
|
|
107
|
+
readonly offset?: number;
|
|
108
|
+
readonly limit?: number;
|
|
109
|
+
readonly total?: number;
|
|
110
|
+
readonly nextPath?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Envelope every Outline API method returns. */
|
|
114
|
+
export interface OutlineResponse<T> {
|
|
115
|
+
readonly ok: boolean;
|
|
116
|
+
readonly data: T;
|
|
117
|
+
readonly pagination?: Pagination;
|
|
118
|
+
readonly status?: number;
|
|
119
|
+
readonly error?: string;
|
|
120
|
+
readonly message?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Tool parameter shapes
|
|
124
|
+
|
|
125
|
+
export interface SetupParams {
|
|
126
|
+
readonly name: string;
|
|
127
|
+
readonly url: string;
|
|
128
|
+
readonly apiKey: string;
|
|
129
|
+
readonly insecureTls?: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Errors
|
|
133
|
+
|
|
134
|
+
export class OutlineNotConfiguredError extends Error {
|
|
135
|
+
constructor() {
|
|
136
|
+
super(
|
|
137
|
+
"Outline is not configured. Use the outline_setup tool first (workspace URL + API key).",
|
|
138
|
+
);
|
|
139
|
+
this.name = "OutlineNotConfiguredError";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class OutlineApiError extends Error {
|
|
144
|
+
readonly method: string;
|
|
145
|
+
readonly status: number;
|
|
146
|
+
readonly code?: string;
|
|
147
|
+
|
|
148
|
+
constructor(method: string, status: number, message: string, code?: string) {
|
|
149
|
+
super(message);
|
|
150
|
+
this.name = "OutlineApiError";
|
|
151
|
+
this.method = method;
|
|
152
|
+
this.status = status;
|
|
153
|
+
this.code = code;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class OutlineNotFoundError extends Error {
|
|
158
|
+
constructor(what: string) {
|
|
159
|
+
super(`${what} not found. Check the id, or search for it with outline_search.`);
|
|
160
|
+
this.name = "OutlineNotFoundError";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class UnsafeExportPathError extends Error {
|
|
165
|
+
constructor(target: string) {
|
|
166
|
+
super(`Refusing to write outside the target directory: ${target}`);
|
|
167
|
+
this.name = "UnsafeExportPathError";
|
|
168
|
+
}
|
|
169
|
+
}
|