@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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * outline_comments tool -- Read the discussion on a document.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import { resolveConfig } from "../config.ts";
7
+ import { listComments } from "../clients/outline-client.ts";
8
+ import { formatComments } from "../formatting/formatters.ts";
9
+
10
+ export const OutlineCommentsTool = {
11
+ name: "outline_comments",
12
+ label: "Outline Read Comments",
13
+ description:
14
+ "Read the comment threads on a document, nested by reply. Useful for open questions and review feedback that never made it into the page body.",
15
+ parameters: Type.Object({
16
+ documentId: Type.String({ description: "Document id." }),
17
+ limit: Type.Optional(
18
+ Type.Number({ description: "How many comments to return. Default 50.", default: 50 }),
19
+ ),
20
+ offset: Type.Optional(
21
+ Type.Number({ description: "Pagination offset. Default 0.", default: 0 }),
22
+ ),
23
+ profile: Type.Optional(
24
+ Type.String({ description: "Outline profile to use. Defaults to the active one." }),
25
+ ),
26
+ }),
27
+
28
+ async execute(
29
+ _toolCallId: string,
30
+ params: { documentId: string; limit?: number; offset?: number; profile?: string },
31
+ signal: AbortSignal,
32
+ ) {
33
+ const config = resolveConfig(params.profile);
34
+ const { comments, pagination } = await listComments(
35
+ config,
36
+ { documentId: params.documentId, limit: params.limit, offset: params.offset },
37
+ signal,
38
+ );
39
+
40
+ return {
41
+ content: [
42
+ { type: "text" as const, text: formatComments(comments, params.documentId) },
43
+ ],
44
+ details: {
45
+ documentId: params.documentId,
46
+ count: comments.length,
47
+ unresolved: comments.filter((c) => !c.resolvedAt).length,
48
+ pagination,
49
+ },
50
+ };
51
+ },
52
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * outline_create tool -- Create a new wiki document.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import { resolveConfig } from "../config.ts";
7
+ import { createDocument } from "../clients/outline-client.ts";
8
+ import { formatDocumentSaved } from "../formatting/formatters.ts";
9
+
10
+ export const OutlineCreateTool = {
11
+ name: "outline_create",
12
+ label: "Outline Create Document",
13
+ description:
14
+ "Create a new document in the Outline wiki from markdown. Give either a collectionId (top level of that collection) or a parentDocumentId (nested under an existing page). Search first -- updating an existing page is usually better than adding a near-duplicate.",
15
+ parameters: Type.Object({
16
+ title: Type.String({ description: "Document title. Keep it specific and searchable." }),
17
+ text: Type.Optional(
18
+ Type.String({
19
+ description:
20
+ "Markdown body. Do not repeat the title as a top-level heading; Outline renders the title separately.",
21
+ }),
22
+ ),
23
+ collectionId: Type.Optional(
24
+ Type.String({
25
+ description: "Collection to create the document in. Required unless parentDocumentId is given.",
26
+ }),
27
+ ),
28
+ parentDocumentId: Type.Optional(
29
+ Type.String({ description: "Create the document as a child of this document." }),
30
+ ),
31
+ templateId: Type.Optional(
32
+ Type.String({ description: "Create the document from this template." }),
33
+ ),
34
+ icon: Type.Optional(
35
+ Type.String({ description: "Emoji or icon name shown next to the title." }),
36
+ ),
37
+ publish: Type.Optional(
38
+ Type.Boolean({
39
+ description:
40
+ "Publish immediately so others can see it. Default true. Set false to keep it as a personal draft.",
41
+ default: true,
42
+ }),
43
+ ),
44
+ profile: Type.Optional(
45
+ Type.String({ description: "Outline profile to use. Defaults to the active one." }),
46
+ ),
47
+ }),
48
+
49
+ async execute(
50
+ _toolCallId: string,
51
+ params: {
52
+ title: string;
53
+ text?: string;
54
+ collectionId?: string;
55
+ parentDocumentId?: string;
56
+ templateId?: string;
57
+ icon?: string;
58
+ publish?: boolean;
59
+ profile?: string;
60
+ },
61
+ signal: AbortSignal,
62
+ ) {
63
+ if (!params.title || !params.title.trim()) {
64
+ throw new Error("title must not be empty.");
65
+ }
66
+ const publish = params.publish ?? true;
67
+ if (publish && !params.collectionId && !params.parentDocumentId) {
68
+ throw new Error(
69
+ "A published document needs a collectionId or a parentDocumentId. List collections with outline_collections, or pass publish: false to create a draft.",
70
+ );
71
+ }
72
+
73
+ const config = resolveConfig(params.profile);
74
+ const doc = await createDocument(
75
+ config,
76
+ {
77
+ title: params.title.trim(),
78
+ text: params.text ?? "",
79
+ collectionId: params.collectionId,
80
+ parentDocumentId: params.parentDocumentId,
81
+ templateId: params.templateId,
82
+ icon: params.icon,
83
+ publish,
84
+ },
85
+ signal,
86
+ );
87
+
88
+ return {
89
+ content: [{ type: "text" as const, text: formatDocumentSaved(doc, "created") }],
90
+ details: {
91
+ id: doc.id,
92
+ title: doc.title,
93
+ url: doc.url,
94
+ collectionId: doc.collectionId,
95
+ published: Boolean(doc.publishedAt),
96
+ },
97
+ };
98
+ },
99
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * outline_delete tool -- Move a document to the trash, or erase it.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import { resolveConfig } from "../config.ts";
7
+ import { documentLifecycle, getDocument } from "../clients/outline-client.ts";
8
+
9
+ export const OutlineDeleteTool = {
10
+ name: "outline_delete",
11
+ label: "Outline Delete Document",
12
+ description:
13
+ "Move a document to the Outline trash, where it can be restored with outline_archive action 'restore'. With permanent: true it is erased and cannot be recovered -- only do that when explicitly asked. Prefer archiving over deleting.",
14
+ parameters: Type.Object({
15
+ id: Type.String({ description: "Document id." }),
16
+ permanent: Type.Optional(
17
+ Type.Boolean({
18
+ description:
19
+ "Erase the document instead of moving it to the trash. Irreversible. Default false.",
20
+ default: false,
21
+ }),
22
+ ),
23
+ profile: Type.Optional(
24
+ Type.String({ description: "Outline profile to use. Defaults to the active one." }),
25
+ ),
26
+ }),
27
+
28
+ async execute(
29
+ _toolCallId: string,
30
+ params: { id: string; permanent?: boolean; profile?: string },
31
+ signal: AbortSignal,
32
+ ) {
33
+ const config = resolveConfig(params.profile);
34
+
35
+ // Resolve the title before deletion so the confirmation names the page
36
+ // rather than a bare uuid.
37
+ let title = params.id;
38
+ try {
39
+ title = (await getDocument(config, params.id, signal)).title;
40
+ } catch {
41
+ /* the delete call below reports a real failure */
42
+ }
43
+
44
+ await documentLifecycle(
45
+ config,
46
+ "delete",
47
+ { id: params.id, permanent: params.permanent },
48
+ signal,
49
+ );
50
+
51
+ const text = params.permanent
52
+ ? `Permanently deleted "${title}" (${params.id}). This cannot be undone.`
53
+ : `Moved "${title}" (${params.id}) to the trash. Restore it with outline_archive action "restore".`;
54
+
55
+ return {
56
+ content: [{ type: "text" as const, text }],
57
+ details: { id: params.id, title, permanent: Boolean(params.permanent) },
58
+ };
59
+ },
60
+ };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * outline_export tool -- Save wiki documents to local markdown files.
3
+ *
4
+ * The point is to get wiki knowledge into the repository the agent is working
5
+ * in (docs/, ADRs, onboarding notes) without copying text through the model.
6
+ */
7
+
8
+ import { Type } from "typebox";
9
+ import { resolveConfig } from "../config.ts";
10
+ import {
11
+ exportDocument,
12
+ getCollectionStructure,
13
+ getDocument,
14
+ slugifyTitle,
15
+ writeMarkdownFile,
16
+ } from "../clients/outline-client.ts";
17
+ import type { NavigationNode } from "../types.ts";
18
+
19
+ function flatten(nodes: ReadonlyArray<NavigationNode>, out: NavigationNode[] = []): NavigationNode[] {
20
+ for (const node of nodes) {
21
+ out.push(node);
22
+ flatten(node.children, out);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ export const OutlineExportTool = {
28
+ name: "outline_export",
29
+ label: "Outline Export to Files",
30
+ description:
31
+ "Export one document (optionally with its children) or a whole collection to local markdown files. Use it to pull wiki content into a repository instead of reading every page through the model.",
32
+ parameters: Type.Object({
33
+ targetDir: Type.String({
34
+ description: "Directory to write the .md files into. Absolute paths are safest.",
35
+ }),
36
+ documentId: Type.Optional(
37
+ Type.String({ description: "Document to export. Mutually exclusive with collectionId." }),
38
+ ),
39
+ collectionId: Type.Optional(
40
+ Type.String({ description: "Export every document of this collection." }),
41
+ ),
42
+ includeChildren: Type.Optional(
43
+ Type.Boolean({
44
+ description: "With documentId: also export nested child documents. Default false.",
45
+ default: false,
46
+ }),
47
+ ),
48
+ limit: Type.Optional(
49
+ Type.Number({
50
+ description: "Maximum number of documents to write. Default 50.",
51
+ default: 50,
52
+ }),
53
+ ),
54
+ profile: Type.Optional(
55
+ Type.String({ description: "Outline profile to use. Defaults to the active one." }),
56
+ ),
57
+ }),
58
+
59
+ async execute(
60
+ _toolCallId: string,
61
+ params: {
62
+ targetDir: string;
63
+ documentId?: string;
64
+ collectionId?: string;
65
+ includeChildren?: boolean;
66
+ limit?: number;
67
+ profile?: string;
68
+ },
69
+ signal: AbortSignal,
70
+ ) {
71
+ if (!params.documentId && !params.collectionId) {
72
+ throw new Error("Pass either documentId or collectionId.");
73
+ }
74
+ if (params.documentId && params.collectionId) {
75
+ throw new Error("Pass documentId or collectionId, not both.");
76
+ }
77
+
78
+ const config = resolveConfig(params.profile);
79
+ const limit = params.limit ?? 50;
80
+ const targets: Array<{ id: string; title: string }> = [];
81
+
82
+ if (params.collectionId) {
83
+ const tree = await getCollectionStructure(config, params.collectionId, signal);
84
+ for (const node of flatten(tree)) {
85
+ targets.push({ id: node.id, title: node.title });
86
+ }
87
+ } else {
88
+ const doc = await getDocument(config, params.documentId!, signal);
89
+ targets.push({ id: doc.id, title: doc.title });
90
+ if (params.includeChildren && doc.collectionId) {
91
+ const tree = await getCollectionStructure(config, doc.collectionId, signal);
92
+ const self = flatten(tree).find((node) => node.id === doc.id);
93
+ if (self) {
94
+ for (const child of flatten(self.children)) {
95
+ targets.push({ id: child.id, title: child.title });
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ const capped = targets.slice(0, limit);
102
+ const written: string[] = [];
103
+ const failed: Array<{ id: string; error: string }> = [];
104
+ const usedNames = new Set<string>();
105
+
106
+ for (const target of capped) {
107
+ try {
108
+ const markdown = await exportDocument(config, target.id, signal);
109
+ let stem = slugifyTitle(target.title);
110
+ // Wikis are full of pages called "Overview"; keep every file.
111
+ if (usedNames.has(stem)) stem = `${stem}-${target.id.slice(0, 8)}`;
112
+ usedNames.add(stem);
113
+ written.push(writeMarkdownFile(params.targetDir, `${stem}.md`, markdown));
114
+ } catch (err) {
115
+ failed.push({ id: target.id, error: (err as Error).message });
116
+ }
117
+ }
118
+
119
+ const lines = [
120
+ `Exported ${written.length} document(s) to ${params.targetDir}:`,
121
+ ...written.map((file) => `- ${file}`),
122
+ ];
123
+ if (targets.length > capped.length) {
124
+ lines.push(
125
+ `\n${targets.length - capped.length} more document(s) not exported (limit ${limit}).`,
126
+ );
127
+ }
128
+ if (failed.length > 0) {
129
+ lines.push(
130
+ `\n${failed.length} document(s) failed:`,
131
+ ...failed.map((f) => `- ${f.id}: ${f.error}`),
132
+ );
133
+ }
134
+
135
+ return {
136
+ content: [{ type: "text" as const, text: lines.join("\n") }],
137
+ details: {
138
+ targetDir: params.targetDir,
139
+ written,
140
+ writtenCount: written.length,
141
+ failedCount: failed.length,
142
+ skipped: targets.length - capped.length,
143
+ },
144
+ };
145
+ },
146
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * outline_list tool -- Browse documents without searching.
3
+ *
4
+ * Useful for "what changed recently", "what is in this collection" and
5
+ * "what are my unpublished drafts".
6
+ */
7
+
8
+ import { Type } from "typebox";
9
+ import { resolveConfig } from "../config.ts";
10
+ import { listDocuments } from "../clients/outline-client.ts";
11
+ import { formatDocumentList } from "../formatting/formatters.ts";
12
+
13
+ const VALID_SORT = new Set(["updatedAt", "createdAt", "title", "index"]);
14
+
15
+ export const OutlineListTool = {
16
+ name: "outline_list",
17
+ label: "Outline List Documents",
18
+ description:
19
+ "List documents without a search query -- most recently updated first by default. Filter by collection, by parent document, or list your own drafts. Use outline_search when you know what you are looking for.",
20
+ parameters: Type.Object({
21
+ collectionId: Type.Optional(
22
+ Type.String({ description: "Only documents in this collection." }),
23
+ ),
24
+ parentDocumentId: Type.Optional(
25
+ Type.String({ description: "Only direct children of this document." }),
26
+ ),
27
+ drafts: Type.Optional(
28
+ Type.Boolean({
29
+ description: "List unpublished drafts of the authenticated user instead.",
30
+ default: false,
31
+ }),
32
+ ),
33
+ limit: Type.Optional(
34
+ Type.Number({ description: "How many documents to return. Default 25.", default: 25 }),
35
+ ),
36
+ offset: Type.Optional(
37
+ Type.Number({ description: "Pagination offset. Default 0.", default: 0 }),
38
+ ),
39
+ sort: Type.Optional(
40
+ Type.String({
41
+ description: "Sort field: updatedAt (default), createdAt, title or index.",
42
+ }),
43
+ ),
44
+ direction: Type.Optional(
45
+ Type.String({ description: "ASC or DESC. Default DESC.", default: "DESC" }),
46
+ ),
47
+ profile: Type.Optional(
48
+ Type.String({ description: "Outline profile to use. Defaults to the active one." }),
49
+ ),
50
+ }),
51
+
52
+ async execute(
53
+ _toolCallId: string,
54
+ params: {
55
+ collectionId?: string;
56
+ parentDocumentId?: string;
57
+ drafts?: boolean;
58
+ limit?: number;
59
+ offset?: number;
60
+ sort?: string;
61
+ direction?: string;
62
+ profile?: string;
63
+ },
64
+ signal: AbortSignal,
65
+ ) {
66
+ if (params.sort && !VALID_SORT.has(params.sort)) {
67
+ throw new Error(
68
+ `Invalid sort "${params.sort}". Use updatedAt, createdAt, title or index.`,
69
+ );
70
+ }
71
+ const direction = (params.direction ?? "DESC").toUpperCase();
72
+ if (direction !== "ASC" && direction !== "DESC") {
73
+ throw new Error(`Invalid direction "${params.direction}". Use ASC or DESC.`);
74
+ }
75
+
76
+ const config = resolveConfig(params.profile);
77
+ const { documents, pagination } = await listDocuments(
78
+ config,
79
+ {
80
+ collectionId: params.collectionId,
81
+ parentDocumentId: params.parentDocumentId,
82
+ drafts: params.drafts,
83
+ limit: params.limit,
84
+ offset: params.offset,
85
+ sort: params.sort,
86
+ direction,
87
+ },
88
+ signal,
89
+ );
90
+
91
+ const heading = params.drafts
92
+ ? "Drafts"
93
+ : params.parentDocumentId
94
+ ? `Children of ${params.parentDocumentId}`
95
+ : params.collectionId
96
+ ? `Documents in collection ${params.collectionId}`
97
+ : "Recently updated documents";
98
+
99
+ return {
100
+ content: [
101
+ { type: "text" as const, text: formatDocumentList(documents, heading, pagination) },
102
+ ],
103
+ details: {
104
+ count: documents.length,
105
+ documents: documents.map((doc) => ({ id: doc.id, title: doc.title })),
106
+ pagination,
107
+ },
108
+ };
109
+ },
110
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * outline_move tool -- Re-file a document within the wiki.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import { resolveConfig } from "../config.ts";
7
+ import { getDocument, moveDocument } from "../clients/outline-client.ts";
8
+
9
+ export const OutlineMoveTool = {
10
+ name: "outline_move",
11
+ label: "Outline Move Document",
12
+ description:
13
+ "Move a document into another collection or under another parent document. Child documents move with it. Pass toTopLevel to lift a nested document to the root of its collection.",
14
+ parameters: Type.Object({
15
+ id: Type.String({ description: "Document id to move." }),
16
+ collectionId: Type.Optional(
17
+ Type.String({ description: "Target collection id." }),
18
+ ),
19
+ parentDocumentId: Type.Optional(
20
+ Type.String({ description: "Target parent document id (nests the document under it)." }),
21
+ ),
22
+ toTopLevel: Type.Optional(
23
+ Type.Boolean({
24
+ description: "Detach from the current parent and place at the collection root.",
25
+ default: false,
26
+ }),
27
+ ),
28
+ index: Type.Optional(
29
+ Type.Number({ description: "Position among its new siblings (0 = first)." }),
30
+ ),
31
+ profile: Type.Optional(
32
+ Type.String({ description: "Outline profile to use. Defaults to the active one." }),
33
+ ),
34
+ }),
35
+
36
+ async execute(
37
+ _toolCallId: string,
38
+ params: {
39
+ id: string;
40
+ collectionId?: string;
41
+ parentDocumentId?: string;
42
+ toTopLevel?: boolean;
43
+ index?: number;
44
+ profile?: string;
45
+ },
46
+ signal: AbortSignal,
47
+ ) {
48
+ if (!params.collectionId && !params.parentDocumentId && !params.toTopLevel) {
49
+ throw new Error(
50
+ "Specify a target: collectionId, parentDocumentId, or toTopLevel: true.",
51
+ );
52
+ }
53
+ if (params.parentDocumentId && params.toTopLevel) {
54
+ throw new Error("parentDocumentId and toTopLevel contradict each other.");
55
+ }
56
+
57
+ const config = resolveConfig(params.profile);
58
+
59
+ // documents.move needs a collection when the parent is cleared, so fall
60
+ // back to the document's current one.
61
+ let collectionId = params.collectionId;
62
+ if (!collectionId && params.toTopLevel) {
63
+ const current = await getDocument(config, params.id, signal);
64
+ collectionId = current.collectionId ?? undefined;
65
+ }
66
+
67
+ const { documentCount } = await moveDocument(
68
+ config,
69
+ {
70
+ id: params.id,
71
+ collectionId,
72
+ parentDocumentId: params.toTopLevel ? null : params.parentDocumentId,
73
+ index: params.index,
74
+ },
75
+ signal,
76
+ );
77
+
78
+ const target = params.toTopLevel
79
+ ? `the root of collection ${collectionId}`
80
+ : params.parentDocumentId
81
+ ? `under document ${params.parentDocumentId}`
82
+ : `collection ${collectionId}`;
83
+
84
+ return {
85
+ content: [
86
+ {
87
+ type: "text" as const,
88
+ text: `Moved document ${params.id} to ${target}. ${documentCount} document(s) affected.`,
89
+ },
90
+ ],
91
+ details: {
92
+ id: params.id,
93
+ collectionId,
94
+ parentDocumentId: params.toTopLevel ? null : params.parentDocumentId,
95
+ documentCount,
96
+ },
97
+ };
98
+ },
99
+ };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * outline_profile tool -- List, switch or delete configured workspaces.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import {
7
+ deleteProfile,
8
+ getActiveProfile,
9
+ getProfiles,
10
+ setActiveProfile,
11
+ } from "../config.ts";
12
+ import { formatProfileStatus } from "../formatting/formatters.ts";
13
+
14
+ export const OutlineProfileTool = {
15
+ name: "outline_profile",
16
+ label: "Manage Outline Profiles",
17
+ description:
18
+ "List configured Outline workspace profiles, switch the active one, or delete a profile. Without arguments it lists all profiles.",
19
+ parameters: Type.Object({
20
+ action: Type.Optional(
21
+ Type.String({ description: "One of: list, use, delete. Defaults to list." }),
22
+ ),
23
+ name: Type.Optional(
24
+ Type.String({ description: "Profile name for 'use' and 'delete'." }),
25
+ ),
26
+ }),
27
+
28
+ execute(
29
+ _toolCallId: string,
30
+ params: { action?: string; name?: string },
31
+ _signal: AbortSignal,
32
+ ) {
33
+ const action = (params.action || "list").toLowerCase();
34
+
35
+ if (action === "list") {
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text" as const,
40
+ text: formatProfileStatus(getProfiles(), getActiveProfile()),
41
+ },
42
+ ],
43
+ details: {
44
+ profiles: Object.keys(getProfiles()),
45
+ activeProfile: getActiveProfile(),
46
+ },
47
+ };
48
+ }
49
+
50
+ if (!params.name) {
51
+ throw new Error(`The "${action}" action requires a profile name.`);
52
+ }
53
+
54
+ if (action === "use") {
55
+ setActiveProfile(params.name);
56
+ return {
57
+ content: [
58
+ {
59
+ type: "text" as const,
60
+ text: `Active Outline profile is now "${params.name}".`,
61
+ },
62
+ ],
63
+ details: { activeProfile: params.name },
64
+ };
65
+ }
66
+
67
+ if (action === "delete") {
68
+ const removed = deleteProfile(params.name);
69
+ const text = removed
70
+ ? `Profile "${params.name}" deleted. Active profile is now ${getActiveProfile() ? `"${getActiveProfile()}"` : "unset"}.`
71
+ : `No profile named "${params.name}".`;
72
+ return {
73
+ content: [{ type: "text" as const, text }],
74
+ details: { deleted: removed, activeProfile: getActiveProfile() },
75
+ };
76
+ }
77
+
78
+ throw new Error(
79
+ `Unknown action "${params.action}". Use one of: list, use, delete.`,
80
+ );
81
+ },
82
+ };