@rahmantech/knl-view 0.1.0 → 0.3.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 (98) hide show
  1. package/README.md +133 -32
  2. package/bin/knl-view.js +591 -58
  3. package/dist/assets/{abnfDiagram-WEYOIA3C-B1gaBaSI.js → abnfDiagram-WEYOIA3C-hoSbsMOz.js} +1 -1
  4. package/dist/assets/{arc-NZ9uRaCl.js → arc-g87Np6E5.js} +1 -1
  5. package/dist/assets/{architecture-WOLXFQ4H-8MalLwTd.js → architecture-WOLXFQ4H-DOIq-0IA.js} +1 -1
  6. package/dist/assets/{architectureDiagram-ZHVYKT53-eZ8i5cl4.js → architectureDiagram-ZHVYKT53-Dxwe8n7m.js} +1 -1
  7. package/dist/assets/{blockDiagram-RJAE7QA3-BavOE8BM.js → blockDiagram-RJAE7QA3-BnGng59Z.js} +1 -1
  8. package/dist/assets/{c4Diagram-XDPPPE54-DqMaTAzx.js → c4Diagram-XDPPPE54-C5LjqIIo.js} +1 -1
  9. package/dist/assets/channel-BVWkRFBw.js +1 -0
  10. package/dist/assets/{chunk-2E7WHVMD-B1Hj2BjJ.js → chunk-2E7WHVMD-BJ1dyGLq.js} +1 -1
  11. package/dist/assets/{chunk-4EMNKR2T-HDscrCiq.js → chunk-4EMNKR2T-DXF02Sgw.js} +1 -1
  12. package/dist/assets/{chunk-55XCU76K-QR_ARj8I.js → chunk-55XCU76K-CM1akAbu.js} +1 -1
  13. package/dist/assets/{chunk-7S6XSTFO-B0p3o547.js → chunk-7S6XSTFO-BOUjqldL.js} +1 -1
  14. package/dist/assets/{chunk-AQDTSKZB-CCxwNyyA.js → chunk-AQDTSKZB-C4isdNLA.js} +1 -1
  15. package/dist/assets/{chunk-DSNLSHJZ-BM5PBFmz.js → chunk-DSNLSHJZ-CbOTkSVM.js} +1 -1
  16. package/dist/assets/{chunk-HAGFOTED-UCDVFuN_.js → chunk-HAGFOTED-CRfnketR.js} +1 -1
  17. package/dist/assets/{chunk-HNE4Q36P-C_1sakp2.js → chunk-HNE4Q36P-rxlpGUU8.js} +1 -1
  18. package/dist/assets/{chunk-NQOKRVIF-b2D1Z4oK.js → chunk-NQOKRVIF-BOYCgTmV.js} +1 -1
  19. package/dist/assets/{chunk-UHW7PFBP-CJs0VwBY.js → chunk-UHW7PFBP-Cnogp1Fb.js} +1 -1
  20. package/dist/assets/{chunk-VQ5W62QI-DQxjgE4_.js → chunk-VQ5W62QI-Cu1WY5Uo.js} +1 -1
  21. package/dist/assets/{chunk-WQQL4CDK-DcjPeAfQ.js → chunk-WQQL4CDK-BJc3FPE2.js} +1 -1
  22. package/dist/assets/{chunk-XXDRQBXY-glA0kB5s.js → chunk-XXDRQBXY-BlPj4H7s.js} +1 -1
  23. package/dist/assets/{chunk-YZO67HFG-DJqACL5V.js → chunk-YZO67HFG-V85H31x4.js} +1 -1
  24. package/dist/assets/{chunk-ZAUX2LTX-CWfaYtwb.js → chunk-ZAUX2LTX-C3XKXW09.js} +1 -1
  25. package/dist/assets/{chunk-ZCHMQSOR-BGX73Hho.js → chunk-ZCHMQSOR-B0mmpEgW.js} +1 -1
  26. package/dist/assets/{classDiagram-v2-4MYWR3UA-BpYXPz-e.js → classDiagram-v2-4MYWR3UA-oMd7YaHP.js} +1 -1
  27. package/dist/assets/{cose-bilkent-JH36ORCC-e1quKm0Q.js → cose-bilkent-JH36ORCC-J0kSVsDf.js} +1 -1
  28. package/dist/assets/{cynefin-EF2NZ3EQ-hb3oUmFO.js → cynefin-EF2NZ3EQ-DK2TNhel.js} +1 -1
  29. package/dist/assets/{cynefinDiagram-WF24UDRP-CnMQvLqi.js → cynefinDiagram-WF24UDRP-DDvKgieA.js} +1 -1
  30. package/dist/assets/{dagre-NK4GIZVX-BH_Y8y3F.js → dagre-NK4GIZVX-Ca2Fe-i-.js} +1 -1
  31. package/dist/assets/{diagram-CBK3MWTC-Dh79VetZ.js → diagram-CBK3MWTC-L6dmeQ2V.js} +1 -1
  32. package/dist/assets/{diagram-I5XIIV2D-D3u-o1RV.js → diagram-I5XIIV2D-DhG8R-v5.js} +1 -1
  33. package/dist/assets/{diagram-ODIL2UCW-Bfuo-OKS.js → diagram-ODIL2UCW-BpYYbYBT.js} +1 -1
  34. package/dist/assets/{diagram-PFBEJUXE-CwHasIbs.js → diagram-PFBEJUXE-DKQpHSsC.js} +1 -1
  35. package/dist/assets/{diagram-RJA3I4VV-D8ttmuE8.js → diagram-RJA3I4VV-DP7Dsnya.js} +1 -1
  36. package/dist/assets/{diagram-TYUNFSNR-CzC-8RZ0.js → diagram-TYUNFSNR-CfSFewUS.js} +1 -1
  37. package/dist/assets/{dist-CUJYGnnI.js → dist-CQNjnrld.js} +1 -1
  38. package/dist/assets/{ebnfDiagram-V4P3TTPT-9AAhU7In.js → ebnfDiagram-V4P3TTPT-DE3MZoJO.js} +1 -1
  39. package/dist/assets/{elk-QONTBCTQ-BgvnD1v_.js → elk-QONTBCTQ-p_aAQPHU.js} +1 -1
  40. package/dist/assets/{erDiagram-KYQIGQAA-RLtbiLlP.js → erDiagram-KYQIGQAA-DZO3vQrk.js} +1 -1
  41. package/dist/assets/{eventmodeling-K75KTNOO-DVY22vNZ.js → eventmodeling-K75KTNOO-BII9EPl5.js} +1 -1
  42. package/dist/assets/flowDiagram-IUGLLWBU-D9uPno39.js +1 -0
  43. package/dist/assets/{ganttDiagram-R7GPNTJY-hHiBGa9W.js → ganttDiagram-R7GPNTJY-BKiH3v2r.js} +1 -1
  44. package/dist/assets/{gitGraph-VSP46ZUC-Cvxay1wh.js → gitGraph-VSP46ZUC-oHL6cr73.js} +1 -1
  45. package/dist/assets/{gitGraphDiagram-PDYL7XHY-AOhlpSqg.js → gitGraphDiagram-PDYL7XHY-CqPVIEeo.js} +1 -1
  46. package/dist/assets/{index-CBLNQvK3.js → index-CSpirV16.js} +3 -3
  47. package/dist/assets/{info-OHQRW6UA-CJ8EUO1m.js → info-OHQRW6UA-Drrbnhpd.js} +1 -1
  48. package/dist/assets/{infoDiagram-MVDRKAX5-DOQu7oYI.js → infoDiagram-MVDRKAX5-DnQQFMAd.js} +1 -1
  49. package/dist/assets/{ishikawaDiagram-HPEHV2GE-DcVMvGmx.js → ishikawaDiagram-HPEHV2GE-DIpQlxRh.js} +1 -1
  50. package/dist/assets/{journeyDiagram-BOSHW7O7-luMeqEqz.js → journeyDiagram-BOSHW7O7-EBR9I5h1.js} +1 -1
  51. package/dist/assets/{kanban-definition-FNSA4HC5-KTZbmrW2.js → kanban-definition-FNSA4HC5-CdvNsjik.js} +1 -1
  52. package/dist/assets/{line-CmPHiOVm.js → line-CDLzgxDc.js} +1 -1
  53. package/dist/assets/{linear-DHYlKXBA.js → linear-DfXKql9u.js} +1 -1
  54. package/dist/assets/{mermaid-parser.core-BRDI-WhC.js → mermaid-parser.core-DnXGphT2.js} +3 -3
  55. package/dist/assets/{mermaid.core-DPTRaYLy.js → mermaid.core-h6fiCaSZ.js} +4 -4
  56. package/dist/assets/{mindmap-definition-6OY57UG3-70EN5Kw_.js → mindmap-definition-6OY57UG3-BiNfio-j.js} +1 -1
  57. package/dist/assets/{packet-JDAUHWVQ-CXWz83oS.js → packet-JDAUHWVQ-D76byv1S.js} +1 -1
  58. package/dist/assets/{pegDiagram-ECIUJNBX-z5UXKmp4.js → pegDiagram-ECIUJNBX-Drs8Pxdt.js} +1 -1
  59. package/dist/assets/{pie-XZMESJXO-rWI1HIQ7.js → pie-XZMESJXO-D_83z1lM.js} +1 -1
  60. package/dist/assets/{pieDiagram-GNMPY74T-Bh4Kv3NQ.js → pieDiagram-GNMPY74T-wvzIphSf.js} +1 -1
  61. package/dist/assets/{quadrantDiagram-D57K4MC3-wGM4Ckig.js → quadrantDiagram-D57K4MC3-B7EfMRWb.js} +1 -1
  62. package/dist/assets/{radar-ABXABTNO-CIJ92p5N.js → radar-ABXABTNO-Bs6dVdAR.js} +1 -1
  63. package/dist/assets/{railroad-I3PHUGI6-8bxua-Hf.js → railroad-I3PHUGI6-lKaN4miw.js} +1 -1
  64. package/dist/assets/{railroad-abnf-LNEFI6M7-B9KVI10m.js → railroad-abnf-LNEFI6M7-Bhh16b7Z.js} +1 -1
  65. package/dist/assets/{railroad-ebnf-DWYD2UWJ-Bt73rnyJ.js → railroad-ebnf-DWYD2UWJ-m6ybW-Y_.js} +1 -1
  66. package/dist/assets/{railroad-peg-JR7BVNR7-Cie82UPT.js → railroad-peg-JR7BVNR7-DNn72e7r.js} +1 -1
  67. package/dist/assets/{railroadDiagram-Y72NRBL7-C1nmi_tl.js → railroadDiagram-Y72NRBL7-DNeop1iZ.js} +1 -1
  68. package/dist/assets/{requirementDiagram-EQYSW5RA-BSianW40.js → requirementDiagram-EQYSW5RA-BVYZ2XYJ.js} +1 -1
  69. package/dist/assets/{sankeyDiagram-SY24JHO5-DIa11-h7.js → sankeyDiagram-SY24JHO5-B2MP51on.js} +1 -1
  70. package/dist/assets/{sequenceDiagram-BLEHNHQU-Df5rZKz2.js → sequenceDiagram-BLEHNHQU-BUvxstXG.js} +1 -1
  71. package/dist/assets/{src-dClg85lX.js → src-BrR0lqlG.js} +1 -1
  72. package/dist/assets/{stateDiagram-v2-A6IESKWZ-B15PTSvX.js → stateDiagram-v2-A6IESKWZ-Ba84OGKE.js} +1 -1
  73. package/dist/assets/{swimlanes-B3ENRKFQ-C9mgRCMi.js → swimlanes-B3ENRKFQ-BMSl2PNA.js} +1 -1
  74. package/dist/assets/swimlanesDiagram-AYQRV4AK-M1dmvq7m.js +8 -0
  75. package/dist/assets/{timeline-definition-JXYKGLGB-BDVf_mIv.js → timeline-definition-JXYKGLGB-DOdW-dxq.js} +1 -1
  76. package/dist/assets/{treeView-D4JQ5SDB-BbBK-25J.js → treeView-D4JQ5SDB-DZ9KQyn5.js} +1 -1
  77. package/dist/assets/{treemap-SAJKECNS-CEDX8apk.js → treemap-SAJKECNS-WEC5wtDV.js} +1 -1
  78. package/dist/assets/{usecaseDiagram-WPOLBNNX-D1QkwFF8.js → usecaseDiagram-WPOLBNNX-cldTztFL.js} +1 -1
  79. package/dist/assets/{vennDiagram-Q3Y3IDCW-5PeGZvEO.js → vennDiagram-Q3Y3IDCW-c41KySoY.js} +1 -1
  80. package/dist/assets/{wardley-7MLQ67FV-BatN94pO.js → wardley-7MLQ67FV-Bf9_aSSZ.js} +1 -1
  81. package/dist/assets/{wardleyDiagram-IVAIDKWA-DyCcDS3Y.js → wardleyDiagram-IVAIDKWA-BeKEvEBG.js} +1 -1
  82. package/dist/assets/{xychartDiagram-4URNHDU4-CbCXcRlg.js → xychartDiagram-4URNHDU4-tNkAxTxG.js} +1 -1
  83. package/dist/index.html +1 -1
  84. package/package.json +5 -1
  85. package/server/config.js +140 -14
  86. package/server/consensus.js +18 -13
  87. package/server/graph/modules_builder.js +290 -0
  88. package/server/graph/modules_catalog.js +288 -0
  89. package/server/graph/parser.js +3 -2
  90. package/server/graph/sources.js +165 -24
  91. package/server/graph/syntax.js +245 -28
  92. package/server/index.js +124 -61
  93. package/server/install.js +350 -10
  94. package/server/models_registry.js +34 -9
  95. package/server/scaffold.js +109 -9
  96. package/dist/assets/channel-BZ2_TTTJ.js +0 -1
  97. package/dist/assets/flowDiagram-IUGLLWBU-l7L5DTb5.js +0 -1
  98. package/dist/assets/swimlanesDiagram-AYQRV4AK-BGFq4t7O.js +0 -8
package/server/install.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { promises as fs } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import crypto from 'node:crypto'
4
+ import net from 'node:net'
5
+ import { globalConfigPath } from './config.js'
4
6
 
5
7
  // AnchorDB installer for knl-view. Two paths:
6
8
  // - Linux: binary from GitHub Releases (tar.gz per architecture, verified
@@ -11,10 +13,64 @@ import crypto from 'node:crypto'
11
13
  // orchestration (prompts, spawn) stays in bin/knl-view.js.
12
14
 
13
15
  export const GHCR_IMAGE = 'ghcr.io/rachmanzz/anchordb'
14
- export const HTTP_PORT_DEFAULT = 7447
15
- export const WIRE_PORT_DEFAULT = 7448
16
+ export const HTTP_PORT_DEFAULT = 17447
17
+ export const WIRE_PORT_DEFAULT = 17448
16
18
  export const CONTAINER_NAME = 'anchordb-knlview'
17
19
 
20
+ /** Check if a TCP port is currently free on a host. */
21
+ export async function isPortAvailable(port, host = '127.0.0.1') {
22
+ return new Promise((resolve) => {
23
+ const server = net.createServer()
24
+ server.once('error', () => resolve(false))
25
+ server.once('listening', () => {
26
+ server.close(() => resolve(true))
27
+ })
28
+ server.listen(port, host)
29
+ })
30
+ }
31
+
32
+ /** Find the next available TCP port sequentially starting from startPort. */
33
+ export async function findNextAvailablePort(startPort, host = '127.0.0.1') {
34
+ let port = Number(startPort)
35
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
36
+ throw new RangeError(`Invalid start port: ${startPort}`)
37
+ }
38
+ while (!(await isPortAvailable(port, host))) {
39
+ port++
40
+ if (port > 65535) {
41
+ throw new Error(`No available port found up to 65535 starting from ${startPort}`)
42
+ }
43
+ }
44
+ return port
45
+ }
46
+
47
+ /** Check desired AnchorDB ports and suggest alternatives if any is taken. */
48
+ export async function resolveAnchorPorts({
49
+ desiredWirePort = WIRE_PORT_DEFAULT,
50
+ desiredHttpPort = HTTP_PORT_DEFAULT,
51
+ host = '127.0.0.1'
52
+ } = {}) {
53
+ const wireAvailable = await isPortAvailable(desiredWirePort, host)
54
+ const httpAvailable = desiredHttpPort !== desiredWirePort && (await isPortAvailable(desiredHttpPort, host))
55
+
56
+ const suggestedWirePort = wireAvailable ? desiredWirePort : await findNextAvailablePort(desiredWirePort + 1, host)
57
+ let candidateHttp = (httpAvailable && desiredHttpPort !== suggestedWirePort)
58
+ ? desiredHttpPort
59
+ : await findNextAvailablePort(desiredHttpPort + 1, host)
60
+ if (candidateHttp === suggestedWirePort) {
61
+ candidateHttp = await findNextAvailablePort(candidateHttp + 1, host)
62
+ }
63
+
64
+ return {
65
+ wirePort: desiredWirePort,
66
+ httpPort: desiredHttpPort,
67
+ wireAvailable,
68
+ httpAvailable,
69
+ suggestedWirePort,
70
+ suggestedHttpPort: candidateHttp
71
+ }
72
+ }
73
+
18
74
  /** Release asset name for a platform/architecture, per the AnchorDB release pattern. */
19
75
  export function releaseAssetName(version, platform = process.platform, arch = process.arch) {
20
76
  const archName = arch === 'arm64' ? 'arm64' : 'amd64'
@@ -66,12 +122,12 @@ export async function verifySha256(filePath, sumsText) {
66
122
  }
67
123
 
68
124
  /**
69
- * Merge the anchordb block into the global config (~/.knlview/knl-config.json)
125
+ * Merge the anchordb block into the global config (knl-config.json)
70
126
  * without dropping other fields that may live there. Returns the config
71
127
  * contents before the overwrite (as a backup, in case the caller shows it).
72
128
  */
73
- export async function mergeGlobalAnchordb(home, anchordb) {
74
- const file = path.join(home, '.knlview', 'knl-config.json')
129
+ export async function mergeGlobalAnchordb(home, anchordb, options = {}) {
130
+ const file = globalConfigPath(home, options)
75
131
  let before = null
76
132
  let raw = {}
77
133
  try {
@@ -83,15 +139,299 @@ export async function mergeGlobalAnchordb(home, anchordb) {
83
139
  // Missing/broken file: start from an empty object.
84
140
  }
85
141
  raw.anchordb = anchordb
86
- await fs.mkdir(path.dirname(file), { recursive: true })
87
- await fs.writeFile(file, JSON.stringify(raw, null, 2) + '\n', 'utf8')
142
+ await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 })
143
+ await fs.writeFile(file, JSON.stringify(raw, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 })
144
+ try { await fs.chmod(file, 0o600) } catch {}
88
145
  return before
89
146
  }
90
147
 
91
- /** Pick an available container runtime: podman first, then docker. */
92
- export async function detectContainerRuntime(which) {
148
+ /** List all available container runtimes on the system. */
149
+ export async function availableContainerRuntimes(which) {
150
+ const found = []
151
+ for (const runtime of ['docker', 'podman']) {
152
+ if (await which(runtime)) found.push(runtime)
153
+ }
154
+ return found
155
+ }
156
+
157
+ /** Pick an available container runtime: preferred first, or first detected. */
158
+ export async function detectContainerRuntime(which, preferred = null) {
159
+ if (preferred) {
160
+ if (await which(preferred)) return preferred
161
+ return null
162
+ }
163
+ const available = await availableContainerRuntimes(which)
164
+ return available[0] || null
165
+ }
166
+
167
+ /**
168
+ * Inspect detailed container lifecycle status across available runtimes.
169
+ * Returns exists, runtime, running, valid, status, id, created, token, wire_port.
170
+ */
171
+ export async function inspectContainerStatus(exec, name = CONTAINER_NAME) {
93
172
  for (const runtime of ['podman', 'docker']) {
94
- if (await which(runtime)) return runtime
173
+ try {
174
+ const { stdout } = await exec(runtime, [
175
+ 'inspect',
176
+ '--format',
177
+ '{{.Id}}|{{.Created}}|{{.State.Status}}|{{.State.Running}}|{{json .Config.Env}}',
178
+ name
179
+ ])
180
+ const parts = stdout.trim().split('|')
181
+ if (parts.length >= 4 && parts[0]) {
182
+ const id = parts[0].trim()
183
+ const created = parts[1].trim()
184
+ const status = (parts[2] || '').trim().toLowerCase()
185
+ const running = (parts[3] || '').trim().toLowerCase() === 'true'
186
+ const valid = !['dead', 'removal', 'corrupt'].includes(status) && id.length >= 12
187
+ const envStr = parts.slice(4).join('|')
188
+ let token = null
189
+ let port = String(WIRE_PORT_DEFAULT)
190
+ try {
191
+ const envs = JSON.parse(envStr)
192
+ for (const e of envs) {
193
+ if (e.startsWith('ANCHORDB_AUTH_TOKEN=')) token = e.slice('ANCHORDB_AUTH_TOKEN='.length)
194
+ if (e.startsWith('ANCHORDB_WIRE_PORT=')) port = e.slice('ANCHORDB_WIRE_PORT='.length)
195
+ }
196
+ } catch {}
197
+
198
+ return {
199
+ exists: true,
200
+ runtime,
201
+ name,
202
+ id,
203
+ created,
204
+ status,
205
+ running,
206
+ valid,
207
+ token,
208
+ wire_port: port
209
+ }
210
+ }
211
+ } catch {
212
+ // not in this runtime
213
+ }
214
+ }
215
+
216
+ return {
217
+ exists: false,
218
+ runtime: null,
219
+ name,
220
+ id: null,
221
+ created: null,
222
+ status: null,
223
+ running: false,
224
+ valid: false,
225
+ token: null,
226
+ wire_port: String(WIRE_PORT_DEFAULT)
227
+ }
228
+ }
229
+
230
+ export async function startContainer(exec, runtime, name = CONTAINER_NAME) {
231
+ await exec(runtime, ['start', name])
232
+ }
233
+
234
+ export async function removeContainer(exec, runtime, name = CONTAINER_NAME) {
235
+ await exec(runtime, ['rm', '-f', name])
236
+ }
237
+
238
+ export async function detectGraphify(exec) {
239
+ try {
240
+ await exec('which', ['graphify'])
241
+ return true
242
+ } catch {
243
+ return false
95
244
  }
245
+ }
246
+
247
+ export const DECISIONSKILLS_REPO = 'rachmanzz/decisionskills'
248
+
249
+ /**
250
+ * Check if the decision-history skill (from rachmanzz/decisionskills) is
251
+ * installed in project or global agent directories, and if an update is available.
252
+ */
253
+ export async function checkDecisionSkillsStatus({ cwd = process.cwd(), home = process.env.HOME }) {
254
+ const projectSkill = path.join(cwd, '.agents', 'skills', 'decision-history', 'SKILL.md')
255
+ let hasProject = false
256
+ try {
257
+ await fs.stat(projectSkill)
258
+ hasProject = true
259
+ } catch {}
260
+
261
+ const globalPaths = [
262
+ path.join(home || '', '.codex', 'skills', 'decision-history', 'SKILL.md'),
263
+ path.join(home || '', '.claude', 'skills', 'decision-history', 'SKILL.md'),
264
+ path.join(home || '', '.config', 'opencode', 'skills', 'decision-history', 'SKILL.md'),
265
+ path.join(home || '', '.gemini', 'config', 'plugins', 'decision-history', 'skills', 'decision-history', 'SKILL.md'),
266
+ path.join(home || '', '.agents', 'skills', 'decision-history', 'SKILL.md')
267
+ ]
268
+
269
+ let hasGlobal = false
270
+ for (const p of globalPaths) {
271
+ try {
272
+ await fs.stat(p)
273
+ hasGlobal = true
274
+ break
275
+ } catch {}
276
+ }
277
+
278
+ let currentVersion = null
279
+ let latestVersion = null
280
+ let status = 'not_installed'
281
+
282
+ if (hasProject || hasGlobal) {
283
+ status = 'installed'
284
+ const versionCandidates = [
285
+ path.join(cwd, '.agents', 'skills', 'decision-history', 'VERSION'),
286
+ path.join(home || '', '.gemini', 'config', 'plugins', 'decision-history', 'VERSION')
287
+ ]
288
+ for (const vFile of versionCandidates) {
289
+ try {
290
+ const text = await fs.readFile(vFile, 'utf8')
291
+ if (text.trim()) {
292
+ currentVersion = text.trim()
293
+ break
294
+ }
295
+ } catch {}
296
+ }
297
+
298
+ try {
299
+ const res = await fetch('https://raw.githubusercontent.com/rachmanzz/decisionskills/main/VERSION', {
300
+ signal: AbortSignal.timeout(2000)
301
+ })
302
+ if (res.ok) {
303
+ latestVersion = (await res.text()).trim()
304
+ if (currentVersion && latestVersion && currentVersion !== latestVersion) {
305
+ status = 'needs_update'
306
+ }
307
+ }
308
+ } catch {}
309
+ }
310
+
311
+ return {
312
+ hasProject,
313
+ hasGlobal,
314
+ status, // 'not_installed' | 'installed' | 'needs_update'
315
+ currentVersion,
316
+ latestVersion,
317
+ projectPath: projectSkill
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Install or update the decision-history skill from rachmanzz/decisionskills.
323
+ * Scope: 'project' | 'global' | 'both'.
324
+ */
325
+ export async function installDecisionSkills({
326
+ scope = 'project',
327
+ cwd = process.cwd(),
328
+ home = process.env.HOME,
329
+ exec
330
+ }) {
331
+ const results = []
332
+
333
+ const copyDir = async (src, dest) => {
334
+ await fs.mkdir(dest, { recursive: true })
335
+ const entries = await fs.readdir(src, { withFileTypes: true })
336
+ for (const entry of entries) {
337
+ const srcPath = path.join(src, entry.name)
338
+ const destPath = path.join(dest, entry.name)
339
+ if (entry.isDirectory()) {
340
+ await copyDir(srcPath, destPath)
341
+ } else {
342
+ await fs.copyFile(srcPath, destPath)
343
+ }
344
+ }
345
+ }
346
+
347
+ const doInstall = async (isGlobal) => {
348
+ let success = false
349
+ if (exec) {
350
+ try {
351
+ const args = ['--yes', 'skills', 'add', DECISIONSKILLS_REPO, '--skill', 'decision-history', '-y', '--copy']
352
+ if (isGlobal) args.push('-g')
353
+ await exec('npx', args, { cwd })
354
+ success = true
355
+ } catch {}
356
+ }
357
+
358
+ if (!success) {
359
+ const fallbackSources = [
360
+ path.join(home || '', '.gemini', 'config', 'plugins', 'decision-history', 'skills', 'decision-history'),
361
+ path.join(home || '', '.agents', 'skills', 'decision-history'),
362
+ path.join(home || '', '.codex', 'skills', 'decision-history')
363
+ ]
364
+ let srcDir = null
365
+ for (const candidate of fallbackSources) {
366
+ try {
367
+ await fs.stat(path.join(candidate, 'SKILL.md'))
368
+ srcDir = candidate
369
+ break
370
+ } catch {}
371
+ }
372
+
373
+ if (srcDir) {
374
+ const dest = isGlobal
375
+ ? path.join(home || '', '.agents', 'skills', 'decision-history')
376
+ : path.join(cwd, '.agents', 'skills', 'decision-history')
377
+ await copyDir(srcDir, dest)
378
+ success = true
379
+ }
380
+ }
381
+
382
+ if (isGlobal && exec) {
383
+ const agyDir = path.join(home || '', '.gemini', 'config', 'plugins', 'decision-history')
384
+ try {
385
+ await fs.stat(agyDir)
386
+ await exec('git', ['pull'], { cwd: agyDir })
387
+ } catch {
388
+ try {
389
+ await fs.mkdir(path.dirname(agyDir), { recursive: true })
390
+ await exec('git', ['clone', 'https://github.com/rachmanzz/decisionskills.git', agyDir])
391
+ } catch {}
392
+ }
393
+ }
394
+
395
+ return success
396
+ }
397
+
398
+ if (scope === 'project' || scope === 'both') {
399
+ const ok = await doInstall(false)
400
+ results.push({ scope: 'project', success: ok })
401
+ }
402
+ if (scope === 'global' || scope === 'both') {
403
+ const ok = await doInstall(true)
404
+ results.push({ scope: 'global', success: ok })
405
+ }
406
+
407
+ return results
408
+ }
409
+
410
+ /**
411
+ * Detect a running AnchorDB daemon on the system (docker, podman, or binary).
412
+ * Inspects containers for CONTAINER_NAME or checks running processes.
413
+ */
414
+ export async function detectRunningDaemon(exec) {
415
+ const container = await inspectContainerStatus(exec)
416
+ if (container.exists && container.running && container.valid) {
417
+ return {
418
+ daemon: container.runtime,
419
+ deamon: container.runtime,
420
+ container_name: container.name,
421
+ container_id: container.id,
422
+ container_created_on: container.created,
423
+ token: container.token,
424
+ wire_port: container.wire_port
425
+ }
426
+ }
427
+
428
+ // Check if binary process is running
429
+ try {
430
+ const { stdout } = await exec('pgrep', ['-f', 'anchordb-server'])
431
+ if (stdout.trim()) {
432
+ return { daemon: 'binary', deamon: 'binary' }
433
+ }
434
+ } catch {}
435
+
96
436
  return null
97
437
  }
@@ -1,8 +1,12 @@
1
1
  import { promises as fs } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import crypto from 'node:crypto'
4
+ import os from 'node:os'
5
+ import { knlGlobalDir } from './config.js'
4
6
 
5
- // Global registry of AnchorDB model names: ~/.knlview/models.json.
7
+ // Global registry of AnchorDB model names:
8
+ // - %APPDATA%\knlview\models.json on Windows
9
+ // - ~/.knlview/models.json on Linux and macOS
6
10
  //
7
11
  // Model names may be random; what makes a random name meaningful is this
8
12
  // registry: the mapping of name → project, role, and project location.
@@ -11,8 +15,8 @@ import crypto from 'node:crypto'
11
15
 
12
16
  const REGISTRY_FILE = 'models.json'
13
17
 
14
- export function modelRegistryPath(home = process.env.HOME) {
15
- return path.join(home || '', '.knlview', REGISTRY_FILE)
18
+ export function modelRegistryPath(home = null, options = {}) {
19
+ return path.join(knlGlobalDir(home, options), REGISTRY_FILE)
16
20
  }
17
21
 
18
22
  /**
@@ -31,9 +35,31 @@ export function generateModelName() {
31
35
  * Read the registry. Missing/broken file = empty registry, not an error.
32
36
  * @returns {Promise<{models: object, error: string|null}>}
33
37
  */
34
- export async function readModelRegistry(home = process.env.HOME) {
38
+ export async function readModelRegistry(home = null, options = {}) {
39
+ const primary = modelRegistryPath(home, options)
40
+ let content = null
35
41
  try {
36
- const parsed = JSON.parse(await fs.readFile(modelRegistryPath(home), 'utf8'))
42
+ content = await fs.readFile(primary, 'utf8')
43
+ } catch (err) {
44
+ if (err?.code === 'ENOENT') {
45
+ const platform = options.platform || process.platform
46
+ const env = options.env || process.env
47
+ if (platform === 'win32' && !home) {
48
+ const winHome = env.USERPROFILE || env.HOME || (typeof os.homedir === 'function' ? os.homedir() : null)
49
+ if (winHome) {
50
+ try {
51
+ content = await fs.readFile(path.join(winHome, '.knlview', REGISTRY_FILE), 'utf8')
52
+ } catch {}
53
+ }
54
+ }
55
+ if (!content) return { models: {}, error: null }
56
+ } else {
57
+ return { models: {}, error: err.message }
58
+ }
59
+ }
60
+
61
+ try {
62
+ const parsed = JSON.parse(content)
37
63
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
38
64
  return { models: {}, error: 'registry contents are not an object' }
39
65
  }
@@ -45,7 +71,6 @@ export async function readModelRegistry(home = process.env.HOME) {
45
71
  }
46
72
  return { models, error: null }
47
73
  } catch (err) {
48
- if (err?.code === 'ENOENT') return { models: {}, error: null }
49
74
  return { models: {}, error: err.message }
50
75
  }
51
76
  }
@@ -56,8 +81,8 @@ export async function readModelRegistry(home = process.env.HOME) {
56
81
  * whether to warn.
57
82
  * @returns {Promise<{previous: object|null, written: boolean, error: string|null}>}
58
83
  */
59
- export async function registerModel(name, { role, project, cwd }, home = process.env.HOME) {
60
- const { models, error } = await readModelRegistry(home)
84
+ export async function registerModel(name, { role, project, cwd }, home = null, options = {}) {
85
+ const { models, error } = await readModelRegistry(home, options)
61
86
  if (error) return { previous: null, written: false, error }
62
87
 
63
88
  const previous = models[name] || null
@@ -74,7 +99,7 @@ export async function registerModel(name, { role, project, cwd }, home = process
74
99
  }
75
100
 
76
101
  try {
77
- const file = modelRegistryPath(home)
102
+ const file = modelRegistryPath(home, options)
78
103
  await fs.mkdir(path.dirname(file), { recursive: true })
79
104
  await fs.writeFile(file, JSON.stringify(models, null, 2) + '\n', 'utf8')
80
105
  return { previous: null, written: true, error: null }
@@ -31,16 +31,18 @@ Nothing here yet. Put markdown files in this folder.
31
31
 
32
32
  export async function scaffoldBase({ root, structure, folders = [] }) {
33
33
  const actions = []
34
- await fs.mkdir(root, { recursive: true })
35
- actions.push({ kind: 'dir', path: root, status: 'created' })
34
+ const rootAbs = path.resolve(root)
35
+ await fs.mkdir(rootAbs, { recursive: true })
36
+ actions.push({ kind: 'dir', path: rootAbs, status: 'created' })
36
37
 
37
38
  if (structure === 'empty') return actions
38
39
 
39
40
  const dirs = structure === 'custom' ? folders.map(cleanRel).filter(Boolean) : STANDARD_DIRS
40
41
 
41
- await writeFileSafe(path.join(root, 'README.md'), ROOT_README, actions)
42
+ await writeFileSafe(path.join(rootAbs, 'README.md'), ROOT_README, actions)
42
43
  for (const dir of dirs) {
43
- const abs = path.join(root, dir)
44
+ const abs = path.resolve(rootAbs, dir)
45
+ if (!abs.startsWith(rootAbs + path.sep)) continue
44
46
  await fs.mkdir(abs, { recursive: true })
45
47
  actions.push({ kind: 'dir', path: abs, status: 'created' })
46
48
  await writeFileSafe(path.join(abs, 'README.md'), SECTION_README(dir), actions)
@@ -78,6 +80,89 @@ export async function ensureGitignore({ cwd }) {
78
80
  return { path: abs, status: 'created' }
79
81
  }
80
82
 
83
+ export function buildAgentInstructionBlock(hasGraphify = false) {
84
+ const fallback = hasGraphify
85
+ ? `- Graphify is the fallback. Use it only when a specific need remains unresolved by KNL-View or the user explicitly requests Graphify. If KNL-View answers the task, stop; do not run Graphify for duplicate analysis.
86
+ - When the user explicitly invokes \`/graphify\`, honor that request without automatically running both tools.
87
+
88
+ `
89
+ : ''
90
+
91
+ return `<!-- knl-view:start -->
92
+ # Knowledge graph tools
93
+ - KNL-View is the primary graph tool. For \`.knowledges\`, source graphs, and project graph questions, read the project's \`knl-view\` skill and use the relevant KNL-View feature first.
94
+ ${fallback}## KNL-View graph access
95
+ - Use the KNL-View feature that fits the task (\`graph trace\`, \`sources trace/sync/collect\`, \`model check/new\`, consensus commands, or the viewer). Do not approximate a supported KNL-View operation with ad-hoc graph queries.
96
+ - Route all graph reads and writes through KNL-View's documented CLI, viewer, or application endpoints. Never connect to AnchorDB directly through AnchorWire, its CLI, direct database endpoints, files, SQL, or custom clients/scripts. If KNL-View cannot perform a requested operation, explain the limitation; do not bypass it.
97
+ - Keep graph work targeted: specify the relevant node/path, direction, and smallest useful hop count; summarize only relevant nodes and edges; avoid dumping whole graphs or large raw JSON.
98
+ - Follow the KNL-View skill's trace-before-edit and \`sources sync\`-after-change rules. Treat edge direction as lineage; do not claim causality without separate evidence.
99
+ <!-- knl-view:end -->`
100
+ }
101
+
102
+ export function stripLegacyKnlInstructions(text) {
103
+ let cleaned = text
104
+ cleaned = cleaned.replace(/<!-- knl-view:start -->[\s\S]*?<!-- knl-view:end -->/g, '\n')
105
+ cleaned = cleaned.replace(/<!--\s*knl-view:(?:start|end)\s*-->/g, '\n')
106
+ cleaned = cleaned.replace(
107
+ /(?:^|\n)# Knowledge graph tools[\s\S]*?(?=(?:\n#+\s)|$)/g,
108
+ '\n'
109
+ )
110
+ cleaned = cleaned.replace(
111
+ /(?:^|\n)## KNL-View graph access[\s\S]*?(?=(?:\n#+\s)|$)/g,
112
+ '\n'
113
+ )
114
+ return cleaned.trim()
115
+ }
116
+
117
+ export async function seedAgentInstructions({ cwd = process.cwd(), home = process.env.HOME, hasGraphify = false }) {
118
+ const block = buildAgentInstructionBlock(hasGraphify)
119
+ const candidatePaths = [
120
+ // Project files:
121
+ path.join(cwd, 'AGENTS.md'),
122
+ path.join(cwd, 'GEMINI.md'),
123
+ path.join(cwd, 'CLAUDE.md'),
124
+ // Global files:
125
+ path.join(home || '', '.codex', 'AGENTS.md'),
126
+ path.join(home || '', '.gemini', 'GEMINI.md'),
127
+ path.join(home || '', '.claude', 'CLAUDE.md'),
128
+ path.join(home || '', '.agents', 'AGENTS.md')
129
+ ]
130
+
131
+ const actions = []
132
+
133
+ for (const abs of candidatePaths) {
134
+ const parentDir = path.dirname(abs)
135
+ const parentExists = await statOrNull(parentDir)
136
+ if (!parentExists) continue
137
+
138
+ const stat = await statOrNull(abs)
139
+ if (stat && stat.isFile()) {
140
+ const content = await fs.readFile(abs, 'utf8')
141
+ const cleaned = stripLegacyKnlInstructions(content)
142
+ const next = cleaned.length ? `${cleaned}\n\n${block}\n` : `${block}\n`
143
+
144
+ if (next !== content) {
145
+ await fs.writeFile(abs, next, 'utf8')
146
+ actions.push({ path: abs, status: 'updated' })
147
+ } else {
148
+ actions.push({ path: abs, status: 'skipped' })
149
+ }
150
+ }
151
+ }
152
+
153
+ // If no project file was found/updated, create ./AGENTS.md
154
+ const projectAgent = path.join(cwd, 'AGENTS.md')
155
+ if (!actions.some((a) => a.path.startsWith(cwd))) {
156
+ const stat = await statOrNull(projectAgent)
157
+ if (!stat) {
158
+ await fs.writeFile(projectAgent, `${block}\n`, 'utf8')
159
+ actions.push({ path: projectAgent, status: 'created' })
160
+ }
161
+ }
162
+
163
+ return actions
164
+ }
165
+
81
166
  async function writeFileSafe(abs, content, actions) {
82
167
  const exists = await statOrNull(abs)
83
168
  if (exists) {
@@ -89,7 +174,12 @@ async function writeFileSafe(abs, content, actions) {
89
174
  }
90
175
 
91
176
  function cleanRel(value) {
92
- return String(value).trim().replace(/^\/+/, '').replace(/\/+$/, '')
177
+ const cleaned = String(value || '').trim()
178
+ const normalized = path.normalize(cleaned).replace(/^(\.\.[\/\\])+/, '').replace(/^[\/\\]+/, '').replace(/[\/\\]+$/, '')
179
+ if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('..' + path.sep)) {
180
+ return ''
181
+ }
182
+ return normalized
93
183
  }
94
184
 
95
185
  async function statOrNull(abs) {
@@ -152,10 +242,10 @@ then folders.
152
242
  },
153
243
  "files": {
154
244
  "personal-assistant.md": { "title": "Personal Assistant" },
155
- "rahasia.md": { "ignore": true }
245
+ "secret.md": { "ignore": true }
156
246
  },
157
247
  "dirs": {
158
- "draft-lama": { "ignore": true }
248
+ "old-drafts": { "ignore": true }
159
249
  }
160
250
  }
161
251
  \`\`\`
@@ -173,7 +263,7 @@ then folders.
173
263
  bring it back:
174
264
 
175
265
  \`\`\`json
176
- { "files": { "rahasia.md": { "ignore": true } }, "dirs": { "draft-lama": { "ignore": true } } }
266
+ { "files": { "secret.md": { "ignore": true } }, "dirs": { "old-drafts": { "ignore": true } } }
177
267
  \`\`\`
178
268
 
179
269
  Paths are relative to the \`.knowledges\` root, use \`/\` separators, and
@@ -276,6 +366,12 @@ file or symbol is tombstoned (attribute \`tombstone = 2\`) after its
276
366
  edges are unlinked; if it comes back, the tombstone lifts and the edges
277
367
  are relinked.
278
368
 
369
+ External modules graph (third-party dependencies): stored in global AnchorDB
370
+ models per ecosystem (\`modules-npm\`, \`modules-go\`, \`modules-cargo\`, \`modules-pypi\`)
371
+ and cataloged in \`~/.knlview/modules/<eco>.json\`. Built on-demand via
372
+ \`knl-view modules build-graph <eco>::<name>\`, avoiding polluting project models.
373
+ Traceable via \`knl-view modules trace <eco>::<name> [symbol]\`.
374
+
279
375
  Decision history skill: recording architectural decisions is delegated
280
376
  to the separate decision-history skill (its decision block format is its
281
377
  own domain; this skill never writes decision blocks). If it is not
@@ -340,7 +436,7 @@ of that directory.
340
436
 
341
437
  Every roast must state:
342
438
  - \`Severity\`: \`P0|P1|P2|P3\`
343
- - \`Class\`: \`BUG|PERF|VULN|DEBT|CEILING|PROSES\`
439
+ - \`Class\`: \`BUG|PERF|VULN|DEBT|CEILING|PROCESS\`
344
440
 
345
441
  ## When the agent triggers the graph
346
442
 
@@ -364,6 +460,10 @@ this.
364
460
  - Before renaming a symbol or moving it to another file: remember its
365
461
  record id contains the old name/path. Sync tombstones the stale record
366
462
  and creates the new one; trace by the new id afterwards.
463
+ - Before reading raw third-party library source files:
464
+ run \`knl-view modules build-graph <eco>::<module>\` (e.g. \`npm::express\`, \`go::github.com/gin-gonic/gin\`)
465
+ to index it on-demand into global AnchorDB model \`modules-<eco>\`, then trace
466
+ its methods with \`knl-view modules trace <eco>::<module> [symbol]\` to save context tokens.
367
467
  - A new architectural decision: trigger the decision-history skill
368
468
  (install it first if missing; see "Decision history skill" above).
369
469
  The decision block format is that skill's domain; this skill never
@@ -1 +0,0 @@
1
- import{ot as e,st as t}from"./chunk-DSNLSHJZ-BM5PBFmz.js";var n=(n,r)=>t.lang.round(e.parse(n)[r]);export{n as t};
@@ -1 +0,0 @@
1
- import"./src-dClg85lX.js";import"./chunk-DSNLSHJZ-BM5PBFmz.js";import"./chunk-7S6XSTFO-B0p3o547.js";import"./chunk-YZO67HFG-DJqACL5V.js";import"./chunk-AQDTSKZB-CCxwNyyA.js";import"./chunk-UHW7PFBP-CJs0VwBY.js";import"./chunk-HAGFOTED-UCDVFuN_.js";import"./chunk-ZAUX2LTX-CWfaYtwb.js";import"./chunk-WQQL4CDK-DcjPeAfQ.js";import"./chunk-55XCU76K-QR_ARj8I.js";import"./chunk-XXDRQBXY-glA0kB5s.js";import"./chunk-VQ5W62QI-DQxjgE4_.js";import"./chunk-HNE4Q36P-C_1sakp2.js";export{n as diagram}from"./chunk-NQOKRVIF-b2D1Z4oK.js";import"./mermaid.core-DPTRaYLy.js";
@@ -1,8 +0,0 @@
1
- import{n as e}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import"./src-dClg85lX.js";import"./chunk-DSNLSHJZ-BM5PBFmz.js";import"./chunk-7S6XSTFO-B0p3o547.js";import"./chunk-YZO67HFG-DJqACL5V.js";import"./chunk-AQDTSKZB-CCxwNyyA.js";import"./chunk-UHW7PFBP-CJs0VwBY.js";import"./chunk-HAGFOTED-UCDVFuN_.js";import"./chunk-ZAUX2LTX-CWfaYtwb.js";import"./chunk-WQQL4CDK-DcjPeAfQ.js";import"./chunk-55XCU76K-QR_ARj8I.js";import"./chunk-XXDRQBXY-glA0kB5s.js";import"./chunk-VQ5W62QI-DQxjgE4_.js";import"./chunk-HNE4Q36P-C_1sakp2.js";import{r as t,t as n}from"./chunk-NQOKRVIF-b2D1Z4oK.js";import"./mermaid.core-DPTRaYLy.js";var r=n({styles:e(e=>`${t(e)}
2
- .swimlane.cluster:not([data-color-id]) rect {
3
- stroke: ${e.clusterBorder} !important;
4
- }
5
- [data-look="neo"].cluster rect {
6
- filter: none;
7
- }
8
- `,`getStyles`)});export{r as diagram};