document360-engine 0.1.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/config.d.ts +73 -0
  4. package/dist/config.js +62 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/d360/client.d.ts +30 -0
  7. package/dist/d360/client.js +132 -0
  8. package/dist/d360/client.js.map +1 -0
  9. package/dist/d360/environments.d.ts +28 -0
  10. package/dist/d360/environments.js +54 -0
  11. package/dist/d360/environments.js.map +1 -0
  12. package/dist/d360/oauth.d.ts +27 -0
  13. package/dist/d360/oauth.js +162 -0
  14. package/dist/d360/oauth.js.map +1 -0
  15. package/dist/d360/profile.d.ts +18 -0
  16. package/dist/d360/profile.js +31 -0
  17. package/dist/d360/profile.js.map +1 -0
  18. package/dist/d360/tokenStore.d.ts +17 -0
  19. package/dist/d360/tokenStore.js +50 -0
  20. package/dist/d360/tokenStore.js.map +1 -0
  21. package/dist/d360/tools.d.ts +14 -0
  22. package/dist/d360/tools.js +335 -0
  23. package/dist/d360/tools.js.map +1 -0
  24. package/dist/index.d.ts +11 -0
  25. package/dist/index.js +16 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/lib/auth.d.ts +14 -0
  28. package/dist/lib/auth.js +25 -0
  29. package/dist/lib/auth.js.map +1 -0
  30. package/dist/lib/messageQueue.d.ts +9 -0
  31. package/dist/lib/messageQueue.js +40 -0
  32. package/dist/lib/messageQueue.js.map +1 -0
  33. package/dist/lib/paths.d.ts +9 -0
  34. package/dist/lib/paths.js +35 -0
  35. package/dist/lib/paths.js.map +1 -0
  36. package/dist/lib/sessionStore.d.ts +24 -0
  37. package/dist/lib/sessionStore.js +100 -0
  38. package/dist/lib/sessionStore.js.map +1 -0
  39. package/dist/lib/titleGen.d.ts +7 -0
  40. package/dist/lib/titleGen.js +64 -0
  41. package/dist/lib/titleGen.js.map +1 -0
  42. package/dist/session.d.ts +47 -0
  43. package/dist/session.js +190 -0
  44. package/dist/session.js.map +1 -0
  45. package/package.json +44 -0
  46. package/skills/CLAUDE.md +92 -0
  47. package/skills/analyze-codebase/SKILL.md +35 -0
  48. package/skills/audit-docs/SKILL.md +48 -0
  49. package/skills/emit-screenshot-spec/SKILL.md +82 -0
  50. package/skills/gather-context-from-mcp/SKILL.md +29 -0
  51. package/skills/propose-structure/SKILL.md +51 -0
  52. package/skills/publish-to-d360/SKILL.md +49 -0
  53. package/skills/write-article/SKILL.md +95 -0
@@ -0,0 +1,162 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import { spawn } from 'node:child_process';
4
+ const LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
5
+ function base64url(buf) {
6
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
7
+ }
8
+ export function buildAuthorizationUrl(env) {
9
+ const verifier = base64url(randomBytes(32));
10
+ const challenge = base64url(createHash('sha256').update(verifier).digest());
11
+ const state = base64url(randomBytes(16));
12
+ const url = new URL(env.authorizationUrl);
13
+ url.searchParams.set('client_id', env.clientId);
14
+ url.searchParams.set('response_type', 'code');
15
+ url.searchParams.set('redirect_uri', env.redirectUri);
16
+ url.searchParams.set('scope', env.scopes.join(' '));
17
+ url.searchParams.set('state', state);
18
+ url.searchParams.set('code_challenge', challenge);
19
+ url.searchParams.set('code_challenge_method', 'S256');
20
+ if (env.acrValues)
21
+ url.searchParams.set('acr_values', env.acrValues);
22
+ if (env.prompt)
23
+ url.searchParams.set('prompt', env.prompt);
24
+ return { url: url.toString(), verifier, state };
25
+ }
26
+ function openBrowser(url) {
27
+ // D360_NO_BROWSER=1: print-only mode (SSH, CI, tests).
28
+ if (process.env.D360_NO_BROWSER)
29
+ return;
30
+ const platform = process.platform;
31
+ if (platform === 'win32') {
32
+ // DO NOT use `cmd /c start` — cmd treats '&' in the URL as a command
33
+ // separator and truncates the query string at the first '&' (dropping
34
+ // redirect_uri → "Invalid redirect_uri"). PowerShell Start-Process with a
35
+ // single-quoted URL passes the whole thing through verbatim.
36
+ const safe = url.replace(/'/g, "''");
37
+ spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', `Start-Process '${safe}'`], {
38
+ detached: true,
39
+ stdio: 'ignore',
40
+ }).unref();
41
+ }
42
+ else if (platform === 'darwin') {
43
+ spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
44
+ }
45
+ else {
46
+ spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
47
+ }
48
+ }
49
+ /** Wait for the OAuth redirect on the loopback address from redirectUri. */
50
+ function waitForCallback(redirectUri, expectedState) {
51
+ const target = new URL(redirectUri);
52
+ let server;
53
+ const result = new Promise((resolve, reject) => {
54
+ const timer = setTimeout(() => {
55
+ reject(new Error(`Timed out after ${LOGIN_TIMEOUT_MS / 60000} minutes waiting for the browser login.`));
56
+ server.close();
57
+ }, LOGIN_TIMEOUT_MS);
58
+ server = createServer((req, res) => {
59
+ const reqUrl = new URL(req.url ?? '/', `http://${target.host}`);
60
+ if (reqUrl.pathname !== target.pathname) {
61
+ res.writeHead(404).end();
62
+ return;
63
+ }
64
+ const error = reqUrl.searchParams.get('error');
65
+ const code = reqUrl.searchParams.get('code');
66
+ const state = reqUrl.searchParams.get('state');
67
+ const fail = (msg) => {
68
+ res.writeHead(400, { 'content-type': 'text/html' });
69
+ res.end(`<html><body><h3>Login failed</h3><p>${msg}</p><p>Return to the terminal.</p></body></html>`);
70
+ clearTimeout(timer);
71
+ reject(new Error(msg));
72
+ server.close();
73
+ };
74
+ if (error)
75
+ return fail(`${error}: ${reqUrl.searchParams.get('error_description') ?? ''}`);
76
+ if (!code)
77
+ return fail('No authorization code in the callback.');
78
+ if (state !== expectedState)
79
+ return fail('State mismatch — possible CSRF; try again.');
80
+ res.writeHead(200, { 'content-type': 'text/html' });
81
+ res.end('<html><body><h3>✓ Document360 login complete</h3><p>You can close this tab and return to the terminal.</p></body></html>');
82
+ clearTimeout(timer);
83
+ resolve(code);
84
+ server.close();
85
+ });
86
+ server.on('error', err => {
87
+ clearTimeout(timer);
88
+ reject(new Error(`Could not listen on ${target.host} (${err.message}). ` +
89
+ `Another process may hold the port — set D360_REDIRECT_URI to a free port and retry.`));
90
+ });
91
+ server.listen(Number(target.port) || 80, target.hostname);
92
+ });
93
+ return { server: server, result };
94
+ }
95
+ async function requestTokens(env, body) {
96
+ const res = await fetch(env.tokenUrl, {
97
+ method: 'POST',
98
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
99
+ body,
100
+ });
101
+ const text = await res.text();
102
+ if (!res.ok) {
103
+ throw new Error(`Token endpoint ${res.status}: ${text.slice(0, 400)}`);
104
+ }
105
+ return JSON.parse(text);
106
+ }
107
+ export async function exchangeCode(env, code, verifier) {
108
+ return requestTokens(env, new URLSearchParams({
109
+ grant_type: 'authorization_code',
110
+ code,
111
+ redirect_uri: env.redirectUri,
112
+ client_id: env.clientId,
113
+ code_verifier: verifier,
114
+ }));
115
+ }
116
+ export async function refreshTokens(env, refreshToken) {
117
+ return requestTokens(env, new URLSearchParams({
118
+ grant_type: 'refresh_token',
119
+ refresh_token: refreshToken,
120
+ client_id: env.clientId,
121
+ }));
122
+ }
123
+ export function toStoredTokens(profile, t) {
124
+ const now = Date.now();
125
+ return {
126
+ profile,
127
+ accessToken: t.access_token,
128
+ refreshToken: t.refresh_token,
129
+ idToken: t.id_token,
130
+ scope: t.scope,
131
+ obtainedAt: new Date(now).toISOString(),
132
+ expiresAt: new Date(now + (t.expires_in ?? 3600) * 1000).toISOString(),
133
+ };
134
+ }
135
+ /**
136
+ * Interactive login. Default: loopback listener + browser. Manual mode: no
137
+ * listener — the user pastes the full redirect URL (works even when the
138
+ * registered redirect URI is not a loopback we can bind).
139
+ */
140
+ export async function loginPkce(env, opts, log) {
141
+ const { url, verifier, state } = buildAuthorizationUrl(env);
142
+ if (opts.manual) {
143
+ log('Open this URL in a browser and sign in:');
144
+ log(url);
145
+ const pasted = await opts.promptForRedirect('Paste the full URL you were redirected to (it contains ?code=...):');
146
+ const redirected = new URL(pasted.trim());
147
+ const code = redirected.searchParams.get('code');
148
+ const gotState = redirected.searchParams.get('state');
149
+ if (!code)
150
+ throw new Error('No code parameter found in the pasted URL.');
151
+ if (gotState !== state)
152
+ throw new Error('State mismatch in the pasted URL — restart the login.');
153
+ return exchangeCode(env, code, verifier);
154
+ }
155
+ const { result } = waitForCallback(env.redirectUri, state);
156
+ log('Opening your browser for Document360 sign-in…');
157
+ log(`If it did not open, visit:\n ${url}`);
158
+ openBrowser(url);
159
+ const code = await result;
160
+ return exchangeCode(env, code, verifier);
161
+ }
162
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/d360/oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { type ProfileProject } from '../config.js';
2
+ import { type D360Environment } from './environments.js';
3
+ export type ActiveProfile = {
4
+ /** Profile name — the auth token store key and what the user sees/switches. */
5
+ name: string;
6
+ connection: D360Environment;
7
+ project: ProfileProject;
8
+ production: boolean;
9
+ };
10
+ /**
11
+ * Resolve the active profile:
12
+ * - no `.d360-writer.json` at all → a bare berlin default (lets you `login` before `init`);
13
+ * - config WITH `profiles` → resolve by name → defaultProfile;
14
+ * - config WITHOUT `profiles` → ProfileConfigError (old shape; forces migration).
15
+ */
16
+ export declare function resolveActiveProfile(cwd: string, nameOverride?: string): ActiveProfile;
17
+ /** Persist a project id into a profile's `project` aspect (used by login auto-fill). */
18
+ export declare function setProfileProject(cwd: string, profileName: string, project: ProfileProject): void;
@@ -0,0 +1,31 @@
1
+ import { readProjectConfig, resolveProfile, writeProjectConfig } from '../config.js';
2
+ import { resolveConnection } from './environments.js';
3
+ /**
4
+ * Resolve the active profile:
5
+ * - no `.d360-writer.json` at all → a bare berlin default (lets you `login` before `init`);
6
+ * - config WITH `profiles` → resolve by name → defaultProfile;
7
+ * - config WITHOUT `profiles` → ProfileConfigError (old shape; forces migration).
8
+ */
9
+ export function resolveActiveProfile(cwd, nameOverride) {
10
+ const cfg = readProjectConfig(cwd);
11
+ if (cfg === null) {
12
+ const name = nameOverride ?? 'berlin';
13
+ return { name, connection: resolveConnection({ environment: name }), project: {}, production: false };
14
+ }
15
+ const { name, profile } = resolveProfile(cfg, nameOverride);
16
+ return {
17
+ name,
18
+ connection: resolveConnection(profile.connection),
19
+ project: profile.project ?? {},
20
+ production: profile.production === true,
21
+ };
22
+ }
23
+ /** Persist a project id into a profile's `project` aspect (used by login auto-fill). */
24
+ export function setProfileProject(cwd, profileName, project) {
25
+ const cfg = readProjectConfig(cwd);
26
+ if (!cfg?.profiles?.[profileName])
27
+ return;
28
+ cfg.profiles[profileName].project = { ...cfg.profiles[profileName].project, ...project };
29
+ writeProjectConfig(cfg, cwd);
30
+ }
31
+ //# sourceMappingURL=profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/d360/profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,kBAAkB,EAA2C,MAAM,cAAc,CAAC;AAC9H,OAAO,EAAE,iBAAiB,EAAwB,MAAM,mBAAmB,CAAC;AAU5E;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW,EAAE,YAAqB;IACrE,MAAM,GAAG,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,YAAY,IAAI,QAAQ,CAAC;QACtC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,iBAAiB,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IACxG,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAC5D,OAAO;QACL,IAAI;QACJ,UAAU,EAAE,iBAAiB,CAAC,OAAO,CAAC,UAAU,CAAC;QACjD,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE;QAC9B,UAAU,EAAE,OAAO,CAAC,UAAU,KAAK,IAAI;KACxC,CAAC;AACJ,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,WAAmB,EAAE,OAAuB;IACzF,MAAM,GAAG,GAAG,iBAAiB,CAAC,GAAG,CAAyB,CAAC;IAC3D,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,WAAW,CAAC;QAAE,OAAO;IAC1C,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,OAAO,GAAG,EAAE,GAAG,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;IACzF,kBAAkB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,17 @@
1
+ export type StoredTokens = {
2
+ /** Profile name this session belongs to (the auth store key). */
3
+ profile: string;
4
+ accessToken: string;
5
+ refreshToken?: string;
6
+ idToken?: string;
7
+ scope?: string;
8
+ obtainedAt: string;
9
+ /** ISO timestamp computed from expires_in at save time. */
10
+ expiresAt: string;
11
+ };
12
+ export declare function saveTokens(tokens: StoredTokens): void;
13
+ export declare function loadTokens(profile: string): StoredTokens | null;
14
+ export declare function clearTokens(profile: string): boolean;
15
+ export declare function isExpired(tokens: StoredTokens, skewSeconds?: number): boolean;
16
+ /** Decode a JWT's payload without verification (for display/claims discovery only). */
17
+ export declare function decodeJwtClaims(token: string | undefined): Record<string, unknown> | null;
@@ -0,0 +1,50 @@
1
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { userDir, ensureDir } from '../lib/paths.js';
4
+ function authDir() {
5
+ return join(userDir(), 'auth');
6
+ }
7
+ function tokenPath(profile) {
8
+ return join(authDir(), `${profile}.json`);
9
+ }
10
+ export function saveTokens(tokens) {
11
+ ensureDir(authDir());
12
+ writeFileSync(tokenPath(tokens.profile), JSON.stringify(tokens, null, 2));
13
+ }
14
+ export function loadTokens(profile) {
15
+ const path = tokenPath(profile);
16
+ if (!existsSync(path))
17
+ return null;
18
+ try {
19
+ return JSON.parse(readFileSync(path, 'utf8'));
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ export function clearTokens(profile) {
26
+ const path = tokenPath(profile);
27
+ if (!existsSync(path))
28
+ return false;
29
+ unlinkSync(path);
30
+ return true;
31
+ }
32
+ export function isExpired(tokens, skewSeconds = 60) {
33
+ return Date.now() >= new Date(tokens.expiresAt).getTime() - skewSeconds * 1000;
34
+ }
35
+ /** Decode a JWT's payload without verification (for display/claims discovery only). */
36
+ export function decodeJwtClaims(token) {
37
+ if (!token)
38
+ return null;
39
+ const parts = token.split('.');
40
+ if (parts.length !== 3)
41
+ return null; // opaque token
42
+ try {
43
+ const payload = Buffer.from(parts[1].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
44
+ return JSON.parse(payload);
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ //# sourceMappingURL=tokenStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenStore.js","sourceRoot":"","sources":["../../src/d360/tokenStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC9E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAcrD,SAAS,OAAO;IACd,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,SAAS,CAAC,OAAe;IAChC,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,GAAG,OAAO,OAAO,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAoB;IAC7C,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC;IACrB,aAAa,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAiB,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,UAAU,CAAC,IAAI,CAAC,CAAC;IACjB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,MAAoB,EAAE,WAAW,GAAG,EAAE;IAC9D,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,GAAG,WAAW,GAAG,IAAI,CAAC;AACjF,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,eAAe,CAAC,KAAyB;IACvD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,eAAe;IACpD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACxG,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAA4B,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { McpServerConfig } from '@anthropic-ai/claude-agent-sdk';
2
+ import { D360AuthError } from './client.js';
3
+ import type { ActiveProfile } from './profile.js';
4
+ export type ToolServerOptions = {
5
+ /** False on a production profile until the user authorizes writes (/allow-prod or --yes). */
6
+ writesAllowed: boolean;
7
+ };
8
+ /**
9
+ * Build the in-process Document360 tool server for the active profile. Project resolves
10
+ * per-call: explicit arg → profile.project.projectId → the logged-in token's doc360_project_id claim.
11
+ * Write tools are gated by opts.writesAllowed (production safety).
12
+ */
13
+ export declare function buildD360ToolServer(active: ActiveProfile, opts: ToolServerOptions): McpServerConfig;
14
+ export { D360AuthError };
@@ -0,0 +1,335 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { basename, isAbsolute, resolve } from 'node:path';
3
+ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
4
+ import { z } from 'zod';
5
+ import { d360Get, d360GetAll, d360Patch, d360Post, d360Upload, resolveProjectId, D360AuthError } from './client.js';
6
+ const PROJECTS = '/v3/projects';
7
+ const p = (projectId) => `${PROJECTS}/${projectId}`;
8
+ function ok(data) {
9
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
10
+ }
11
+ function fail(err) {
12
+ const msg = err instanceof Error ? err.message : String(err);
13
+ return { content: [{ type: 'text', text: msg }], isError: true };
14
+ }
15
+ /**
16
+ * Build the in-process Document360 tool server for the active profile. Project resolves
17
+ * per-call: explicit arg → profile.project.projectId → the logged-in token's doc360_project_id claim.
18
+ * Write tools are gated by opts.writesAllowed (production safety).
19
+ */
20
+ export function buildD360ToolServer(active, opts) {
21
+ const ctx = { profile: active.name, connection: active.connection };
22
+ const configProjectId = active.project.projectId;
23
+ const defaultWorkspaceId = active.project.workspaceId;
24
+ const project = () => resolveProjectId(ctx, configProjectId);
25
+ const workspace = (given) => {
26
+ const w = given ?? defaultWorkspaceId;
27
+ if (!w)
28
+ throw new Error('No workspace_id given and none in the profile. Call d360_list_workspaces first.');
29
+ return w;
30
+ };
31
+ /** Returns a fail() result if writes are blocked on a production profile, else null. */
32
+ const writeBlocked = () => opts.writesAllowed
33
+ ? null
34
+ : fail(`Refusing to write to PRODUCTION profile "${active.name}". Authorize this session first: ` +
35
+ `run /allow-prod in the REPL, or pass --yes in one-shot/CI.`);
36
+ const context = tool('d360_context', 'Report the active Document360 connection: profile name, environment, project/workspace id, and whether it is a production profile. Call this before maintaining d360-category-map.json so you scope IDs to the right profile.', {}, async () => {
37
+ try {
38
+ return ok({
39
+ profile: active.name,
40
+ environment: active.connection.name,
41
+ production: active.production,
42
+ projectId: configProjectId ?? resolveProjectId(ctx),
43
+ workspaceId: defaultWorkspaceId ?? null,
44
+ });
45
+ }
46
+ catch (e) {
47
+ return fail(e);
48
+ }
49
+ });
50
+ const listProjects = tool('d360_list_projects', 'List all Document360 projects the signed-in user can access (id, name, sub-domain, status).', {}, async () => {
51
+ try {
52
+ return ok(await d360GetAll(ctx, PROJECTS));
53
+ }
54
+ catch (e) {
55
+ return fail(e);
56
+ }
57
+ });
58
+ const listWorkspaces = tool('d360_list_workspaces', 'List workspaces (project versions) for the current project. Each has id, name, slug, is_default, workspace_type.', { project_id: z.string().optional().describe('Defaults to the logged-in/config project.') }, async (args) => {
59
+ try {
60
+ return ok(await d360GetAll(ctx, `${p(args.project_id ?? project())}/workspaces`));
61
+ }
62
+ catch (e) {
63
+ return fail(e);
64
+ }
65
+ });
66
+ const listCategories = tool('d360_list_categories', 'List categories in a workspace (the folder structure for articles).', {
67
+ workspace_id: z.string().optional().describe('Defaults to d360.workspaceId in config.'),
68
+ project_id: z.string().optional(),
69
+ }, async (args) => {
70
+ try {
71
+ const workspaceId = args.workspace_id ?? defaultWorkspaceId;
72
+ if (!workspaceId)
73
+ return fail('No workspace_id given and none in config. Call d360_list_workspaces first.');
74
+ return ok(await d360GetAll(ctx, `${p(args.project_id ?? project())}/workspaces/${workspaceId}/categories`));
75
+ }
76
+ catch (e) {
77
+ return fail(e);
78
+ }
79
+ });
80
+ const listArticles = tool('d360_list_articles', 'List articles in a workspace (id, title, status, category). Use to see existing docs before writing.', {
81
+ workspace_id: z.string().optional().describe('Defaults to d360.workspaceId in config.'),
82
+ project_id: z.string().optional(),
83
+ }, async (args) => {
84
+ try {
85
+ const workspaceId = args.workspace_id ?? defaultWorkspaceId;
86
+ if (!workspaceId)
87
+ return fail('No workspace_id given and none in config. Call d360_list_workspaces first.');
88
+ return ok(await d360GetAll(ctx, `${p(args.project_id ?? project())}/workspaces/${workspaceId}/articles`));
89
+ }
90
+ catch (e) {
91
+ return fail(e);
92
+ }
93
+ });
94
+ const getArticle = tool('d360_get_article', 'Get a single article including its content. content_mode=raw returns the stored markdown source (best for editing); display returns processed content.', {
95
+ article_id: z.string(),
96
+ content_mode: z.enum(['raw', 'display']).optional().describe('raw = stored markdown source (default), display = processed.'),
97
+ published: z.boolean().optional().describe('Read the published version instead of the latest draft.'),
98
+ project_id: z.string().optional(),
99
+ }, async (args) => {
100
+ try {
101
+ return ok(await d360Get(ctx, `${p(args.project_id ?? project())}/articles/${args.article_id}`, {
102
+ query: { content_mode: args.content_mode, published: args.published },
103
+ }));
104
+ }
105
+ catch (e) {
106
+ return fail(e);
107
+ }
108
+ });
109
+ const aiQuery = tool('d360_ai_query', "Ask Document360's AI search over a workspace's published content. Returns an answer grounded in existing articles — use to check what's already documented.", {
110
+ query: z.string(),
111
+ workspace_id: z.string().optional().describe('Defaults to d360.workspaceId in config.'),
112
+ project_id: z.string().optional(),
113
+ }, async (args) => {
114
+ try {
115
+ const workspaceId = args.workspace_id ?? defaultWorkspaceId;
116
+ if (!workspaceId)
117
+ return fail('No workspace_id given and none in config. Call d360_list_workspaces first.');
118
+ return ok(await d360Post(ctx, `${p(args.project_id ?? project())}/workspaces/ai/query`, {
119
+ body: { query: args.query, workspace_id: workspaceId },
120
+ }));
121
+ }
122
+ catch (e) {
123
+ return fail(e);
124
+ }
125
+ });
126
+ // ---- write tools (gated by writesAllowed) ----
127
+ const createCategory = tool('d360_create_category', 'Create a category (a docs folder). Returns the new category id.', {
128
+ name: z.string(),
129
+ workspace_id: z.string().optional(),
130
+ parent_category_id: z.string().optional().describe('Omit for a top-level category.'),
131
+ content: z.string().optional(),
132
+ slug: z.string().optional(),
133
+ order: z.number().optional(),
134
+ project_id: z.string().optional(),
135
+ }, async (args) => {
136
+ const blocked = writeBlocked();
137
+ if (blocked)
138
+ return blocked;
139
+ try {
140
+ return ok(await d360Post(ctx, `${p(args.project_id ?? project())}/categories`, {
141
+ body: {
142
+ name: args.name,
143
+ workspace_id: workspace(args.workspace_id),
144
+ parent_category_id: args.parent_category_id,
145
+ content: args.content,
146
+ slug: args.slug,
147
+ order: args.order,
148
+ // Markdown-only by product decision (matches articles).
149
+ content_type: 'markdown',
150
+ },
151
+ }));
152
+ }
153
+ catch (e) {
154
+ return fail(e);
155
+ }
156
+ });
157
+ const createArticle = tool('d360_create_article', 'Create a DRAFT article in a category. The body is always Markdown (product rule — we never create WYSIWYG/Block articles). Returns the new article id. Does not publish.', {
158
+ title: z.string(),
159
+ category_id: z.string(),
160
+ content: z.string().optional().describe('Markdown body.'),
161
+ workspace_id: z.string().optional(),
162
+ slug: z.string().optional(),
163
+ order: z.number().optional(),
164
+ project_id: z.string().optional(),
165
+ }, async (args) => {
166
+ const blocked = writeBlocked();
167
+ if (blocked)
168
+ return blocked;
169
+ try {
170
+ // articles/bulk with a single item is the create path. content_type is
171
+ // hardcoded to 'markdown' by product decision — Document360 articles are
172
+ // markdown-only for control + simplicity (no WYSIWYG/Block).
173
+ return ok(await d360Post(ctx, `${p(args.project_id ?? project())}/articles/bulk`, {
174
+ body: {
175
+ articles: [
176
+ {
177
+ title: args.title,
178
+ category_id: args.category_id,
179
+ workspace_id: workspace(args.workspace_id),
180
+ content: args.content,
181
+ slug: args.slug,
182
+ order: args.order,
183
+ content_type: 'markdown',
184
+ },
185
+ ],
186
+ },
187
+ }));
188
+ }
189
+ catch (e) {
190
+ return fail(e);
191
+ }
192
+ });
193
+ const updateArticle = tool('d360_update_article', 'Update an article\'s title/content/category. Edits the latest draft by default; set auto_fork to safely edit a published article (creates a new draft version).', {
194
+ article_id: z.string(),
195
+ title: z.string().optional(),
196
+ content: z.string().optional().describe('Markdown body.'),
197
+ category_id: z.string().optional(),
198
+ hidden: z.boolean().optional(),
199
+ version_number: z.number().optional(),
200
+ auto_fork: z.boolean().optional().describe('If the target version is published, fork a new draft instead of erroring.'),
201
+ project_id: z.string().optional(),
202
+ }, async (args) => {
203
+ const blocked = writeBlocked();
204
+ if (blocked)
205
+ return blocked;
206
+ try {
207
+ const { article_id, project_id, ...body } = args;
208
+ return ok(await d360Patch(ctx, `${p(project_id ?? project())}/articles/${article_id}`, { body }));
209
+ }
210
+ catch (e) {
211
+ return fail(e);
212
+ }
213
+ });
214
+ const forkArticle = tool('d360_fork_article', 'Fork a published article into a new draft version — the safe way to start editing live content.', { article_id: z.string(), project_id: z.string().optional() }, async (args) => {
215
+ const blocked = writeBlocked();
216
+ if (blocked)
217
+ return blocked;
218
+ try {
219
+ return ok(await d360Post(ctx, `${p(args.project_id ?? project())}/articles/${args.article_id}/fork`));
220
+ }
221
+ catch (e) {
222
+ return fail(e);
223
+ }
224
+ });
225
+ const publishArticle = tool('d360_publish_article', 'Publish an article version. This makes the draft live to readers — only call when the user explicitly asks to publish.', {
226
+ article_id: z.string(),
227
+ version_number: z.number(),
228
+ workspace_id: z.string().optional(),
229
+ message: z.string().optional(),
230
+ project_id: z.string().optional(),
231
+ }, async (args) => {
232
+ const blocked = writeBlocked();
233
+ if (blocked)
234
+ return blocked;
235
+ try {
236
+ return ok(await d360Post(ctx, `${p(args.project_id ?? project())}/articles/${args.article_id}/publish`, {
237
+ body: { workspace_id: workspace(args.workspace_id), version_number: args.version_number, message: args.message },
238
+ }));
239
+ }
240
+ catch (e) {
241
+ return fail(e);
242
+ }
243
+ });
244
+ const unpublishArticle = tool('d360_unpublish_article', 'Unpublish an article, reverting it to draft (removes it from readers).', {
245
+ article_id: z.string(),
246
+ version_number: z.number().optional(),
247
+ workspace_id: z.string().optional(),
248
+ project_id: z.string().optional(),
249
+ }, async (args) => {
250
+ const blocked = writeBlocked();
251
+ if (blocked)
252
+ return blocked;
253
+ try {
254
+ return ok(await d360Post(ctx, `${p(args.project_id ?? project())}/articles/${args.article_id}/unpublish`, {
255
+ body: { workspace_id: workspace(args.workspace_id), version_number: args.version_number },
256
+ }));
257
+ }
258
+ catch (e) {
259
+ return fail(e);
260
+ }
261
+ });
262
+ const uploadDriveFile = tool('d360_upload_drive_file', 'Upload a local file (e.g. a captured screenshot PNG) to Drive and return its URL for embedding in an article. Uploads to the default folder unless folder_id is given.', {
263
+ file_path: z.string().describe('Local path to the file (absolute, or relative to the repo).'),
264
+ folder_id: z.string().optional(),
265
+ title: z.string().optional(),
266
+ project_id: z.string().optional(),
267
+ }, async (args) => {
268
+ const blocked = writeBlocked();
269
+ if (blocked)
270
+ return blocked;
271
+ try {
272
+ const pid = args.project_id ?? project();
273
+ const filePath = isAbsolute(args.file_path) ? args.file_path : resolve(process.cwd(), args.file_path);
274
+ let folderId = args.folder_id;
275
+ if (!folderId) {
276
+ const def = await d360Get(ctx, `${p(pid)}/drive/folders/default`);
277
+ folderId = def?.id;
278
+ if (!folderId)
279
+ return fail('Could not resolve the default Drive folder.');
280
+ }
281
+ const bytes = readFileSync(filePath);
282
+ const form = new FormData();
283
+ // NOTE: field name 'file' is unverified against staging (spec models the
284
+ // multipart body loosely) — confirm during the dogfood; adjust if rejected.
285
+ form.append('file', new Blob([bytes]), args.title ?? basename(filePath));
286
+ return ok(await d360Upload(ctx, `${p(pid)}/drive/folders/${folderId}/files`, form));
287
+ }
288
+ catch (e) {
289
+ return fail(e);
290
+ }
291
+ });
292
+ const searchDrive = tool('d360_search_drive', 'Search Drive files (e.g. to reuse an already-uploaded screenshot instead of uploading a duplicate).', {
293
+ search_keyword: z.string().optional(),
294
+ allow_images_only: z.boolean().optional(),
295
+ project_id: z.string().optional(),
296
+ }, async (args) => {
297
+ try {
298
+ return ok(await d360GetAll(ctx, `${p(args.project_id ?? project())}/drive/search`, {
299
+ query: { search_keyword: args.search_keyword, allow_images_only: args.allow_images_only },
300
+ }));
301
+ }
302
+ catch (e) {
303
+ return fail(e);
304
+ }
305
+ });
306
+ return createSdkMcpServer({
307
+ name: 'document360',
308
+ version: '0.2.0',
309
+ instructions: 'First-party Document360 tools. The signed-in user\'s permissions apply server-side. ' +
310
+ 'Project/workspace default from the active profile; pass ids explicitly to override. ' +
311
+ 'Create articles as DRAFTS; only call d360_publish_article when the user explicitly asks to publish. ' +
312
+ 'All articles are Markdown — the tools enforce this; never attempt WYSIWYG/Block content. ' +
313
+ 'To edit a published article, fork it (d360_fork_article) or update with auto_fork. ' +
314
+ 'Auth errors mean the session expired — tell the user to run: d360-writer login.',
315
+ tools: [
316
+ context,
317
+ listProjects,
318
+ listWorkspaces,
319
+ listCategories,
320
+ listArticles,
321
+ getArticle,
322
+ aiQuery,
323
+ createCategory,
324
+ createArticle,
325
+ updateArticle,
326
+ forkArticle,
327
+ publishArticle,
328
+ unpublishArticle,
329
+ uploadDriveFile,
330
+ searchDrive,
331
+ ],
332
+ });
333
+ }
334
+ export { D360AuthError };
335
+ //# sourceMappingURL=tools.js.map