@nostale/cli 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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/assets/docker-compose.server.yml +70 -0
  4. package/dist/commands/config/generation.js +23 -0
  5. package/dist/commands/config/providers.js +138 -0
  6. package/dist/commands/config/sources.js +145 -0
  7. package/dist/commands/config/templates.js +117 -0
  8. package/dist/commands/config/tokens.js +83 -0
  9. package/dist/commands/config.js +43 -0
  10. package/dist/commands/doctor.js +98 -0
  11. package/dist/commands/setup.js +5 -0
  12. package/dist/commands/start.js +23 -0
  13. package/dist/commands/status.js +17 -0
  14. package/dist/commands/stop.js +16 -0
  15. package/dist/index.js +23 -0
  16. package/dist/lib/api-config.js +21 -0
  17. package/dist/lib/env-writer.js +41 -0
  18. package/dist/lib/http.js +117 -0
  19. package/dist/lib/install-root.js +75 -0
  20. package/dist/lib/paths.js +12 -0
  21. package/dist/lib/ports.js +19 -0
  22. package/dist/lib/process-manager.js +151 -0
  23. package/dist/lib/repo-root.js +34 -0
  24. package/dist/lib/wizard.js +267 -0
  25. package/dist/tui/prompts.js +54 -0
  26. package/package.json +69 -0
  27. package/services/mcp-server/index.cjs +48326 -0
  28. package/services/migrations/001_init_rag_v1.sql +128 -0
  29. package/services/migrations/002_doc_lifecycle.sql +96 -0
  30. package/services/migrations/004_mcp_tokens.sql +11 -0
  31. package/services/migrations/005_settings.sql +28 -0
  32. package/services/migrations/006_document_sources.sql +14 -0
  33. package/services/migrations/007_generation_settings.sql +9 -0
  34. package/services/migrations/008_llm_provider_configs_unique.sql +5 -0
  35. package/services/wasm/tree-sitter-c.wasm +0 -0
  36. package/services/wasm/tree-sitter-c_sharp.wasm +0 -0
  37. package/services/wasm/tree-sitter-cpp.wasm +0 -0
  38. package/services/wasm/tree-sitter-dart.wasm +0 -0
  39. package/services/wasm/tree-sitter-go.wasm +0 -0
  40. package/services/wasm/tree-sitter-java.wasm +0 -0
  41. package/services/wasm/tree-sitter-javascript.wasm +0 -0
  42. package/services/wasm/tree-sitter-kotlin.wasm +0 -0
  43. package/services/wasm/tree-sitter-php.wasm +0 -0
  44. package/services/wasm/tree-sitter-python.wasm +0 -0
  45. package/services/wasm/tree-sitter-rust.wasm +0 -0
  46. package/services/wasm/tree-sitter-tsx.wasm +0 -0
  47. package/services/wasm/tree-sitter-typescript.wasm +0 -0
  48. package/services/wasm/tree-sitter.wasm +0 -0
  49. package/services/wiki-api/index.cjs +82370 -0
  50. package/services/wiki-web/.output/nitro.json +15 -0
  51. package/services/wiki-web/.output/public/_nuxt/68caad34b1e80fa12b3a.4Cpu5VkB.woff +0 -0
  52. package/services/wiki-web/.output/public/_nuxt/6b042d50787d4a81d659.D7JGeKLp.svg +1835 -0
  53. package/services/wiki-web/.output/public/_nuxt/9065bfe481020b04b68a.CMveUrS1.woff2 +0 -0
  54. package/services/wiki-web/.output/public/_nuxt/B5j-AdCY.js +1 -0
  55. package/services/wiki-web/.output/public/_nuxt/BAgZ2ZFT.js +1 -0
  56. package/services/wiki-web/.output/public/_nuxt/BcxnhBGC.js +1 -0
  57. package/services/wiki-web/.output/public/_nuxt/BeIDAJKt.js +1 -0
  58. package/services/wiki-web/.output/public/_nuxt/BebqDWPm.js +1 -0
  59. package/services/wiki-web/.output/public/_nuxt/CFx_Wb7e.js +1 -0
  60. package/services/wiki-web/.output/public/_nuxt/CSEkmdhK.js +1 -0
  61. package/services/wiki-web/.output/public/_nuxt/CXYMfke_.js +1 -0
  62. package/services/wiki-web/.output/public/_nuxt/Cap3hpB7.js +1 -0
  63. package/services/wiki-web/.output/public/_nuxt/Ckd7UvSj.js +3 -0
  64. package/services/wiki-web/.output/public/_nuxt/D0DJQL09.js +1 -0
  65. package/services/wiki-web/.output/public/_nuxt/DRDlUXmk.js +66 -0
  66. package/services/wiki-web/.output/public/_nuxt/DW42QLqA.js +1 -0
  67. package/services/wiki-web/.output/public/_nuxt/DdDtdYim.js +1 -0
  68. package/services/wiki-web/.output/public/_nuxt/Dm1y7TW1.js +1 -0
  69. package/services/wiki-web/.output/public/_nuxt/DptiLPkM.js +4 -0
  70. package/services/wiki-web/.output/public/_nuxt/QFl-TMXl.js +1 -0
  71. package/services/wiki-web/.output/public/_nuxt/_id_.JvKV2jiN.css +1 -0
  72. package/services/wiki-web/.output/public/_nuxt/a9c9056a643325579e2a.CCNAgZDa.ttf +0 -0
  73. package/services/wiki-web/.output/public/_nuxt/bojElwT6.js +1 -0
  74. package/services/wiki-web/.output/public/_nuxt/builds/latest.json +1 -0
  75. package/services/wiki-web/.output/public/_nuxt/builds/meta/f68e5546-2919-478b-b4ad-702826e8383c.json +1 -0
  76. package/services/wiki-web/.output/public/_nuxt/d495aba62101f856c80f.DJ4Lg1TH.eot +0 -0
  77. package/services/wiki-web/.output/public/_nuxt/entry.isoIPAS3.css +1 -0
  78. package/services/wiki-web/.output/public/_nuxt/f8QtLg7g.js +1 -0
  79. package/services/wiki-web/.output/public/_nuxt/nhMvlSQm.js +1 -0
  80. package/services/wiki-web/.output/public/_nuxt/pFBugkEL.js +1 -0
  81. package/services/wiki-web/.output/public/_nuxt/plus-jakarta-sans-latin-ext-wght-normal.DmpS2jIq.woff2 +0 -0
  82. package/services/wiki-web/.output/public/_nuxt/plus-jakarta-sans-latin-wght-normal.eXO_dkmS.woff2 +0 -0
  83. package/services/wiki-web/.output/public/_nuxt/plus-jakarta-sans-vietnamese-wght-normal.qRpaaN48.woff2 +0 -0
  84. package/services/wiki-web/.output/public/favicon.svg +6 -0
  85. package/services/wiki-web/.output/public/robots.txt +2 -0
  86. package/services/wiki-web/.output/server/chunks/_/error-500.mjs +19 -0
  87. package/services/wiki-web/.output/server/chunks/_/error-500.mjs.map +1 -0
  88. package/services/wiki-web/.output/server/chunks/build/client.precomputed.mjs +4 -0
  89. package/services/wiki-web/.output/server/chunks/build/client.precomputed.mjs.map +1 -0
  90. package/services/wiki-web/.output/server/chunks/nitro/nitro.mjs +5352 -0
  91. package/services/wiki-web/.output/server/chunks/nitro/nitro.mjs.map +1 -0
  92. package/services/wiki-web/.output/server/chunks/routes/api-proxy/_...path_.mjs +34 -0
  93. package/services/wiki-web/.output/server/chunks/routes/api-proxy/_...path_.mjs.map +1 -0
  94. package/services/wiki-web/.output/server/chunks/routes/renderer.mjs +369 -0
  95. package/services/wiki-web/.output/server/chunks/routes/renderer.mjs.map +1 -0
  96. package/services/wiki-web/.output/server/chunks/virtual/_virtual_spa-template.mjs +4 -0
  97. package/services/wiki-web/.output/server/chunks/virtual/_virtual_spa-template.mjs.map +1 -0
  98. package/services/wiki-web/.output/server/index.mjs +10 -0
  99. package/services/wiki-web/.output/server/index.mjs.map +1 -0
  100. package/services/wiki-web/.output/server/node_modules/@babel/parser/lib/index.js +14599 -0
  101. package/services/wiki-web/.output/server/node_modules/@babel/parser/package.json +50 -0
  102. package/services/wiki-web/.output/server/node_modules/@vue/compiler-core/dist/compiler-core.cjs.prod.js +6759 -0
  103. package/services/wiki-web/.output/server/node_modules/@vue/compiler-core/package.json +58 -0
  104. package/services/wiki-web/.output/server/node_modules/@vue/compiler-dom/dist/compiler-dom.cjs.prod.js +689 -0
  105. package/services/wiki-web/.output/server/node_modules/@vue/compiler-dom/package.json +57 -0
  106. package/services/wiki-web/.output/server/node_modules/@vue/compiler-ssr/dist/compiler-ssr.cjs.js +1413 -0
  107. package/services/wiki-web/.output/server/node_modules/@vue/compiler-ssr/package.json +34 -0
  108. package/services/wiki-web/.output/server/node_modules/@vue/reactivity/dist/reactivity.cjs.prod.js +1899 -0
  109. package/services/wiki-web/.output/server/node_modules/@vue/reactivity/package.json +55 -0
  110. package/services/wiki-web/.output/server/node_modules/@vue/runtime-core/dist/runtime-core.cjs.prod.js +6940 -0
  111. package/services/wiki-web/.output/server/node_modules/@vue/runtime-core/package.json +52 -0
  112. package/services/wiki-web/.output/server/node_modules/@vue/runtime-dom/dist/runtime-dom.cjs.prod.js +1822 -0
  113. package/services/wiki-web/.output/server/node_modules/@vue/runtime-dom/package.json +60 -0
  114. package/services/wiki-web/.output/server/node_modules/@vue/server-renderer/dist/server-renderer.cjs.prod.js +896 -0
  115. package/services/wiki-web/.output/server/node_modules/@vue/server-renderer/package.json +55 -0
  116. package/services/wiki-web/.output/server/node_modules/@vue/shared/dist/shared.cjs.prod.js +604 -0
  117. package/services/wiki-web/.output/server/node_modules/@vue/shared/package.json +47 -0
  118. package/services/wiki-web/.output/server/node_modules/devalue/index.js +4 -0
  119. package/services/wiki-web/.output/server/node_modules/devalue/package.json +40 -0
  120. package/services/wiki-web/.output/server/node_modules/devalue/src/base64.js +60 -0
  121. package/services/wiki-web/.output/server/node_modules/devalue/src/constants.js +12 -0
  122. package/services/wiki-web/.output/server/node_modules/devalue/src/parse.js +283 -0
  123. package/services/wiki-web/.output/server/node_modules/devalue/src/stringify.js +401 -0
  124. package/services/wiki-web/.output/server/node_modules/devalue/src/uneval.js +552 -0
  125. package/services/wiki-web/.output/server/node_modules/devalue/src/utils.js +160 -0
  126. package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/decode-codepoint.js +77 -0
  127. package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/decode.js +568 -0
  128. package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/generated/decode-data-html.js +7 -0
  129. package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/generated/decode-data-xml.js +7 -0
  130. package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/internal/bin-trie-flags.js +21 -0
  131. package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/internal/decode-shared.js +31 -0
  132. package/services/wiki-web/.output/server/node_modules/entities/dist/commonjs/package.json +3 -0
  133. package/services/wiki-web/.output/server/node_modules/entities/package.json +120 -0
  134. package/services/wiki-web/.output/server/node_modules/estree-walker/dist/umd/estree-walker.js +344 -0
  135. package/services/wiki-web/.output/server/node_modules/estree-walker/package.json +37 -0
  136. package/services/wiki-web/.output/server/node_modules/hookable/dist/index.mjs +257 -0
  137. package/services/wiki-web/.output/server/node_modules/hookable/package.json +53 -0
  138. package/services/wiki-web/.output/server/node_modules/source-map-js/lib/array-set.js +121 -0
  139. package/services/wiki-web/.output/server/node_modules/source-map-js/lib/base64-vlq.js +140 -0
  140. package/services/wiki-web/.output/server/node_modules/source-map-js/lib/base64.js +67 -0
  141. package/services/wiki-web/.output/server/node_modules/source-map-js/lib/binary-search.js +111 -0
  142. package/services/wiki-web/.output/server/node_modules/source-map-js/lib/mapping-list.js +79 -0
  143. package/services/wiki-web/.output/server/node_modules/source-map-js/lib/quick-sort.js +132 -0
  144. package/services/wiki-web/.output/server/node_modules/source-map-js/lib/source-map-consumer.js +1188 -0
  145. package/services/wiki-web/.output/server/node_modules/source-map-js/lib/source-map-generator.js +444 -0
  146. package/services/wiki-web/.output/server/node_modules/source-map-js/lib/source-node.js +413 -0
  147. package/services/wiki-web/.output/server/node_modules/source-map-js/lib/util.js +594 -0
  148. package/services/wiki-web/.output/server/node_modules/source-map-js/package.json +71 -0
  149. package/services/wiki-web/.output/server/node_modules/source-map-js/source-map.js +8 -0
  150. package/services/wiki-web/.output/server/node_modules/ufo/dist/index.mjs +645 -0
  151. package/services/wiki-web/.output/server/node_modules/ufo/package.json +48 -0
  152. package/services/wiki-web/.output/server/node_modules/unhead/dist/parser.mjs +508 -0
  153. package/services/wiki-web/.output/server/node_modules/unhead/dist/plugins.mjs +101 -0
  154. package/services/wiki-web/.output/server/node_modules/unhead/dist/server.mjs +180 -0
  155. package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.B3VbJo7Y.mjs +207 -0
  156. package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.BYvz9V1x.mjs +43 -0
  157. package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.CaI5ZD4O.mjs +182 -0
  158. package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.CbpEuj3y.mjs +71 -0
  159. package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.D4vSFytZ.mjs +236 -0
  160. package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.DQc16pHI.mjs +196 -0
  161. package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.TxTMM7cM.mjs +166 -0
  162. package/services/wiki-web/.output/server/node_modules/unhead/dist/shared/unhead.yem5I2v_.mjs +38 -0
  163. package/services/wiki-web/.output/server/node_modules/unhead/dist/utils.mjs +5 -0
  164. package/services/wiki-web/.output/server/node_modules/unhead/package.json +105 -0
  165. package/services/wiki-web/.output/server/node_modules/vue/dist/vue.cjs.js +80 -0
  166. package/services/wiki-web/.output/server/node_modules/vue/dist/vue.cjs.prod.js +66 -0
  167. package/services/wiki-web/.output/server/node_modules/vue/index.js +7 -0
  168. package/services/wiki-web/.output/server/node_modules/vue/index.mjs +1 -0
  169. package/services/wiki-web/.output/server/node_modules/vue/package.json +112 -0
  170. package/services/wiki-web/.output/server/node_modules/vue/server-renderer/index.mjs +1 -0
  171. package/services/wiki-web/.output/server/node_modules/vue-bundle-renderer/dist/runtime.mjs +249 -0
  172. package/services/wiki-web/.output/server/node_modules/vue-bundle-renderer/package.json +57 -0
  173. package/services/wiki-web/.output/server/package.json +26 -0
  174. package/services/wiki-worker/index.cjs +66661 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ferdy Hasan Isma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @nostale/cli
2
+
3
+ **NoStale** extracts knowledge from your codebase, indexes it, and generates PRD/TDD documents that stay in sync with every commit — no stale docs, no manual regeneration. Browse it from a web UI; ask it questions from the coding agent you already use, over MCP. This package is the installer CLI/TUI: it runs the setup wizard, supervises the wiki's services, and diagnoses a broken install.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @nostale/cli
9
+ nostale setup
10
+ ```
11
+
12
+ No prior clone of the [nostale](https://github.com/fetot/nostale) repo is needed — the wizard walks you through personal mode (no Docker, local file DB) or server mode (full `docker-compose` stack), writes your config, runs migrations, and in personal mode provisions your first account automatically.
13
+
14
+ ## Commands
15
+
16
+ | Command | What it does |
17
+ |---|---|
18
+ | `nostale setup` | Interactive wizard — choose personal or server mode, configure the LLM provider, start everything |
19
+ | `nostale start` | Start services (personal mode) or the docker compose stack (server mode) |
20
+ | `nostale stop` | Stop services / the docker compose stack |
21
+ | `nostale status` | Show what's running |
22
+ | `nostale config [subcommand]` | Reconfigure an existing install — providers, templates, MCP tokens, sources, generation toggle |
23
+ | `nostale doctor` | Diagnose a broken installation |
24
+
25
+ ## Personal vs. server mode
26
+
27
+ **Personal mode** runs the wiki as plain local processes (no Docker), storing data in a local libSQL file — the fastest way to try NoStale on your own machine. **Server mode** runs the full stack (including `sqld`) via Docker Compose, for a team sharing one wiki; images are pulled from GHCR, so it's zero-clone too. Either mode can be reconfigured later with `nostale config`, and `nostale doctor` helps diagnose whichever one you're running.
28
+
29
+ ## Learn more
30
+
31
+ - Repository, issues, and full documentation: [github.com/fetot/nostale](https://github.com/fetot/nostale)
32
+ - MCP integration, configuration reference, and FAQ: see the root [README](https://github.com/fetot/nostale#readme) (a dedicated docs site is planned)
33
+
34
+ ## License
35
+
36
+ MIT
@@ -0,0 +1,70 @@
1
+ services:
2
+ sqld:
3
+ # Pinned, not :latest — an unpinned mutable tag makes deployments non-reproducible and
4
+ # silently pulls a compromised/regressed upstream image (SECURITY-REVIEW-2026-07-10.md F-16).
5
+ # Bump deliberately; check https://github.com/tursodatabase/libsql/pkgs/container/libsql-server for newer tags.
6
+ image: ghcr.io/tursodatabase/libsql-server:v0.23.7
7
+ environment:
8
+ SQLD_NODE: primary
9
+ SQLD_AUTH_TOKEN: ${SQLD_AUTH_TOKEN:?SQLD_AUTH_TOKEN must be set — run `nostale setup` to generate one (see SECURITY-REVIEW-2026-07-10.md F-06)}
10
+ volumes:
11
+ - wiki_data:/var/lib/sqld
12
+ # No host port mapping: sibling services reach sqld over the internal Compose network by
13
+ # service name (http://sqld:8080) already — see the root docker-compose.yml for why.
14
+
15
+ wiki-api:
16
+ # M23: zero-clone server mode — pull the image release.yml's build-and-push job already
17
+ # publishes for this exact `@nostale/cli` version, instead of building from a local checkout
18
+ # (there is none, in a bundled install).
19
+ image: ghcr.io/fetot/nostale-wiki-api:${NOSTALE_VERSION}
20
+ environment:
21
+ LIBSQL_URL: http://sqld:8080
22
+ LIBSQL_AUTH_TOKEN: ${SQLD_AUTH_TOKEN:?SQLD_AUTH_TOKEN must be set — run `nostale setup` to generate one (see SECURITY-REVIEW-2026-07-10.md F-06)}
23
+ PORT: "3001"
24
+ NOSTALE_MODE: server
25
+ WIKI_WEB_ORIGIN: http://localhost:3000
26
+ BETTER_AUTH_URL: ${WIKI_API_PUBLIC_URL:-http://localhost:3001}
27
+ BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET must be set — run \`nostale setup\` to generate one (see SECURITY-REVIEW-2026-07-10.md F-01)}
28
+ SETTINGS_ENCRYPTION_KEY: ${SETTINGS_ENCRYPTION_KEY}
29
+ WIKI_WORKER_SHARED_SECRET: ${WIKI_WORKER_SHARED_SECRET}
30
+ ports: ["3001:3001"]
31
+ depends_on: [sqld]
32
+
33
+ wiki-web:
34
+ # M18: optional Web UI, same `web` profile convention as the monorepo's docker-compose.yml.
35
+ profiles: ["web"]
36
+ image: ghcr.io/fetot/nostale-wiki-web:${NOSTALE_VERSION}
37
+ environment:
38
+ WIKI_API_BASE: http://wiki-api:3001
39
+ ports: ["3000:3000"]
40
+ depends_on: [wiki-api]
41
+
42
+ wiki-worker:
43
+ image: ghcr.io/fetot/nostale-wiki-worker:${NOSTALE_VERSION}
44
+ environment:
45
+ LIBSQL_URL: http://sqld:8080
46
+ LIBSQL_AUTH_TOKEN: ${SQLD_AUTH_TOKEN:?SQLD_AUTH_TOKEN must be set — run `nostale setup` to generate one (see SECURITY-REVIEW-2026-07-10.md F-06)}
47
+ PORT: "3002"
48
+ SETTINGS_ENCRYPTION_KEY: ${SETTINGS_ENCRYPTION_KEY}
49
+ WIKI_WORKER_SHARED_SECRET: ${WIKI_WORKER_SHARED_SECRET}
50
+ LLM_PROVIDER: ${LLM_PROVIDER:-anthropic}
51
+ LLM_API_KEY: ${LLM_API_KEY:-}
52
+ LLM_BASE_URL: ${LLM_BASE_URL:-}
53
+ # No host port mapping — only wiki-api calls wiki-worker, over the internal Compose network.
54
+ depends_on: [sqld]
55
+
56
+ mcp-server:
57
+ image: ghcr.io/fetot/nostale-mcp-server:${NOSTALE_VERSION}
58
+ environment:
59
+ LIBSQL_URL: http://sqld:8080
60
+ LIBSQL_AUTH_TOKEN: ${SQLD_AUTH_TOKEN:?SQLD_AUTH_TOKEN must be set — run `nostale setup` to generate one (see SECURITY-REVIEW-2026-07-10.md F-06)}
61
+ PORT: "3100"
62
+ SETTINGS_ENCRYPTION_KEY: ${SETTINGS_ENCRYPTION_KEY}
63
+ LLM_PROVIDER: ${LLM_PROVIDER:-anthropic}
64
+ LLM_API_KEY: ${LLM_API_KEY:-}
65
+ LLM_BASE_URL: ${LLM_BASE_URL:-}
66
+ ports: ["3100:3100"]
67
+ depends_on: [sqld]
68
+
69
+ volumes:
70
+ wiki_data:
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `nostale config generation` — the M17 auto-generate-docs toggle via wiki-api's
3
+ * `GET/PUT /projects/:project/generation-settings`. Use `apiGet`/`apiPut` from
4
+ * `../../lib/http.js`.
5
+ */
6
+ import { promptConfirm, promptText, printInfo, printStep } from '../../tui/prompts.js';
7
+ import { resolveApiConfig } from '../../lib/api-config.js';
8
+ import { apiGet, apiPut } from '../../lib/http.js';
9
+ export async function run(installDir) {
10
+ const { wikiApiBase, origin } = resolveApiConfig(installDir);
11
+ const project = await promptText('Project key');
12
+ const path = `/projects/${encodeURIComponent(project)}/generation-settings`;
13
+ const current = await apiGet(wikiApiBase, origin, path);
14
+ printInfo(`Auto-generate PRD/TDD documents: ${current.autoGenerateDocs ? 'on' : 'off'}`);
15
+ const next = await promptConfirm('Auto-generate PRD/TDD documents with an LLM after each sync?', current.autoGenerateDocs);
16
+ if (next === current.autoGenerateDocs) {
17
+ printInfo('No change.');
18
+ return;
19
+ }
20
+ await apiPut(wikiApiBase, origin, path, { autoGenerateDocs: next });
21
+ const confirmed = await apiGet(wikiApiBase, origin, path);
22
+ printStep(`Auto-generate PRD/TDD documents is now: ${confirmed.autoGenerateDocs ? 'on' : 'off'}`);
23
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * `nostale config providers` — manage `llm_provider_configs` via wiki-api's
3
+ * `GET/POST /settings/providers`. Use `apiGet`/`apiPost` from `../../lib/http.js`.
4
+ *
5
+ * `GET /settings/providers` never returns the API key at all. `LlmProviderConfig`
6
+ * (packages/rag-core/src/types.ts) has no `apiKey`/`apiKeyEncrypted` field, and
7
+ * `mapLlmProviderConfigRow` (store-libsql.ts) only copies id/provider/label/baseUrl/
8
+ * defaultModel/isActive/createdByUserId/createdAt/updatedAt off the row — so there is
9
+ * nothing to redact defensively here, the key structurally cannot round-trip.
10
+ *
11
+ * `upsertLlmProviderConfig` upserts by the (provider, label) natural key (008_llm_provider_configs_unique.sql)
12
+ * — POSTing the same provider+label again updates that row in place rather than creating a
13
+ * new one. Edit/Deactivate below rely on this: they re-POST with the target's existing
14
+ * provider+label, so the mutation lands on the same row.
15
+ */
16
+ import { resolveApiConfig } from '../../lib/api-config.js';
17
+ import { apiGet, apiPost } from '../../lib/http.js';
18
+ import { promptSelect, promptText, promptPassword, promptConfirm, printStep, printInfo } from '../../tui/prompts.js';
19
+ function formatProvider(p) {
20
+ const status = p.isActive ? 'active' : 'inactive';
21
+ return ` ${p.id} ${p.provider}/${p.label} model=${p.defaultModel} [${status}]`;
22
+ }
23
+ async function fetchProviders(wikiApiBase, origin) {
24
+ return apiGet(wikiApiBase, origin, '/settings/providers');
25
+ }
26
+ async function listProviders(wikiApiBase, origin) {
27
+ const providers = await fetchProviders(wikiApiBase, origin);
28
+ printStep('LLM providers');
29
+ if (providers.length === 0) {
30
+ printInfo('(none yet)');
31
+ }
32
+ else {
33
+ for (const p of providers)
34
+ printInfo(formatProvider(p));
35
+ }
36
+ return providers;
37
+ }
38
+ /**
39
+ * Shared by Add and Edit — Add prompts every field; Edit fixes `provider`/`label` (the
40
+ * natural key the backend upserts on, per 008_llm_provider_configs_unique.sql) to the
41
+ * existing row's values so the mutation updates it in place instead of creating a new row
42
+ * under a different key, and only prompts for the genuinely mutable fields.
43
+ */
44
+ async function askProviderFields(existing) {
45
+ let provider;
46
+ let label;
47
+ if (existing) {
48
+ provider = existing.provider;
49
+ label = existing.label;
50
+ }
51
+ else {
52
+ provider = await promptSelect('Provider', [
53
+ { label: 'Anthropic', value: 'anthropic' },
54
+ { label: 'OpenAI-compatible (self-hosted / proxy)', value: 'openai_compatible' },
55
+ ]);
56
+ label = await promptText('Label');
57
+ }
58
+ const apiKey = await promptPassword(existing ? 'API key (required again — the existing key is never returned, so it cannot be pre-filled)' : 'API key');
59
+ const baseUrl = provider === 'openai_compatible'
60
+ ? await promptText('Base URL', { defaultValue: existing?.baseUrl ?? 'http://localhost:11434/v1' })
61
+ : undefined;
62
+ const defaultModel = await promptText('Default model', {
63
+ defaultValue: existing?.defaultModel ?? (provider === 'anthropic' ? 'claude-sonnet-4-5' : 'gpt-4o'),
64
+ });
65
+ return { provider, label, apiKey, baseUrl, defaultModel };
66
+ }
67
+ async function addProvider(wikiApiBase, origin) {
68
+ printStep('Add a provider');
69
+ const fields = await askProviderFields();
70
+ const { id } = await apiPost(wikiApiBase, origin, '/settings/providers', { ...fields, isActive: true });
71
+ const after = await listProviders(wikiApiBase, origin);
72
+ const created = after.find((p) => p.id === id);
73
+ printInfo(created ? `Confirmed: provider ${id} ("${created.label}") is active.` : `Warning: provider ${id} not found in list after creation.`);
74
+ }
75
+ async function editProvider(wikiApiBase, origin) {
76
+ const providers = await fetchProviders(wikiApiBase, origin);
77
+ if (providers.length === 0) {
78
+ printInfo('No providers yet — add one first.');
79
+ return;
80
+ }
81
+ const id = await promptSelect('Which provider do you want to edit?', providers.map((p) => ({ label: `${p.label} (${p.provider}) — ${p.id}`, value: p.id })));
82
+ const target = providers.find((p) => p.id === id);
83
+ printStep(`Edit "${target.label}"`);
84
+ const fields = await askProviderFields(target);
85
+ await apiPost(wikiApiBase, origin, '/settings/providers', { ...fields, isActive: target.isActive });
86
+ const after = await listProviders(wikiApiBase, origin);
87
+ const updated = after.find((p) => p.id === target.id);
88
+ printInfo(updated ? `Confirmed: ${target.id} ("${updated.label}") now uses model ${updated.defaultModel}.` : `Warning: provider ${target.id} not found after edit.`);
89
+ }
90
+ async function deactivateProvider(wikiApiBase, origin) {
91
+ const providers = await fetchProviders(wikiApiBase, origin);
92
+ const active = providers.filter((p) => p.isActive);
93
+ if (active.length === 0) {
94
+ printInfo('No active providers to deactivate.');
95
+ return;
96
+ }
97
+ const id = await promptSelect('Which provider do you want to deactivate?', active.map((p) => ({ label: `${p.label} (${p.provider}) — ${p.id}`, value: p.id })));
98
+ const target = active.find((p) => p.id === id);
99
+ const confirmed = await promptConfirm(`Deactivate "${target.label}"?`, false);
100
+ if (!confirmed) {
101
+ printInfo('Cancelled.');
102
+ return;
103
+ }
104
+ const apiKey = await promptPassword(`Re-enter the API key for "${target.label}" (required by this endpoint, never stored/returned)`);
105
+ await apiPost(wikiApiBase, origin, '/settings/providers', {
106
+ provider: target.provider,
107
+ label: target.label,
108
+ apiKey,
109
+ baseUrl: target.baseUrl ?? undefined,
110
+ defaultModel: target.defaultModel,
111
+ isActive: false,
112
+ });
113
+ const after = await listProviders(wikiApiBase, origin);
114
+ const updated = after.find((p) => p.id === target.id);
115
+ printInfo(updated && !updated.isActive ? `Confirmed: ${target.id} ("${updated.label}") is now inactive.` : `Warning: provider ${target.id} still active after deactivation.`);
116
+ }
117
+ export async function run(installDir) {
118
+ const { wikiApiBase, origin } = resolveApiConfig(installDir);
119
+ for (;;) {
120
+ const action = await promptSelect('Providers — what would you like to do?', [
121
+ { label: 'List providers', value: 'list' },
122
+ { label: 'Add a provider', value: 'add' },
123
+ { label: 'Edit a provider', value: 'edit' },
124
+ { label: 'Deactivate a provider', value: 'deactivate' },
125
+ { label: 'Done', value: 'done' },
126
+ ]);
127
+ if (action === 'done')
128
+ return;
129
+ if (action === 'list')
130
+ await listProviders(wikiApiBase, origin);
131
+ else if (action === 'add')
132
+ await addProvider(wikiApiBase, origin);
133
+ else if (action === 'edit')
134
+ await editProvider(wikiApiBase, origin);
135
+ else if (action === 'deactivate')
136
+ await deactivateProvider(wikiApiBase, origin);
137
+ }
138
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * `nostale config sources` — manage repo + document-folder/upload sources (M15/M16) via
3
+ * wiki-api's `GET/PUT /projects/:project/repo-state` and `GET/POST /projects/:project/sources`,
4
+ * plus triggering `POST /projects/:project/sync`. Use `apiGet`/`apiPost`/`apiPut` from
5
+ * `../../lib/http.js`.
6
+ *
7
+ * Field-per-sourceType logic mirrors `apps/wiki-web/app/pages/settings/sources/index.vue`:
8
+ * `local_path` only needs `localPath`; `github_app`/`deploy_key` need `gitUrl` plus their own
9
+ * one extra field (`githubAppInstallationId` / `deployKeySecretRef`). `webhookSecret` is never
10
+ * fetched/prompted — wiki-api omits it from GET responses on purpose.
11
+ */
12
+ import { apiGet, apiPost, apiPut } from '../../lib/http.js';
13
+ import { resolveApiConfig } from '../../lib/api-config.js';
14
+ import { printInfo, printStep, promptSelect, promptText } from '../../tui/prompts.js';
15
+ async function fetchRepoState(wikiApiBase, origin, project) {
16
+ try {
17
+ return await apiGet(wikiApiBase, origin, `/projects/${project}/repo-state`);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ async function fetchDocumentSources(wikiApiBase, origin, project) {
24
+ try {
25
+ return await apiGet(wikiApiBase, origin, `/projects/${project}/sources`);
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ }
31
+ function printRepoState(state) {
32
+ if (!state) {
33
+ printInfo('Code repo: not configured yet.');
34
+ return;
35
+ }
36
+ printInfo(`Code repo: ${state.sourceType}`);
37
+ if (state.gitUrl)
38
+ printInfo(` Git URL: ${state.gitUrl}`);
39
+ if (state.localPath)
40
+ printInfo(` Local path: ${state.localPath}`);
41
+ if (state.githubAppInstallationId)
42
+ printInfo(` GitHub App installation ID: ${state.githubAppInstallationId}`);
43
+ if (state.deployKeySecretRef)
44
+ printInfo(` Deploy key secret ref: ${state.deployKeySecretRef}`);
45
+ printInfo(` Default branch: ${state.defaultBranch}`);
46
+ printInfo(` Latest known commit: ${state.latestKnownCommit ?? 'none'}`);
47
+ printInfo(` Last synced commit: ${state.lastSyncedCommit ?? 'none'}`);
48
+ printInfo(` Last synced at: ${state.lastSyncedAt ?? 'never'}`);
49
+ }
50
+ function printDocumentSources(sources) {
51
+ if (sources.length === 0) {
52
+ printInfo('Document sources: none registered.');
53
+ return;
54
+ }
55
+ printInfo('Document sources:');
56
+ for (const source of sources) {
57
+ const label = source.sourceType === 'document_folder' ? source.folderPath : 'uploaded files';
58
+ printInfo(` [${source.sourceType}] ${label} — last synced: ${source.lastSyncedAt ?? 'never'}`);
59
+ }
60
+ }
61
+ async function viewState(wikiApiBase, origin, project) {
62
+ printStep(`Current state for "${project}"`);
63
+ const [repoState, documentSources] = await Promise.all([
64
+ fetchRepoState(wikiApiBase, origin, project),
65
+ fetchDocumentSources(wikiApiBase, origin, project),
66
+ ]);
67
+ printRepoState(repoState);
68
+ printDocumentSources(documentSources);
69
+ }
70
+ /** Prompts only the fields relevant to `sourceType`, mirroring the Web UI's conditional form. */
71
+ async function promptRepoFields(sourceType) {
72
+ if (sourceType === 'local_path') {
73
+ const localPath = await promptText('Local path (mounted into wiki-worker)');
74
+ return { gitUrl: null, githubAppInstallationId: null, deployKeySecretRef: null, localPath };
75
+ }
76
+ const gitUrl = await promptText('Git URL');
77
+ if (sourceType === 'github_app') {
78
+ const githubAppInstallationId = await promptText('GitHub App installation ID');
79
+ return { gitUrl, githubAppInstallationId, deployKeySecretRef: null, localPath: null };
80
+ }
81
+ const deployKeySecretRef = await promptText('Deploy key secret reference');
82
+ return { gitUrl, githubAppInstallationId: null, deployKeySecretRef, localPath: null };
83
+ }
84
+ async function registerRepo(wikiApiBase, origin, project) {
85
+ printStep('Register/update code repo');
86
+ const sourceType = await promptSelect('Source type', [
87
+ { label: 'local_path (worker reads a mounted directory directly)', value: 'local_path' },
88
+ { label: 'deploy_key (SSH deploy key, read-only)', value: 'deploy_key' },
89
+ { label: 'github_app (GitHub App installation)', value: 'github_app' },
90
+ ]);
91
+ const fields = await promptRepoFields(sourceType);
92
+ const defaultBranch = await promptText('Default branch', { defaultValue: 'main' });
93
+ const result = await apiPut(wikiApiBase, origin, `/projects/${project}/repo-state`, {
94
+ project,
95
+ sourceType,
96
+ ...fields,
97
+ defaultBranch,
98
+ });
99
+ printInfo('Saved.');
100
+ printRepoState(result);
101
+ }
102
+ async function registerFolder(wikiApiBase, origin, project) {
103
+ printStep('Register a document folder');
104
+ const folderPath = await promptText('Folder path (mounted into wiki-worker)');
105
+ await apiPost(wikiApiBase, origin, `/projects/${project}/sources`, { sourceType: 'document_folder', folderPath });
106
+ printInfo('Registered.');
107
+ printDocumentSources(await fetchDocumentSources(wikiApiBase, origin, project));
108
+ }
109
+ async function syncNow(wikiApiBase, origin, project) {
110
+ printStep('Syncing…');
111
+ const result = await apiPost(wikiApiBase, origin, `/projects/${project}/sync`, {});
112
+ printInfo(result.codeSync ? 'Code repo synced.' : 'Code repo: no sync performed (not configured, or nothing new).');
113
+ if (result.documentSyncs.length === 0) {
114
+ printInfo('Document sources: none synced.');
115
+ }
116
+ else {
117
+ for (const sync of result.documentSyncs) {
118
+ const label = sync.sourceType === 'document_folder' ? sync.folderPath : 'uploaded files';
119
+ printInfo(` [${sync.sourceType}] ${label}: ${sync.result.documentsIngested} document(s), ${sync.result.chunksIngested} chunk(s) ingested.`);
120
+ }
121
+ }
122
+ }
123
+ export async function run(installDir) {
124
+ const { wikiApiBase, origin } = resolveApiConfig(installDir);
125
+ const project = await promptText('Project key');
126
+ for (;;) {
127
+ const choice = await promptSelect(`Sources for "${project}"`, [
128
+ { label: 'View current state', value: 'view' },
129
+ { label: 'Register/update code repo', value: 'repo' },
130
+ { label: 'Register a document folder', value: 'folder' },
131
+ { label: 'Sync now', value: 'sync' },
132
+ { label: 'Done', value: 'done' },
133
+ ]);
134
+ if (choice === 'done')
135
+ return;
136
+ if (choice === 'view')
137
+ await viewState(wikiApiBase, origin, project);
138
+ if (choice === 'repo')
139
+ await registerRepo(wikiApiBase, origin, project);
140
+ if (choice === 'folder')
141
+ await registerFolder(wikiApiBase, origin, project);
142
+ if (choice === 'sync')
143
+ await syncNow(wikiApiBase, origin, project);
144
+ }
145
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * `nostale config templates` — CRUD `document_templates` via wiki-api's
3
+ * `GET/POST /settings/templates`. `upsertDocumentTemplate` (packages/rag-core/src/store-libsql.ts)
4
+ * keys its upsert on `(project, docKind, name)`, so "edit" is just re-posting the same
5
+ * project+docKind+name with changed fields — no separate PATCH endpoint needed.
6
+ */
7
+ import { resolveApiConfig } from '../../lib/api-config.js';
8
+ import { apiGet, apiPost } from '../../lib/http.js';
9
+ import { promptText, promptSelect, promptConfirm, printStep, printInfo } from '../../tui/prompts.js';
10
+ function describe(t) {
11
+ const scope = t.project ?? 'global';
12
+ const status = t.isActive ? '' : ' (inactive)';
13
+ return `${scope} / ${t.docKind} / ${t.name}${status} — ${t.sections.length} section(s)`;
14
+ }
15
+ async function fetchAll(ctx) {
16
+ return apiGet(ctx.wikiApiBase, ctx.origin, '/settings/templates');
17
+ }
18
+ /** Prompts for an optional project filter, fetches, prints. Returns the fetched list. */
19
+ async function listTemplates(ctx) {
20
+ const project = await promptText('Project to scope to (empty = all)');
21
+ const path = project ? `/settings/templates?project=${encodeURIComponent(project)}` : '/settings/templates';
22
+ const templates = await apiGet(ctx.wikiApiBase, ctx.origin, path);
23
+ printStep(`${templates.length} template(s)`);
24
+ for (const t of templates)
25
+ printInfo(describe(t));
26
+ return templates;
27
+ }
28
+ /** Lists, then lets the user pick one by id. Returns null if the list is empty. */
29
+ async function pickTemplate(ctx) {
30
+ const templates = await fetchAll(ctx);
31
+ if (templates.length === 0) {
32
+ printInfo('No templates yet.');
33
+ return null;
34
+ }
35
+ const id = await promptSelect('Which template?', templates.map((t) => ({ label: describe(t), value: t.id })));
36
+ return templates.find((t) => t.id === id) ?? null;
37
+ }
38
+ async function buildSections(existing = []) {
39
+ if (existing.length > 0) {
40
+ printInfo(`Existing sections: ${existing.map((s) => s.key).join(', ')}`);
41
+ if (await promptConfirm('Keep existing sections as-is?', true))
42
+ return existing;
43
+ }
44
+ const sections = [];
45
+ while (await promptConfirm(sections.length === 0 ? 'Add a section?' : 'Add another section?', sections.length === 0)) {
46
+ const key = await promptText(' Section key');
47
+ const title = await promptText(' Section title');
48
+ const guidance = await promptText(' Section guidance');
49
+ const required = await promptConfirm(' Required?', true);
50
+ sections.push({ key, title, guidance, required });
51
+ }
52
+ return sections;
53
+ }
54
+ /** Shared create+edit flow: prompts every field, pre-filled from `existing` when editing. */
55
+ async function upsertTemplate(ctx, existing) {
56
+ const project = await promptText('Project (empty for global)', { defaultValue: existing?.project ?? '' });
57
+ const docKindChoices = existing?.docKind === 'tdd' ? ['tdd', 'prd'] : ['prd', 'tdd'];
58
+ const docKind = await promptSelect('Doc kind', docKindChoices.map((k) => ({ label: k.toUpperCase(), value: k })));
59
+ const name = await promptText('Name', { defaultValue: existing?.name });
60
+ const sections = await buildSections(existing?.sections ?? []);
61
+ const systemPromptOverride = await promptText('System prompt override (empty for none)', {
62
+ defaultValue: existing?.systemPromptOverride ?? '',
63
+ });
64
+ const isActive = await promptConfirm('Active?', existing?.isActive ?? true);
65
+ await apiPost(ctx.wikiApiBase, ctx.origin, '/settings/templates', {
66
+ project: project || null,
67
+ docKind,
68
+ name,
69
+ sections,
70
+ systemPromptOverride: systemPromptOverride || null,
71
+ isActive,
72
+ });
73
+ const saved = (await fetchAll(ctx)).find((t) => t.project === (project || null) && t.docKind === docKind && t.name === name);
74
+ printStep(saved ? `Saved: ${describe(saved)}` : 'Saved, but could not confirm by re-fetch.');
75
+ }
76
+ async function deactivateTemplate(ctx) {
77
+ const template = await pickTemplate(ctx);
78
+ if (!template)
79
+ return;
80
+ if (!(await promptConfirm(`Deactivate "${template.name}"?`, false)))
81
+ return;
82
+ await apiPost(ctx.wikiApiBase, ctx.origin, '/settings/templates', {
83
+ project: template.project,
84
+ docKind: template.docKind,
85
+ name: template.name,
86
+ sections: template.sections,
87
+ systemPromptOverride: template.systemPromptOverride,
88
+ isActive: false,
89
+ });
90
+ const saved = (await fetchAll(ctx)).find((t) => t.id === template.id);
91
+ printStep(saved && !saved.isActive ? `Deactivated: ${describe(saved)}` : 'Deactivate did not take effect — check wiki-api logs.');
92
+ }
93
+ export async function run(installDir) {
94
+ const ctx = resolveApiConfig(installDir);
95
+ for (;;) {
96
+ const action = await promptSelect('Templates', [
97
+ { label: 'List templates', value: 'list' },
98
+ { label: 'Create a template', value: 'create' },
99
+ { label: 'Edit a template', value: 'edit' },
100
+ { label: 'Deactivate a template', value: 'deactivate' },
101
+ { label: 'Back', value: 'back' },
102
+ ]);
103
+ if (action === 'back')
104
+ return;
105
+ if (action === 'list')
106
+ await listTemplates(ctx);
107
+ if (action === 'create')
108
+ await upsertTemplate(ctx);
109
+ if (action === 'deactivate')
110
+ await deactivateTemplate(ctx);
111
+ if (action === 'edit') {
112
+ const template = await pickTemplate(ctx);
113
+ if (template)
114
+ await upsertTemplate(ctx, template);
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `nostale config tokens` — generate/list/revoke `mcp_access_tokens` via wiki-api's
3
+ * `GET/POST /settings/mcp-tokens` and `POST /settings/mcp-tokens/:id/revoke`. Use
4
+ * `apiGet`/`apiPost` from `../../lib/http.js`. Note: a freshly created token is returned once in
5
+ * the POST response body only — print it to the user immediately, it is never retrievable again.
6
+ */
7
+ import { resolveApiConfig } from '../../lib/api-config.js';
8
+ import { apiGet, apiPost } from '../../lib/http.js';
9
+ import { promptSelect, promptText, promptConfirm, printStep, printInfo } from '../../tui/prompts.js';
10
+ function formatToken(t) {
11
+ const status = t.revokedAt ? `revoked ${t.revokedAt}` : 'active';
12
+ const lastUsed = t.lastUsedAt ?? 'never';
13
+ return ` ${t.id} "${t.label ?? '(no label)'}" [${status}] last used: ${lastUsed}`;
14
+ }
15
+ async function listTokens(wikiApiBase, origin) {
16
+ const tokens = await apiGet(wikiApiBase, origin, '/settings/mcp-tokens');
17
+ printStep('MCP access tokens');
18
+ if (tokens.length === 0) {
19
+ printInfo('(none yet)');
20
+ }
21
+ else {
22
+ for (const t of tokens)
23
+ printInfo(formatToken(t));
24
+ }
25
+ return tokens;
26
+ }
27
+ async function generateToken(wikiApiBase, origin) {
28
+ printStep('Generate a new MCP access token');
29
+ const label = await promptText('Label (optional)');
30
+ const { id, token } = await apiPost(wikiApiBase, origin, '/settings/mcp-tokens', {
31
+ label: label || undefined,
32
+ });
33
+ const bar = '='.repeat(64);
34
+ console.log(`\n${bar}`);
35
+ console.log(' NEW MCP ACCESS TOKEN — copy it now, it will NOT be shown again:');
36
+ console.log('');
37
+ console.log(` ${token}`);
38
+ console.log('');
39
+ console.log(` id: ${id}`);
40
+ console.log(bar + '\n');
41
+ // Confirm the mutation actually landed server-side (can't confirm the raw token — it's
42
+ // one-time-only by design — but we can confirm the new token id shows up as active).
43
+ const tokens = await listTokens(wikiApiBase, origin);
44
+ const created = tokens.find((t) => t.id === id);
45
+ printInfo(created ? `Confirmed: token ${id} is active.` : `Warning: token ${id} not found in list after creation.`);
46
+ }
47
+ async function revokeToken(wikiApiBase, origin) {
48
+ const tokens = await apiGet(wikiApiBase, origin, '/settings/mcp-tokens');
49
+ const active = tokens.filter((t) => !t.revokedAt);
50
+ if (active.length === 0) {
51
+ printInfo('No active tokens to revoke.');
52
+ return;
53
+ }
54
+ const id = await promptSelect('Which token do you want to revoke?', active.map((t) => ({ label: `${t.label ?? '(no label)'} — ${t.id}`, value: t.id })));
55
+ const confirmed = await promptConfirm(`Revoke token ${id}? This cannot be undone.`, false);
56
+ if (!confirmed) {
57
+ printInfo('Cancelled.');
58
+ return;
59
+ }
60
+ await apiPost(wikiApiBase, origin, `/settings/mcp-tokens/${id}/revoke`);
61
+ const after = await apiGet(wikiApiBase, origin, '/settings/mcp-tokens');
62
+ const revoked = after.find((t) => t.id === id);
63
+ printInfo(revoked?.revokedAt ? `Confirmed: token ${id} revoked at ${revoked.revokedAt}.` : `Warning: token ${id} does not show as revoked.`);
64
+ }
65
+ export async function run(installDir) {
66
+ const { wikiApiBase, origin } = resolveApiConfig(installDir);
67
+ for (;;) {
68
+ const action = await promptSelect('MCP Tokens — what would you like to do?', [
69
+ { label: 'List tokens', value: 'list' },
70
+ { label: 'Generate a new token', value: 'generate' },
71
+ { label: 'Revoke a token', value: 'revoke' },
72
+ { label: 'Done', value: 'done' },
73
+ ]);
74
+ if (action === 'done')
75
+ return;
76
+ if (action === 'list')
77
+ await listTokens(wikiApiBase, origin);
78
+ else if (action === 'generate')
79
+ await generateToken(wikiApiBase, origin);
80
+ else if (action === 'revoke')
81
+ await revokeToken(wikiApiBase, origin);
82
+ }
83
+ }