opencode-wiki-historian 0.2.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +381 -0
  3. package/dist/chronology.d.ts +36 -0
  4. package/dist/chronology.js +67 -0
  5. package/dist/config.d.ts +112 -0
  6. package/dist/config.js +158 -0
  7. package/dist/index.d.ts +31 -0
  8. package/dist/index.js +136 -0
  9. package/dist/jsonc.d.ts +17 -0
  10. package/dist/jsonc.js +131 -0
  11. package/dist/map.d.ts +58 -0
  12. package/dist/map.js +196 -0
  13. package/dist/migrate-apply.d.ts +40 -0
  14. package/dist/migrate-apply.js +144 -0
  15. package/dist/migrate-score.d.ts +29 -0
  16. package/dist/migrate-score.js +267 -0
  17. package/dist/migrate-store.d.ts +52 -0
  18. package/dist/migrate-store.js +77 -0
  19. package/dist/migrate.d.ts +65 -0
  20. package/dist/migrate.js +111 -0
  21. package/dist/templates/genres.d.ts +65 -0
  22. package/dist/templates/genres.js +228 -0
  23. package/dist/templates/skeletons.d.ts +48 -0
  24. package/dist/templates/skeletons.js +558 -0
  25. package/dist/tools/create.d.ts +9 -0
  26. package/dist/tools/create.js +77 -0
  27. package/dist/tools/local.d.ts +10 -0
  28. package/dist/tools/local.js +107 -0
  29. package/dist/tools/mutate.d.ts +11 -0
  30. package/dist/tools/mutate.js +157 -0
  31. package/dist/tools/read.d.ts +9 -0
  32. package/dist/tools/read.js +104 -0
  33. package/dist/tools/shared.d.ts +52 -0
  34. package/dist/tools/shared.js +87 -0
  35. package/dist/tools/write.d.ts +10 -0
  36. package/dist/tools/write.js +148 -0
  37. package/dist/tools.d.ts +23 -0
  38. package/dist/tools.js +43 -0
  39. package/dist/translate.d.ts +44 -0
  40. package/dist/translate.js +207 -0
  41. package/dist/wiki/assets.d.ts +42 -0
  42. package/dist/wiki/assets.js +91 -0
  43. package/dist/wiki/client.d.ts +67 -0
  44. package/dist/wiki/client.js +221 -0
  45. package/dist/wiki/locale.d.ts +66 -0
  46. package/dist/wiki/locale.js +154 -0
  47. package/dist/wiki/pages.d.ts +7 -0
  48. package/dist/wiki/pages.js +7 -0
  49. package/dist/wiki/pages.read.d.ts +114 -0
  50. package/dist/wiki/pages.read.js +114 -0
  51. package/dist/wiki/pages.write.d.ts +109 -0
  52. package/dist/wiki/pages.write.js +201 -0
  53. package/package.json +36 -0
  54. package/skills/historian/SKILL.md +294 -0
  55. package/skills/historian/references/adapting-your-own-wiki.md +53 -0
  56. package/skills/historian/references/genres.md +160 -0
  57. package/skills/historian/references/rules.md +30 -0
  58. package/skills/historian/references/style.md +84 -0
  59. package/skills/historian/references/wikijs-guide.md +87 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * historian_translate_snippet + historian_map: tools that never write to the
3
+ * wiki engine (map refresh writes its cache page + mirror via the engine, but
4
+ * only on the explicit refresh action). translate_snippet surfaces engine
5
+ * TranslateError causes as structured output — never a throw.
6
+ */
7
+ import { tool } from '@opencode-ai/plugin';
8
+ import { TranslateError } from '../translate.js';
9
+ import { buildChronology, filterRowsByPath } from '../chronology.js';
10
+ import { getMap, refreshMapCache, CACHE_PATH } from '../map.js';
11
+ import { errEnvelope, okJson, reportUrls, URL_MANDATE } from './shared.js';
12
+ const s = tool.schema;
13
+ const TRANSLATE_ARGS = {
14
+ text: s.string(),
15
+ from: s.enum(['en', 'zh']).default('en'),
16
+ to: s.enum(['en', 'zh']).default('zh'),
17
+ };
18
+ const TranslateArgsSchema = s.object(TRANSLATE_ARGS);
19
+ // --- historian_translate_snippet ---------------------------------------------
20
+ export function makeTranslateSnippetTool(deps) {
21
+ return tool({
22
+ description: `Translate a text snippet with the configured translation engine (pure-local, no wiki interaction). ` +
23
+ `The engine may emit thinking parts first — only the final translated text is returned. ` +
24
+ `Read the translation and verify it reads naturally in context before reuse.`,
25
+ args: TRANSLATE_ARGS,
26
+ execute: async (raw) => {
27
+ const args = TranslateArgsSchema.parse(raw);
28
+ if (deps.translate === undefined) {
29
+ return JSON.stringify({
30
+ ok: false,
31
+ error: 'not-wired',
32
+ detail: 'No translation engine wired — configure translate (endpoint/model/apiKey) or pass a translator to buildTools.',
33
+ actionableHint: 'Wire translation in the plugin config and retry.',
34
+ }, null, 2);
35
+ }
36
+ try {
37
+ const translated = await deps.translate(args.text, args.from, args.to);
38
+ return okJson({ translated, from: args.from, to: args.to });
39
+ }
40
+ catch (err) {
41
+ if (err instanceof TranslateError) {
42
+ return JSON.stringify({
43
+ ok: false,
44
+ error: err.cause,
45
+ detail: err.detail,
46
+ message: err.message,
47
+ actionableHint: 'Retry the translation, or use the raw text as-is.',
48
+ }, null, 2);
49
+ }
50
+ return errEnvelope(err);
51
+ }
52
+ },
53
+ });
54
+ }
55
+ const MAP_ARGS = {
56
+ action: s.enum(['show', 'refresh', 'timeline']).default('show'),
57
+ days: s.number().int().positive().optional().describe('timeline: keep only rows updated within the last N days'),
58
+ path: s.string().optional().describe('timeline: section/path prefix filter (e.g. ops)'),
59
+ };
60
+ const MapArgsSchema = s.object(MAP_ARGS);
61
+ // --- historian_map -----------------------------------------------------------
62
+ export function makeMapTool(deps) {
63
+ return tool({
64
+ description: `Inspect (show), rebuild (refresh), or aggregate recent updates (timeline) over the en/zh page map ` +
65
+ `with its local mirror + _meta/page-map cache page. ` +
66
+ `show reads the local mirror (zero writes); refresh rebuilds from the wiki and writes the mirror + cache page ` +
67
+ `(idempotent — the engine upserts via full RMW); timeline groups mirror rows by ISO week (newest first, ` +
68
+ `optional days window + section/path prefix filter) into a human markdown table + machine-readable weeks JSON. ` +
69
+ `${URL_MANDATE}.`,
70
+ args: MAP_ARGS,
71
+ execute: async (raw) => {
72
+ const args = MapArgsSchema.parse(raw);
73
+ try {
74
+ const mapDeps = { client: deps.getClient(), options: deps.options };
75
+ if (args.action === 'refresh') {
76
+ const result = await refreshMapCache(mapDeps, { homeDir: deps.homeDir });
77
+ return okJson({ action: 'refresh', stats: result.stats, cacheUrl: result.cacheUrl });
78
+ }
79
+ const snapshot = await getMap(mapDeps, deps.homeDir);
80
+ if (args.action === 'timeline') {
81
+ const rows = args.path === undefined ? snapshot.rows : filterRowsByPath(snapshot.rows, args.path);
82
+ const chrono = buildChronology(rows, { days: args.days });
83
+ return okJson({
84
+ action: 'timeline',
85
+ days: args.days ?? null,
86
+ pathPrefix: args.path ?? null,
87
+ generatedAt: snapshot.generatedAt,
88
+ rows: rows.length,
89
+ weeks: chrono.weeks,
90
+ markdown: chrono.markdown,
91
+ urls: reportUrls(deps.options.baseUrl, CACHE_PATH, 'en'),
92
+ });
93
+ }
94
+ return okJson({
95
+ action: 'show',
96
+ generatedAt: snapshot.generatedAt,
97
+ staleSeconds: snapshot.staleSeconds,
98
+ stats: snapshot.stats,
99
+ rows: snapshot.rows,
100
+ });
101
+ }
102
+ catch (err) {
103
+ return errEnvelope(err);
104
+ }
105
+ },
106
+ });
107
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * historian_delete + historian_move + historian_migrate: destructive ops are
3
+ * gated on confirm:"yes" BEFORE any fetch; migrate runs the todo-14 engine —
4
+ * dry-run (LLM restyle + deterministic checklist, no writes) or apply
5
+ * (pre-image backup first, per-locale upsert, checkpoint, verification).
6
+ */
7
+ import { type ToolDefinition } from '@opencode-ai/plugin';
8
+ import { type ToolDeps } from './shared.js';
9
+ export declare function makeDeleteTool(deps: ToolDeps): ToolDefinition;
10
+ export declare function makeMoveTool(deps: ToolDeps): ToolDefinition;
11
+ export declare function makeMigrateTool(deps: ToolDeps): ToolDefinition;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * historian_delete + historian_move + historian_migrate: destructive ops are
3
+ * gated on confirm:"yes" BEFORE any fetch; migrate runs the todo-14 engine —
4
+ * dry-run (LLM restyle + deterministic checklist, no writes) or apply
5
+ * (pre-image backup first, per-locale upsert, checkpoint, verification).
6
+ */
7
+ import { tool } from '@opencode-ai/plugin';
8
+ import { deletePage, movePage } from '../wiki/pages.js';
9
+ import { selfReviewChecklist } from '../templates/genres.js';
10
+ import { confirmRequiredJson, errEnvelope, okJson, urlPair, URL_MANDATE, pageDeps } from './shared.js';
11
+ import { reformatPageDraft } from '../migrate.js';
12
+ import { applyMigration } from '../migrate-apply.js';
13
+ const s = tool.schema;
14
+ const GENRES = ['G1', 'G2', 'G3', 'G4', 'G5'];
15
+ const DELETE_ARGS = {
16
+ path: s.string(),
17
+ locale: s.enum(['en', 'zh']).default('en'),
18
+ confirm: s.string().optional().describe('Must be exactly "yes" to delete'),
19
+ };
20
+ const DeleteArgsSchema = s.object(DELETE_ARGS);
21
+ // --- historian_delete --------------------------------------------------------
22
+ export function makeDeleteTool(deps) {
23
+ return tool({
24
+ description: `Delete a wiki page. Requires the explicit acknowledgement confirm:"yes" — anything else is ` +
25
+ `refused before the wiki is even contacted. ${URL_MANDATE}.`,
26
+ args: DELETE_ARGS,
27
+ execute: async (raw) => {
28
+ const args = DeleteArgsSchema.parse(raw);
29
+ if (args.confirm !== 'yes')
30
+ return confirmRequiredJson('historian_delete', args.confirm);
31
+ try {
32
+ const result = await deletePage(pageDeps(deps), args.path, args.locale, 'yes');
33
+ return okJson({
34
+ mode: 'delete',
35
+ path: args.path,
36
+ locale: args.locale,
37
+ pageId: result.pageId,
38
+ urls: urlPair(result),
39
+ });
40
+ }
41
+ catch (err) {
42
+ return errEnvelope(err);
43
+ }
44
+ },
45
+ });
46
+ }
47
+ const MOVE_ARGS = {
48
+ path: s.string(),
49
+ locale: s.enum(['en', 'zh']).default('en'),
50
+ newPath: s.string().describe('Destination path (first segment must NOT look like a locale code)'),
51
+ newLocale: s.enum(['en', 'zh']).optional(),
52
+ confirm: s.string().optional().describe('Must be exactly "yes" to move'),
53
+ };
54
+ const MoveArgsSchema = s.object(MOVE_ARGS);
55
+ // --- historian_move ----------------------------------------------------------
56
+ export function makeMoveTool(deps) {
57
+ return tool({
58
+ description: `Move a wiki page to a new path (optionally a new locale). Requires the explicit acknowledgement ` +
59
+ `confirm:"yes". Reports the NEW page URLs. ${URL_MANDATE}.`,
60
+ args: MOVE_ARGS,
61
+ execute: async (raw) => {
62
+ const args = MoveArgsSchema.parse(raw);
63
+ if (args.confirm !== 'yes')
64
+ return confirmRequiredJson('historian_move', args.confirm);
65
+ try {
66
+ const destLocale = args.newLocale ?? args.locale;
67
+ const result = await movePage(pageDeps(deps), args.path, args.locale, args.newPath, destLocale, 'yes');
68
+ return okJson({
69
+ mode: 'move',
70
+ from: { path: args.path, locale: args.locale },
71
+ path: result.path,
72
+ locale: result.locale,
73
+ pageId: result.pageId,
74
+ urls: urlPair(result),
75
+ });
76
+ }
77
+ catch (err) {
78
+ return errEnvelope(err);
79
+ }
80
+ },
81
+ });
82
+ }
83
+ const MIGRATE_ARGS = {
84
+ path: s.string(),
85
+ genre: s.enum(GENRES).optional().describe('Explicit genre; absent → engine classification of the page content'),
86
+ apply: s.boolean().default(false).describe('true → apply the migration: pre-image backup first, per-locale upsert, checkpoint'),
87
+ };
88
+ const MigrateArgsSchema = s.object(MIGRATE_ARGS);
89
+ // --- historian_migrate -------------------------------------------------------
90
+ /** MigrateDeps built from the tool deps; the engine reaches the LLM through
91
+ * the injected fetchImpl (mocked in tests, real fetch in production). */
92
+ function migrateDeps(deps) {
93
+ return {
94
+ client: deps.getClient(),
95
+ options: deps.options,
96
+ translate: deps.translate,
97
+ fetchImpl: deps.fetchImpl,
98
+ homeDir: deps.homeDir,
99
+ resultsDir: deps.resultsDir,
100
+ };
101
+ }
102
+ export function makeMigrateTool(deps) {
103
+ return tool({
104
+ description: `Migration of a legacy page into a genre skeleton: reads the page (en, or zh fallback), ` +
105
+ `reformats it via the LLM to the suggested genre (explicit arg wins, else engine classification) ` +
106
+ `and scores the 10-item self-review checklist on the DRAFT. Dry-run writes nothing. ` +
107
+ `apply=true persists: pre-image backup FIRST (results/pilot-backup-<section>-<date>.json), then ` +
108
+ `a per-locale upsert (missing twin auto-created via the translation engine) and a path-level ` +
109
+ `checkpoint (re-apply of unchanged content is a no-op). ${URL_MANDATE}.`,
110
+ args: MIGRATE_ARGS,
111
+ execute: async (raw) => {
112
+ const args = MigrateArgsSchema.parse(raw);
113
+ try {
114
+ const engine = migrateDeps(deps);
115
+ const dry = await reformatPageDraft(engine, { path: args.path, genre: args.genre });
116
+ if (!dry.ok)
117
+ return errEnvelope(dry.error);
118
+ if (!args.apply) {
119
+ return okJson({
120
+ mode: 'dry-run',
121
+ path: args.path,
122
+ locale: dry.sourceLocale,
123
+ suggestedGenre: dry.genre,
124
+ genre: dry.genre,
125
+ confidence: dry.confidence,
126
+ signals: dry.signals,
127
+ content: dry.sourceContent,
128
+ draft: dry.draft,
129
+ alreadyConforms: dry.alreadyConforms,
130
+ missingTwin: dry.missingTwin,
131
+ checklist: selfReviewChecklist(dry.genre),
132
+ checklistResults: dry.checklistResults,
133
+ urls: dry.urls,
134
+ });
135
+ }
136
+ const out = await applyMigration(engine, { path: args.path, genre: dry.genre, draft: dry.draft });
137
+ if (!out.ok)
138
+ return errEnvelope(out.error);
139
+ return okJson({
140
+ mode: 'apply',
141
+ path: args.path,
142
+ genre: dry.genre,
143
+ alreadyConforms: dry.alreadyConforms,
144
+ applied: out.applied,
145
+ backupPath: out.backupPath,
146
+ skipped: out.skipped,
147
+ urls: dry.urls,
148
+ note: `Pre-image backed up at ${out.backupPath}; restore = replay that file (historian_page_update ` +
149
+ `with its fields, historian_delete for locales recorded null) or the wiki.js history view.`,
150
+ });
151
+ }
152
+ catch (err) {
153
+ return errEnvelope(err);
154
+ }
155
+ },
156
+ });
157
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * historian_read + historian_search: read-side tools. All URL composition is
3
+ * reserved-path safe (reportUrls); search results carry per-hit URLs so the
4
+ * agent never needs a second call to find a page.
5
+ */
6
+ import { type ToolDefinition } from '@opencode-ai/plugin';
7
+ import { type ToolDeps } from './shared.js';
8
+ export declare function makeReadTool(deps: ToolDeps): ToolDefinition;
9
+ export declare function makeSearchTool(deps: ToolDeps): ToolDefinition;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * historian_read + historian_search: read-side tools. All URL composition is
3
+ * reserved-path safe (reportUrls); search results carry per-hit URLs so the
4
+ * agent never needs a second call to find a page.
5
+ */
6
+ import { tool } from '@opencode-ai/plugin';
7
+ import { normalizeLocale } from '../wiki/locale.js';
8
+ import { readPage, searchPages } from '../wiki/pages.js';
9
+ import { errEnvelope, okJson, reportUrls, URL_MANDATE } from './shared.js';
10
+ const s = tool.schema;
11
+ const READ_ARGS = {
12
+ path: s.string(),
13
+ locale: s.enum(['en', 'zh']).default('en'),
14
+ };
15
+ const ReadArgsSchema = s.object(READ_ARGS);
16
+ // --- historian_read ----------------------------------------------------------
17
+ export function makeReadTool(deps) {
18
+ return tool({
19
+ description: `Read a wiki page (path + locale → full content; no raw id). A missing page returns ` +
20
+ `found:false with a twin hint instead of failing. ${URL_MANDATE}.`,
21
+ args: READ_ARGS,
22
+ execute: async (raw) => {
23
+ const args = ReadArgsSchema.parse(raw);
24
+ try {
25
+ const page = await readPage(deps.getClient(), args.path, args.locale);
26
+ if (page === null) {
27
+ const pair = reportUrls(deps.options.baseUrl, args.path, args.locale);
28
+ const twinUrl = args.locale === 'en' ? pair.zh : pair.en;
29
+ return okJson({
30
+ found: false,
31
+ path: args.path,
32
+ locale: args.locale,
33
+ twinHint: `No page at '${args.path}' (${args.locale}). The twin may exist at ${twinUrl} — run historian_map or historian_search to verify.`,
34
+ });
35
+ }
36
+ return okJson({
37
+ found: true,
38
+ path: page.path,
39
+ locale: page.locale,
40
+ page: {
41
+ id: page.id,
42
+ title: page.title,
43
+ description: page.description,
44
+ tags: page.tags,
45
+ isPublished: page.isPublished,
46
+ contentType: page.contentType,
47
+ updatedAt: page.updatedAt,
48
+ },
49
+ content: page.content,
50
+ urls: reportUrls(deps.options.baseUrl, page.path, page.locale),
51
+ });
52
+ }
53
+ catch (err) {
54
+ return errEnvelope(err);
55
+ }
56
+ },
57
+ });
58
+ }
59
+ const SEARCH_ARGS = {
60
+ query: s.string(),
61
+ kind: s.enum(['title', 'content']).default('content').describe('Informational intent; the wiki index covers both title and content'),
62
+ };
63
+ const SearchArgsSchema = s.object(SEARCH_ARGS);
64
+ // --- historian_search --------------------------------------------------------
65
+ export function makeSearchTool(deps) {
66
+ return tool({
67
+ description: `Full-text search over the wiki. kind is informational intent only — the live wiki.js ` +
68
+ `search indexes title AND content (the engine signature is search(query, path, locale), no field scope). ` +
69
+ `Results carry their en/zh URLs. ${URL_MANDATE}.`,
70
+ args: SEARCH_ARGS,
71
+ execute: async (raw) => {
72
+ const args = SearchArgsSchema.parse(raw);
73
+ try {
74
+ const resp = await searchPages(deps.getClient(), args.query);
75
+ const results = resp.results.map((r) => {
76
+ try {
77
+ const locale = normalizeLocale(r.locale);
78
+ return {
79
+ id: r.id,
80
+ title: r.title,
81
+ description: r.description,
82
+ path: r.path,
83
+ locale: r.locale,
84
+ url: reportUrls(deps.options.baseUrl, r.path, locale)[locale],
85
+ };
86
+ }
87
+ catch {
88
+ return { id: r.id, title: r.title, description: r.description, path: r.path, locale: r.locale, url: null };
89
+ }
90
+ });
91
+ return okJson({
92
+ query: args.query,
93
+ kind: args.kind,
94
+ totalHits: resp.totalHits,
95
+ suggestions: resp.suggestions,
96
+ results,
97
+ });
98
+ }
99
+ catch (err) {
100
+ return errEnvelope(err);
101
+ }
102
+ },
103
+ });
104
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared plumbing for the historian_* tool surface (todo 11): the tool
3
+ * dependency bag, locale-aware URL helpers (reserved-path safe), the uniform
4
+ * JSON envelope helpers and the error → envelope mapping.
5
+ *
6
+ * The tools layer is a thin adapter: all wiki logic lives in the engine
7
+ * modules (src/wiki/*, src/map.ts, src/templates/*, src/translate.ts,
8
+ * src/migrate*.ts) and is reused verbatim — no business logic is duplicated
9
+ * here.
10
+ */
11
+ import type { ToolResult } from '@opencode-ai/plugin';
12
+ import type { GqlClient } from '../wiki/client.js';
13
+ import type { HistorianOptions } from '../config.js';
14
+ import type { PageDeps } from '../wiki/pages.write.js';
15
+ import type { TranslateFn, Locale } from '../wiki/pages.read.js';
16
+ import { type LocalePair } from '../wiki/locale.js';
17
+ /** Per-tool dependency bag; the client is a thunk so a bad key surface at
18
+ * buildTools time as nothing — only the first execution that touches the
19
+ * wiki resolves it (and then yields a ConfigError envelope, not a crash).
20
+ * The thunk is memoized per buildTools call (no module-level cache). */
21
+ export interface ToolDeps {
22
+ readonly getClient: () => GqlClient;
23
+ readonly options: HistorianOptions;
24
+ readonly translate?: TranslateFn;
25
+ readonly fetchImpl?: typeof fetch;
26
+ readonly resultsDir?: string;
27
+ readonly homeDir: string;
28
+ }
29
+ /** Engine deps for one operation; client resolved lazily at use time. */
30
+ export declare function pageDeps(deps: ToolDeps): PageDeps;
31
+ /** Plan-mandated closing sentence for every tool that reports URLs
32
+ * (verbatim —「结果必须把 en/zh URL 转述给用户」). */
33
+ export declare const URL_MANDATE = "\u7ED3\u679C\u5FC5\u987B\u628A en/zh URL \u8F6C\u8FF0\u7ED9\u7528\u6237";
34
+ /** Locale-aware engine pair → plain en/zh URL map (the pair is primary-locale
35
+ * aware; the agent contract is always {en, zh}). */
36
+ export declare function urlPair(pair: LocalePair): {
37
+ en: string;
38
+ zh: string;
39
+ };
40
+ /** URLs for a server-reported path. Mirrors map.ts's *private* urlsOf: a live
41
+ * page can legally sit on a reserved path (the instance hosts 'home'), so
42
+ * the raw join is the fallback — never a failure — for read-style output.
43
+ * map.ts may not be modified, hence the ~10-line reproduction. */
44
+ export declare function reportUrls(baseUrl: string, path: string, locale: Locale): {
45
+ en: string;
46
+ zh: string;
47
+ };
48
+ export declare function okJson(body: Record<string, unknown>): ToolResult;
49
+ /** Engine error → uniform failure envelope (never a raw stack). */
50
+ export declare function errEnvelope(err: unknown): ToolResult;
51
+ /** Destructive-action gate: refusal BEFORE any fetch when confirm is absent. */
52
+ export declare function confirmRequiredJson(toolName: string, got: unknown): ToolResult;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shared plumbing for the historian_* tool surface (todo 11): the tool
3
+ * dependency bag, locale-aware URL helpers (reserved-path safe), the uniform
4
+ * JSON envelope helpers and the error → envelope mapping.
5
+ *
6
+ * The tools layer is a thin adapter: all wiki logic lives in the engine
7
+ * modules (src/wiki/*, src/map.ts, src/templates/*, src/translate.ts,
8
+ * src/migrate*.ts) and is reused verbatim — no business logic is duplicated
9
+ * here.
10
+ */
11
+ import { assertLocalePair, PathValidationError } from '../wiki/locale.js';
12
+ /** Engine deps for one operation; client resolved lazily at use time. */
13
+ export function pageDeps(deps) {
14
+ return { client: deps.getClient(), options: deps.options, translate: deps.translate };
15
+ }
16
+ /** Plan-mandated closing sentence for every tool that reports URLs
17
+ * (verbatim —「结果必须把 en/zh URL 转述给用户」). */
18
+ export const URL_MANDATE = '结果必须把 en/zh URL 转述给用户';
19
+ /** Locale-aware engine pair → plain en/zh URL map (the pair is primary-locale
20
+ * aware; the agent contract is always {en, zh}). */
21
+ export function urlPair(pair) {
22
+ return pair.locale === 'en' ? { en: pair.url, zh: pair.twinUrl } : { en: pair.twinUrl, zh: pair.url };
23
+ }
24
+ /** URLs for a server-reported path. Mirrors map.ts's *private* urlsOf: a live
25
+ * page can legally sit on a reserved path (the instance hosts 'home'), so
26
+ * the raw join is the fallback — never a failure — for read-style output.
27
+ * map.ts may not be modified, hence the ~10-line reproduction. */
28
+ export function reportUrls(baseUrl, path, locale) {
29
+ const raw = (l) => `${baseUrl.replace(/\/+$/, '')}/${l}/${path}`;
30
+ try {
31
+ return urlPair(assertLocalePair(path, locale, baseUrl));
32
+ }
33
+ catch (err) {
34
+ if (!(err instanceof PathValidationError))
35
+ throw err;
36
+ return { en: raw('en'), zh: raw('zh') };
37
+ }
38
+ }
39
+ // --- JSON envelopes ----------------------------------------------------------
40
+ const dump = (body) => JSON.stringify(body, null, 2);
41
+ export function okJson(body) {
42
+ return dump({ ok: true, ...body });
43
+ }
44
+ /** Engine error → uniform failure envelope (never a raw stack). */
45
+ export function errEnvelope(err) {
46
+ const errorKind = err instanceof Error ? err.name : 'UnknownError';
47
+ const message = err instanceof Error ? err.message : String(err);
48
+ return dump({ ok: false, errorKind, message, actionableHint: hintFor(errorKind) });
49
+ }
50
+ /** Destructive-action gate: refusal BEFORE any fetch when confirm is absent. */
51
+ export function confirmRequiredJson(toolName, got) {
52
+ return dump({
53
+ ok: false,
54
+ error: 'confirm-required',
55
+ errorKind: 'ConfirmRequiredError',
56
+ message: `${toolName} requires confirm:"yes" (got ${JSON.stringify(got)})`,
57
+ actionableHint: 'Re-run the tool with confirm:"yes" to acknowledge the destructive action.',
58
+ });
59
+ }
60
+ /** errorKind (class name) → what the agent should DO. Default covers
61
+ * engine-internal errors whose class names map to no dedicated guidance. */
62
+ function hintFor(errorKind) {
63
+ switch (errorKind) {
64
+ case 'PathValidationError':
65
+ return 'Correct the path argument per the error message and retry.';
66
+ case 'ContentEmptyError':
67
+ return 'Provide non-empty content, or call historian_page_create without content for a local genre template.';
68
+ case 'ConfirmRequiredError':
69
+ return 'Re-run the tool with confirm:"yes" to acknowledge the destructive action.';
70
+ case 'PageNotFoundError':
71
+ return 'The page does not exist at that path/locale — run historian_map or historian_read to verify.';
72
+ case 'PermissionError':
73
+ return 'The wiki.js token lacks the required scope — check Admin ▸ API Access token groups / page rules (move additionally needs manage:pages).';
74
+ case 'WikiError':
75
+ return 'The wiki rejected the operation — inspect message/errorCode and retry.';
76
+ case 'TranslateError':
77
+ return 'Translation failed — the page may still be saved with zh_status pending; retry translation later.';
78
+ case 'ConfigError':
79
+ return 'Fix the plugin configuration (api key file / env) and retry.';
80
+ case 'HttpError':
81
+ return 'The wiki endpoint is unreachable or misconfigured — check baseUrl and network.';
82
+ case 'GraphQLError':
83
+ return 'The wiki answered a GraphQL error — check the path/locale arguments.';
84
+ default:
85
+ return 'Inspect the message and retry.';
86
+ }
87
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * historian_page_update + historian_page_append: engine RMW update and the
3
+ * bilingual append (en append + zh twin handling per the wiki-biling
4
+ * semantics — the twin bootstrap composes engine primitives only:
5
+ * appendSection / createPage / readPage / translate; never reimplemented).
6
+ */
7
+ import { type ToolDefinition } from '@opencode-ai/plugin';
8
+ import { type ToolDeps } from './shared.js';
9
+ export declare function makeUpdateTool(deps: ToolDeps): ToolDefinition;
10
+ export declare function makeAppendTool(deps: ToolDeps): ToolDefinition;