dsh-code-review-graph 0.1.1 → 0.1.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/client.tsx", "../src/html.ts"],
4
- "sourcesContent": ["import React, { useEffect, useState } from 'react';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client';\nimport type {} from '@deepseek-ai/dsh-client-ui-slots';\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client';\nimport type {} from '@deepseek-ai/dsh-api-remotes/client';\nimport type {} from '@deepseek-ai/dsh-api-workspace-files/client';\nimport { inlineGraph } from './html.ts';\n\nexport const inject = ['slots', 'sidebarRightTabs', 'remote', 'remote.workspaceFiles'];\nexport const name = 'code-review-graph-client';\ntype ReadResult = { ok: true; value: { data: Uint8Array | string } } | { ok: false; error: { message: string } };\ntype WorkspaceReader = {\n readBytes?: (session: string, path: string, options: object, signal: AbortSignal) => Promise<ReadResult>;\n readAll?: (session: string, path: string, signal: AbortSignal) => Promise<ReadResult>;\n readRelated?: (session: string, base: string, path: string, signal: AbortSignal) => Promise<ReadResult>;\n};\nexport async function readAsset(reader: WorkspaceReader, session: string, path: string, signal: AbortSignal, base?: string): Promise<string> {\n const result = reader.readBytes ? await reader.readBytes(session, path, base ? { baseFile: base } : {}, signal)\n : base && reader.readRelated ? await reader.readRelated(session, base, path, signal)\n : reader.readAll ? await reader.readAll(session, path, signal) : (() => { throw new Error('Unsupported DSH file reader'); })();\n if (!result.ok) throw new Error(result.error.message);\n if (result.value.data instanceof Uint8Array) return new TextDecoder().decode(result.value.data);\n const base64 = result.value.data;\n if (typeof base64 !== 'string') throw new Error('Invalid graph asset response');\n return new TextDecoder().decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)));\n}\n\nexport function apply(ctx: Context): void {\n const id = 'dsh-code-review-graph';\n ctx.effect(() => ctx.sidebarRightTabs.register({ id, kind: 'code-review-graph', priority: 'builtin', title: () => 'Code graph / \u4EE3\u7801\u56FE\u8C31', guide: [{ id: 'graph', order: 30, title: () => 'Code graph / \u4EE3\u7801\u56FE\u8C31', description: () => 'Explore dependencies, changes and execution paths locally.' }] }));\n function Graph({ sessionId, useTabInfo }: { sessionId: string; useTabInfo: () => { tab: { signal: AbortSignal } } }) {\n const { tab } = useTabInfo();\n const [html, setHtml] = useState('');\n const [error, setError] = useState('');\n const [generation, refresh] = useState(0);\n const download = () => {\n const url = URL.createObjectURL(new Blob([html], { type: 'text/html;charset=utf-8' }));\n const link = document.createElement('a'); link.href = url; link.download = 'code-review-graph.html'; link.click();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n };\n useEffect(() => {\n const life = new AbortController();\n const signal = AbortSignal.any([life.signal, tab.signal]);\n const base = '.code-review-graph/graph.html';\n const reader = ctx.remote.workspaceFiles as unknown as WorkspaceReader;\n setError('');\n void readAsset(reader, sessionId, base, signal).then(html => inlineGraph(html, path => readAsset(reader, sessionId, path, signal, base))).then(result => {\n if (!signal.aborted) setHtml(result);\n }).catch(error => { if (!signal.aborted) setError(String(error)); });\n return () => life.abort();\n }, [sessionId, generation, tab.signal]);\n return <section style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 8 }}>\n <div><button type=\"button\" onClick={() => refresh(n => n + 1)}>Refresh / \u5237\u65B0</button> <button type=\"button\" disabled={!html} onClick={download}>Export HTML / \u5BFC\u51FA HTML</button></div>\n {error && <p role=\"status\">Generate the graph with crg_visualize, then refresh. / \u4F7F\u7528 crg_visualize \u751F\u6210\u56FE\u8C31\u540E\u5237\u65B0\u3002<br />{error}</p>}\n {html && <iframe title=\"Interactive code graph / \u4EA4\u4E92\u4EE3\u7801\u56FE\u8C31\" sandbox=\"allow-scripts allow-downloads\" srcDoc={html} style={{ width: '100%', flex: 1, border: 0 }} />}\n </section>;\n }\n ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: id }, Graph)));\n}\n", "/** Inline only bundled scripts and local data; the iframe never fetches remote assets. */\nexport async function inlineGraph(html: string, read: (path: string) => Promise<string>): Promise<string> {\n // The upstream CDN fallback contains a literal script tag inside a JS string.\n // Local D3 is required here, so remove that loader before inspecting HTML tags.\n html = html.replace(/<script>\\s*window\\.d3\\s*\\|\\|\\s*document\\.write\\([\\s\\S]*?\\);\\s*<\\/script>/gi, '');\n const scripts = [...html.matchAll(/<script\\b([^>]*?)\\bsrc=[\"']([^\"']+)[\"']([^>]*)>[\\s\\S]*?<\\/script>/gi)];\n for (const match of scripts) {\n const path = match[2];\n if (/^(?:https?:|data:|\\/|\\\\)|\\.\\./i.test(path)) throw new Error('Graph contains a nonlocal script');\n const script = await read(path);\n html = html.replace(match[0], () => `<script>${script.replace(/<\\/script/gi, '<\\\\/script')}</script>`);\n }\n const policy = `<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'; font-src 'none'\">`;\n return html.replace(/<head>/i, `<head>${policy}`);\n}\n"],
5
- "mappings": "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",
6
- "names": ["html", "error", "React"]
4
+ "sourcesContent": ["import React, { useEffect, useRef, useState } from 'react';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client';\nimport type {} from '@deepseek-ai/dsh-client-ui-slots';\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client';\nimport type {} from '@deepseek-ai/dsh-api-remotes/client';\nimport type {} from '@deepseek-ai/dsh-api-workspace-files/client';\nimport { inlineGraph } from './html.ts';\n\nexport const inject = ['slots', 'sidebarRightTabs', 'remote', 'remote.workspaceFiles', 'remote.commands'];\nexport const name = 'code-review-graph-client';\ntype ReadResult = { ok: true; value: { data: Uint8Array | string } } | { ok: false; error: { message: string } };\ntype WorkspaceReader = {\n readBytes?: (session: string, path: string, options: object, signal: AbortSignal) => Promise<ReadResult>;\n readAll?: (session: string, path: string, signal: AbortSignal) => Promise<ReadResult>;\n readRelated?: (session: string, base: string, path: string, signal: AbortSignal) => Promise<ReadResult>;\n};\nexport async function readAsset(reader: WorkspaceReader, session: string, path: string, signal: AbortSignal, base?: string): Promise<string> {\n const result = reader.readBytes ? await reader.readBytes(session, path, base ? { baseFile: base } : {}, signal)\n : base && reader.readRelated ? await reader.readRelated(session, base, path, signal)\n : reader.readAll ? await reader.readAll(session, path, signal) : (() => { throw new Error('Unsupported DSH file reader'); })();\n if (!result.ok) throw new Error(result.error.message);\n if (result.value.data instanceof Uint8Array) return new TextDecoder().decode(result.value.data);\n const base64 = result.value.data;\n if (typeof base64 !== 'string') throw new Error('Invalid graph asset response');\n return new TextDecoder().decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)));\n}\n\nexport function apply(ctx: Context): void {\n const id = 'dsh-code-review-graph';\n ctx.effect(() => ctx.sidebarRightTabs.register({ id, kind: 'code-review-graph', priority: 'builtin', title: () => 'Code graph / \u4EE3\u7801\u56FE\u8C31', guide: [{ id: 'graph', order: 30, title: () => 'Code graph / \u4EE3\u7801\u56FE\u8C31', description: () => 'Explore dependencies, changes and execution paths locally.' }] }));\n function Graph({ sessionId, useTabInfo }: { sessionId: string; useTabInfo: () => { tab: { signal: AbortSignal } } }) {\n const { tab } = useTabInfo();\n const [html, setHtml] = useState('');\n const [error, setError] = useState('');\n const [notice, setNotice] = useState('');\n const [busy, setBusy] = useState(false);\n const operation = useRef<AbortController>();\n const [generation, refresh] = useState(0);\n useEffect(() => {\n setHtml(''); setNotice(''); setError(''); setBusy(false);\n return () => { operation.current?.abort(); operation.current = undefined; };\n }, [sessionId, tab.signal]);\n const execute = async (command: 'crg-setup' | 'crg-graph') => {\n if (operation.current) return;\n const current = new AbortController(); operation.current = current;\n const signal = AbortSignal.any([current.signal, tab.signal]);\n setBusy(true); setError('');\n setNotice(command === 'crg-setup' ? '\u6B63\u5728\u51C6\u5907\u5F15\u64CE\uFF1B\u9996\u6B21\u9700\u8981\u4E0B\u8F7D\u4F9D\u8D56\u2026 / Preparing engine; first use downloads dependencies\u2026' : '\u6B63\u5728\u51C6\u5907\u5F15\u64CE\u5E76\u751F\u6210\u56FE\u8C31\u2026 / Preparing engine and generating graph\u2026');\n try {\n const result = await ctx.remote.commands.execute(sessionId as Parameters<typeof ctx.remote.commands.execute>[0], `/${command}`, [], signal);\n signal.throwIfAborted();\n if (!result.ok) throw new Error(result.error.message);\n if (!result.value) throw new Error('Graph commands are unavailable. Enable the Host component. / \u56FE\u8C31\u547D\u4EE4\u4E0D\u53EF\u7528\uFF0C\u8BF7\u542F\u7528\u63D2\u4EF6\u7EC4\u4EF6\u3002');\n if (result.value.result.kind === 'error') throw new Error(result.value.result.text);\n setNotice(result.value.result.text ?? '\u5B8C\u6210 / Done');\n if (command === 'crg-graph') refresh(n => n + 1);\n } catch (error) {\n if (operation.current !== current) return;\n if (current.signal.aborted) setNotice('\u5DF2\u53D6\u6D88\uFF0C\u53EF\u91CD\u65B0\u5C1D\u8BD5\u3002 / Cancelled. You can retry.');\n else if (!tab.signal.aborted) { setNotice(''); setError(String(error)); }\n } finally {\n if (operation.current === current) { operation.current = undefined; setBusy(false); }\n }\n };\n const download = () => {\n const url = URL.createObjectURL(new Blob([html], { type: 'text/html;charset=utf-8' }));\n const link = document.createElement('a'); link.href = url; link.download = 'code-review-graph.html'; link.click();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n };\n useEffect(() => {\n const life = new AbortController();\n const signal = AbortSignal.any([life.signal, tab.signal]);\n const base = '.code-review-graph/graph.html';\n const reader = ctx.remote.workspaceFiles as unknown as WorkspaceReader;\n setError('');\n void readAsset(reader, sessionId, base, signal).then(html => inlineGraph(html, path => readAsset(reader, sessionId, path, signal, base))).then(result => {\n if (!signal.aborted) setHtml(result);\n }).catch(error => { if (!signal.aborted) setError(String(error)); });\n return () => life.abort();\n }, [sessionId, generation, tab.signal]);\n return <section style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 8 }}>\n <div>\n <button type=\"button\" disabled={busy} onClick={() => void execute('crg-graph')}>{html ? '\u66F4\u65B0\u56FE\u8C31 / Update graph' : '\u51C6\u5907\u5F15\u64CE\u5E76\u751F\u6210\u56FE\u8C31 / Set up and generate graph'}</button>{' '}\n <button type=\"button\" disabled={busy} onClick={() => void execute('crg-setup')}>\u51C6\u5907\u5F15\u64CE / Set up engine</button>{' '}\n {busy && <button type=\"button\" onClick={() => operation.current?.abort()}>\u53D6\u6D88 / Cancel</button>}{' '}\n <button type=\"button\" disabled={busy} onClick={() => refresh(n => n + 1)}>Refresh / \u91CD\u65B0\u8BFB\u53D6</button>{' '}\n <button type=\"button\" disabled={!html} onClick={download}>Export HTML / \u5BFC\u51FA HTML</button>\n </div>\n {!html && !busy && <p>\u70B9\u51FB\u201C\u51C6\u5907\u5F15\u64CE\u5E76\u751F\u6210\u56FE\u8C31\u201D\u5373\u53EF\u5F00\u59CB\uFF0C\u65E0\u9700\u804A\u5929\u6216\u586B\u5199\u53C2\u6570\u3002\u9996\u6B21\u4F1A\u5728 DSH \u7684\u9694\u79BB\u76EE\u5F55\u4E0B\u8F7D\u5F15\u64CE\u4F9D\u8D56\uFF0C\u9700\u8981 uv \u6216 Python 3.10+\u3002 / Click Set up and generate graph. No chat or parameters needed. First use downloads isolated engine dependencies; uv or Python 3.10+ is required.</p>}\n {notice && <p role=\"status\" style={{ whiteSpace: 'pre-wrap' }}>{notice}</p>}\n {error && <details open><summary>\u65E0\u6CD5\u52A0\u8F7D\u6216\u751F\u6210\u56FE\u8C31 / Graph unavailable</summary><p role=\"alert\" style={{ whiteSpace: 'pre-wrap' }}>{error}</p></details>}\n {html && <iframe title=\"Interactive code graph / \u4EA4\u4E92\u4EE3\u7801\u56FE\u8C31\" sandbox=\"allow-scripts allow-downloads\" srcDoc={html} style={{ width: '100%', flex: 1, border: 0 }} />}\n </section>;\n }\n ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: id }, Graph)));\n}\n", "/** Inline only bundled scripts and local data; the iframe never fetches remote assets. */\nexport async function inlineGraph(html: string, read: (path: string) => Promise<string>): Promise<string> {\n // The upstream CDN fallback contains a literal script tag inside a JS string.\n // Local D3 is required here, so remove that loader before inspecting HTML tags.\n html = html.replace(/<script>\\s*window\\.d3\\s*\\|\\|\\s*document\\.write\\([\\s\\S]*?\\);\\s*<\\/script>/gi, '');\n const scripts = [...html.matchAll(/<script\\b([^>]*?)\\bsrc=[\"']([^\"']+)[\"']([^>]*)>[\\s\\S]*?<\\/script>/gi)];\n for (const match of scripts) {\n const path = match[2];\n if (/^(?:https?:|data:|\\/|\\\\)|\\.\\./i.test(path)) throw new Error('Graph contains a nonlocal script');\n const script = await read(path);\n html = html.replace(match[0], () => `<script>${script.replace(/<\\/script/gi, '<\\\\/script')}</script>`);\n }\n const policy = `<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'; font-src 'none'\">`;\n return html.replace(/<head>/i, `<head>${policy}`);\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAmD;;;ACCnD,eAAsB,YAAY,MAAc,MAA0D;AAGxG,SAAO,KAAK,QAAQ,8EAA8E,EAAE;AACpG,QAAM,UAAU,CAAC,GAAG,KAAK,SAAS,qEAAqE,CAAC;AACxG,aAAW,SAAS,SAAS;AAC3B,UAAM,OAAO,MAAM,CAAC;AACpB,QAAI,iCAAiC,KAAK,IAAI,EAAG,OAAM,IAAI,MAAM,kCAAkC;AACnG,UAAM,SAAS,MAAM,KAAK,IAAI;AAC9B,WAAO,KAAK,QAAQ,MAAM,CAAC,GAAG,MAAM,WAAW,OAAO,QAAQ,eAAe,YAAY,CAAC,YAAW;AAAA,EACvG;AACA,QAAM,SAAS;AACf,SAAO,KAAK,QAAQ,WAAW,SAAS,MAAM,EAAE;AAClD;;;ADLO,IAAM,SAAS,CAAC,SAAS,oBAAoB,UAAU,yBAAyB,iBAAiB;AACjG,IAAM,OAAO;AAOpB,eAAsB,UAAU,QAAyB,SAAiB,MAAc,QAAqB,MAAgC;AAC3I,QAAM,SAAS,OAAO,YAAY,MAAM,OAAO,UAAU,SAAS,MAAM,OAAO,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,MAAM,IAC1G,QAAQ,OAAO,cAAc,MAAM,OAAO,YAAY,SAAS,MAAM,MAAM,MAAM,IACjF,OAAO,UAAU,MAAM,OAAO,QAAQ,SAAS,MAAM,MAAM,KAAK,MAAM;AAAE,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAAG,GAAG;AAC/H,MAAI,CAAC,OAAO,GAAI,OAAM,IAAI,MAAM,OAAO,MAAM,OAAO;AACpD,MAAI,OAAO,MAAM,gBAAgB,WAAY,QAAO,IAAI,YAAY,EAAE,OAAO,OAAO,MAAM,IAAI;AAC9F,QAAM,SAAS,OAAO,MAAM;AAC5B,MAAI,OAAO,WAAW,SAAU,OAAM,IAAI,MAAM,8BAA8B;AAC9E,SAAO,IAAI,YAAY,EAAE,OAAO,WAAW,KAAK,KAAK,MAAM,GAAG,OAAK,EAAE,WAAW,CAAC,CAAC,CAAC;AACrF;AAEO,SAAS,MAAM,KAAoB;AACxC,QAAM,KAAK;AACX,MAAI,OAAO,MAAM,IAAI,iBAAiB,SAAS,EAAE,IAAI,MAAM,qBAAqB,UAAU,WAAW,OAAO,MAAM,yCAAqB,OAAO,CAAC,EAAE,IAAI,SAAS,OAAO,IAAI,OAAO,MAAM,yCAAqB,aAAa,MAAM,6DAA6D,CAAC,EAAE,CAAC,CAAC;AAChS,WAAS,MAAM,EAAE,WAAW,WAAW,GAA8E;AACnH,UAAM,EAAE,IAAI,IAAI,WAAW;AAC3B,UAAM,CAAC,MAAM,OAAO,QAAI,uBAAS,EAAE;AACnC,UAAM,CAAC,OAAO,QAAQ,QAAI,uBAAS,EAAE;AACrC,UAAM,CAAC,QAAQ,SAAS,QAAI,uBAAS,EAAE;AACvC,UAAM,CAAC,MAAM,OAAO,QAAI,uBAAS,KAAK;AACtC,UAAM,gBAAY,qBAAwB;AAC1C,UAAM,CAAC,YAAY,OAAO,QAAI,uBAAS,CAAC;AACxC,gCAAU,MAAM;AACd,cAAQ,EAAE;AAAG,gBAAU,EAAE;AAAG,eAAS,EAAE;AAAG,cAAQ,KAAK;AACvD,aAAO,MAAM;AAAE,kBAAU,SAAS,MAAM;AAAG,kBAAU,UAAU;AAAA,MAAW;AAAA,IAC5E,GAAG,CAAC,WAAW,IAAI,MAAM,CAAC;AAC1B,UAAM,UAAU,OAAO,YAAuC;AAC5D,UAAI,UAAU,QAAS;AACvB,YAAM,UAAU,IAAI,gBAAgB;AAAG,gBAAU,UAAU;AAC3D,YAAM,SAAS,YAAY,IAAI,CAAC,QAAQ,QAAQ,IAAI,MAAM,CAAC;AAC3D,cAAQ,IAAI;AAAG,eAAS,EAAE;AAC1B,gBAAU,YAAY,cAAc,gKAA2E,wHAAuD;AACtK,UAAI;AACF,cAAM,SAAS,MAAM,IAAI,OAAO,SAAS,QAAQ,WAAgE,IAAI,OAAO,IAAI,CAAC,GAAG,MAAM;AAC1I,eAAO,eAAe;AACtB,YAAI,CAAC,OAAO,GAAI,OAAM,IAAI,MAAM,OAAO,MAAM,OAAO;AACpD,YAAI,CAAC,OAAO,MAAO,OAAM,IAAI,MAAM,+JAA+E;AAClH,YAAI,OAAO,MAAM,OAAO,SAAS,QAAS,OAAM,IAAI,MAAM,OAAO,MAAM,OAAO,IAAI;AAClF,kBAAU,OAAO,MAAM,OAAO,QAAQ,qBAAW;AACjD,YAAI,YAAY,YAAa,SAAQ,OAAK,IAAI,CAAC;AAAA,MACjD,SAASA,QAAO;AACd,YAAI,UAAU,YAAY,QAAS;AACnC,YAAI,QAAQ,OAAO,QAAS,WAAU,0FAAwC;AAAA,iBACrE,CAAC,IAAI,OAAO,SAAS;AAAE,oBAAU,EAAE;AAAG,mBAAS,OAAOA,MAAK,CAAC;AAAA,QAAG;AAAA,MAC1E,UAAE;AACA,YAAI,UAAU,YAAY,SAAS;AAAE,oBAAU,UAAU;AAAW,kBAAQ,KAAK;AAAA,QAAG;AAAA,MACtF;AAAA,IACF;AACA,UAAM,WAAW,MAAM;AACrB,YAAM,MAAM,IAAI,gBAAgB,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,MAAM,0BAA0B,CAAC,CAAC;AACrF,YAAM,OAAO,SAAS,cAAc,GAAG;AAAG,WAAK,OAAO;AAAK,WAAK,WAAW;AAA0B,WAAK,MAAM;AAChH,iBAAW,MAAM,IAAI,gBAAgB,GAAG,GAAG,GAAI;AAAA,IACjD;AACA,gCAAU,MAAM;AACd,YAAM,OAAO,IAAI,gBAAgB;AACjC,YAAM,SAAS,YAAY,IAAI,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC;AACxD,YAAM,OAAO;AACb,YAAM,SAAS,IAAI,OAAO;AAC1B,eAAS,EAAE;AACX,WAAK,UAAU,QAAQ,WAAW,MAAM,MAAM,EAAE,KAAK,CAAAC,UAAQ,YAAYA,OAAM,UAAQ,UAAU,QAAQ,WAAW,MAAM,QAAQ,IAAI,CAAC,CAAC,EAAE,KAAK,YAAU;AACvJ,YAAI,CAAC,OAAO,QAAS,SAAQ,MAAM;AAAA,MACrC,CAAC,EAAE,MAAM,CAAAD,WAAS;AAAE,YAAI,CAAC,OAAO,QAAS,UAAS,OAAOA,MAAK,CAAC;AAAA,MAAG,CAAC;AACnE,aAAO,MAAM,KAAK,MAAM;AAAA,IAC1B,GAAG,CAAC,WAAW,YAAY,IAAI,MAAM,CAAC;AACtC,WAAO,6BAAAE,QAAA,cAAC,aAAQ,OAAO,EAAE,QAAQ,QAAQ,SAAS,QAAQ,eAAe,UAAU,KAAK,EAAE,KACxF,6BAAAA,QAAA,cAAC,aACC,6BAAAA,QAAA,cAAC,YAAO,MAAK,UAAS,UAAU,MAAM,SAAS,MAAM,KAAK,QAAQ,WAAW,KAAI,OAAO,4CAAwB,oFAAwC,GAAU,KAClK,6BAAAA,QAAA,cAAC,YAAO,MAAK,UAAS,UAAU,MAAM,SAAS,MAAM,KAAK,QAAQ,WAAW,KAAG,0CAAoB,GAAU,KAC7G,QAAQ,6BAAAA,QAAA,cAAC,YAAO,MAAK,UAAS,SAAS,MAAM,UAAU,SAAS,MAAM,KAAG,uBAAW,GAAW,KAChG,6BAAAA,QAAA,cAAC,YAAO,MAAK,UAAS,UAAU,MAAM,SAAS,MAAM,QAAQ,OAAK,IAAI,CAAC,KAAG,oCAAc,GAAU,KAClG,6BAAAA,QAAA,cAAC,YAAO,MAAK,UAAS,UAAU,CAAC,MAAM,SAAS,YAAU,iCAAqB,CACjF,GACC,CAAC,QAAQ,CAAC,QAAQ,6BAAAA,QAAA,cAAC,WAAE,2cAAyN,GAC9O,UAAU,6BAAAA,QAAA,cAAC,OAAE,MAAK,UAAS,OAAO,EAAE,YAAY,WAAW,KAAI,MAAO,GACtE,SAAS,6BAAAA,QAAA,cAAC,aAAQ,MAAI,QAAC,6BAAAA,QAAA,cAAC,iBAAQ,4EAA6B,GAAU,6BAAAA,QAAA,cAAC,OAAE,MAAK,SAAQ,OAAO,EAAE,YAAY,WAAW,KAAI,KAAM,CAAI,GACrI,QAAQ,6BAAAA,QAAA,cAAC,YAAO,OAAM,iEAAkC,SAAQ,iCAAgC,QAAQ,MAAM,OAAO,EAAE,OAAO,QAAQ,MAAM,GAAG,QAAQ,EAAE,GAAG,CAC/J;AAAA,EACF;AACA,MAAI,OAAO,MAAM,IAAI,MAAM,OAAO,0BAA0B,MAAM,IAAI,MAAM,SAAS,EAAE,MAAM,0BAA0B,KAAK,GAAG,GAAG,KAAK,CAAC,CAAC;AAC3I;",
6
+ "names": ["error", "html", "React"]
7
7
  }
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/index.ts
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { Ajv } from "ajv";
4
- import { readFile, readdir as readdir2, stat as stat2 } from "node:fs/promises";
5
- import { join as join2, basename as basename2 } from "node:path";
6
- import { homedir } from "node:os";
4
+ import { readFile as readFile2, readdir as readdir2, stat as stat2 } from "node:fs/promises";
5
+ import { join as join3, basename as basename2 } from "node:path";
6
+ import { homedir as homedir2 } from "node:os";
7
7
 
8
8
  // src/backend.ts
9
9
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -75,25 +75,25 @@ async function connect(engine, cwd) {
75
75
  const transport = new StdioClientTransport({ command: engine.command, args: [...engine.args, "serve", "--repo", cwd], cwd, env: environment(engine), stderr: "pipe" });
76
76
  transport.stderr?.on("data", () => {
77
77
  });
78
- const client = new Client({ name: "dsh-code-review-graph", version: "0.1.1" });
78
+ const client = new Client({ name: "dsh-code-review-graph", version: "0.1.2" });
79
79
  try {
80
80
  await client.connect(transport);
81
81
  return client;
82
82
  } catch (error) {
83
83
  await transport.close();
84
- throw new Error(`CRG engine unavailable. Run dsh-crg prepare explicitly. ${String(error)}`);
84
+ throw new Error(`CRG engine unavailable. Open Code graph and click Set up engine, or run /crg-setup (CLI: dsh-crg prepare). ${String(error)}`);
85
85
  }
86
86
  }
87
87
  async function stop(child) {
88
88
  if (!child || child.exitCode !== null || child.signalCode !== null) return;
89
- await new Promise((resolve2) => {
89
+ await new Promise((resolve3) => {
90
90
  const timer = setTimeout(() => {
91
91
  child.kill("SIGKILL");
92
- resolve2();
92
+ resolve3();
93
93
  }, 3e3);
94
94
  child.once("exit", () => {
95
95
  clearTimeout(timer);
96
- resolve2();
96
+ resolve3();
97
97
  });
98
98
  child.kill();
99
99
  });
@@ -161,8 +161,8 @@ var BackendPool = class {
161
161
  signal.throwIfAborted();
162
162
  const watcher = spawn(this.engine.command, [...this.engine.args, "watch", "--repo", root], { cwd: root, env: environment(this.engine), stdio: "ignore", windowsHide: true });
163
163
  entry.watcher = watcher;
164
- await new Promise((resolve2, reject) => {
165
- watcher.once("spawn", resolve2);
164
+ await new Promise((resolve3, reject) => {
165
+ watcher.once("spawn", resolve3);
166
166
  watcher.once("error", reject);
167
167
  });
168
168
  watcher.on("error", (error) => {
@@ -212,7 +212,7 @@ var BackendPool = class {
212
212
  async function run(engine, args, cwd, signal) {
213
213
  await checkedData(cwd);
214
214
  signal = AbortSignal.any([signal, AbortSignal.timeout(engine.timeoutMs)]);
215
- return new Promise((resolve2, reject) => {
215
+ return new Promise((resolve3, reject) => {
216
216
  let log = "";
217
217
  const child = spawn(engine.command, [...engine.args, ...args], { cwd, env: environment(engine), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, signal });
218
218
  const capture = (bytes) => {
@@ -221,7 +221,7 @@ async function run(engine, args, cwd, signal) {
221
221
  child.stdout?.on("data", capture);
222
222
  child.stderr?.on("data", capture);
223
223
  child.on("error", reject);
224
- child.on("exit", (code) => code === 0 ? resolve2({ log }) : reject(new Error(`Engine command exited ${code}: ${log}`)));
224
+ child.on("exit", (code) => code === 0 ? resolve3({ log }) : reject(new Error(`Engine command exited ${code}: ${log}`)));
225
225
  });
226
226
  }
227
227
 
@@ -291,12 +291,99 @@ function exposed(workflow, names) {
291
291
  return workflow === "all" ? names : names.filter((name2) => members[workflow].includes(name2));
292
292
  }
293
293
 
294
+ // src/setup.ts
295
+ import { spawn as spawn2 } from "node:child_process";
296
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
297
+ import { homedir } from "node:os";
298
+ import { join as join2, resolve as resolve2 } from "node:path";
299
+ import { fileURLToPath } from "node:url";
300
+ var environment2 = join2(process.env.DSH_HOME ?? join2(homedir(), ".dsh"), "code-review-graph", "engine");
301
+ var pythonIn = (dir) => join2(dir, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
302
+ async function command(exe, args, quiet = false, options = {}) {
303
+ options.signal?.throwIfAborted();
304
+ await new Promise((resolve3, reject) => {
305
+ let log = "";
306
+ const child = spawn2(exe, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true, signal: options.signal });
307
+ const capture = (bytes) => {
308
+ const text = bytes.toString();
309
+ log = (log + text).slice(-16e3);
310
+ if (!quiet) options.onProgress?.(text);
311
+ };
312
+ child.stdout.on("data", capture);
313
+ child.stderr.on("data", capture);
314
+ child.on("error", reject);
315
+ child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`${exe} exited ${code}: ${log}`)));
316
+ });
317
+ }
318
+ async function prepare(source, python, options = {}) {
319
+ options = { ...options, signal: AbortSignal.any([...options.signal ? [options.signal] : [], AbortSignal.timeout(15 * 60 * 1e3)]) };
320
+ options.signal.throwIfAborted();
321
+ const execute = (exe, args, quiet = false) => command(exe, args, quiet, options);
322
+ const engine = JSON.parse(await readFile(new URL("../engine.json", import.meta.url), "utf8"));
323
+ const pinned = source ?? engine.source;
324
+ const stamp = join2(environment2, "dsh-crg-source.json");
325
+ if (await readFile(stamp, "utf8").then((value) => JSON.parse(value).source === pinned, () => false)) {
326
+ try {
327
+ await execute(pythonIn(environment2), ["-c", "from code_review_graph.refactor import get_refactor_edit_plan"], true);
328
+ options.onProgress?.("CRG engine is already prepared.");
329
+ return;
330
+ } catch {
331
+ options.signal.throwIfAborted();
332
+ }
333
+ }
334
+ await mkdir(environment2, { recursive: true });
335
+ let uv = false;
336
+ try {
337
+ await execute("uv", ["--version"], true);
338
+ uv = true;
339
+ } catch {
340
+ options.signal.throwIfAborted();
341
+ }
342
+ if (uv && !python) {
343
+ await execute("uv", ["venv", "--allow-existing", "--python", ">=3.10", environment2]);
344
+ await execute("uv", ["pip", "install", "--python", pythonIn(environment2), pinned]);
345
+ } else {
346
+ let selected;
347
+ for (const candidate of python ? [{ exe: python, args: [] }] : [{ exe: "python3", args: [] }, { exe: "python", args: [] }, { exe: "py", args: ["-3"] }]) {
348
+ try {
349
+ await execute(candidate.exe, [...candidate.args, "-c", "import sys; assert sys.version_info >= (3,10)"], true);
350
+ selected = candidate;
351
+ break;
352
+ } catch {
353
+ options.signal.throwIfAborted();
354
+ }
355
+ }
356
+ if (!selected) throw new Error("Install uv or real Python 3.10+; Windows Store aliases are not a Python runtime.");
357
+ await execute(selected.exe, [...selected.args, "-m", "venv", environment2]);
358
+ await execute(pythonIn(environment2), ["-m", "pip", "install", pinned]);
359
+ }
360
+ await execute(pythonIn(environment2), ["-c", "from code_review_graph.refactor import get_refactor_edit_plan"], true);
361
+ options.signal.throwIfAborted();
362
+ await writeFile(stamp, JSON.stringify({ source: pinned }), "utf8");
363
+ }
364
+ if (process.argv[1] && resolve2(fileURLToPath(import.meta.url)) === resolve2(process.argv[1])) {
365
+ if (process.argv[2] !== "prepare") {
366
+ console.error("Usage: dsh-crg prepare [engine-source] [--python executable]");
367
+ process.exitCode = 2;
368
+ } else {
369
+ const args = process.argv.slice(3);
370
+ const flag = args.indexOf("--python");
371
+ const python = flag >= 0 ? args.splice(flag, 2)[1] : void 0;
372
+ try {
373
+ await prepare(args[0], python, { onProgress: (text) => process.stdout.write(text.endsWith("\n") ? text : text + "\n") });
374
+ } catch (error) {
375
+ console.error(String(error));
376
+ process.exitCode = 1;
377
+ }
378
+ }
379
+ }
380
+
294
381
  // src/index.ts
295
382
  var name = "code-review-graph";
296
383
  var inject = ["tools", "agents", "fs", "sandboxPolicy", "skills", "systemPrompt"];
297
- var engineDir = join2(process.env.DSH_HOME ?? join2(homedir(), ".dsh"), "code-review-graph", "engine");
384
+ var engineDir = join3(process.env.DSH_HOME ?? join3(homedir2(), ".dsh"), "code-review-graph", "engine");
298
385
  var Config = z.object({
299
- engineCommand: z.string().default(join2(engineDir, process.platform === "win32" ? "Scripts/code-review-graph.exe" : "bin/code-review-graph")),
386
+ engineCommand: z.string().default(join3(engineDir, process.platform === "win32" ? "Scripts/code-review-graph.exe" : "bin/code-review-graph")),
300
387
  engineArgs: z.array(z.string()).default([]),
301
388
  timeoutMs: z.number().min(1e3).default(12e4),
302
389
  authorizedRepos: z.array(z.string()).default([]),
@@ -310,7 +397,7 @@ var output = {
310
397
  async function apply(ctx, config) {
311
398
  const engine = { command: config.engineCommand, args: config.engineArgs, timeoutMs: config.timeoutMs, env: Object.fromEntries(config.embeddingEnv.filter((key) => /^(OPENAI|GOOGLE|GEMINI|MINIMAX|VOYAGE|CRG)_/.test(key) && process.env[key] !== void 0).map((key) => [key, process.env[key]])) };
312
399
  const pool = new BackendPool(engine);
313
- const snapshot = JSON.parse(await readFile(new URL("../capabilities.json", import.meta.url), "utf8"));
400
+ const snapshot = JSON.parse(await readFile2(new URL("../capabilities.json", import.meta.url), "utf8"));
314
401
  const tools = snapshot.tools.filter((t) => t.name !== "get_refactor_edit_plan_tool");
315
402
  const names = tools.map((t) => t.name);
316
403
  const masks = /* @__PURE__ */ new Map();
@@ -358,7 +445,7 @@ async function apply(ctx, config) {
358
445
  if (selected.some((n) => !aliases.some((r) => r.alias === n))) throw new Error("Repository is not authorized");
359
446
  const results = [];
360
447
  for (const { path: repo, alias } of aliases.filter((r) => !selected.length || selected.includes(r.alias))) {
361
- if (repo !== root && !await stat2(join2(repo, ".code-review-graph/graph.db")).then(() => true, () => false)) throw new Error(`Build ${repo} in its own session first`);
448
+ if (repo !== root && !await stat2(join3(repo, ".code-review-graph/graph.db")).then(() => true, () => false)) throw new Error(`Build ${repo} in its own session first`);
362
449
  await pool.ensure(repo, owner, signal);
363
450
  const data = valueOf(await pool.call(repo, owner, "semantic_search_nodes_tool", { query: args.query, kind: args.kind, limit: args.limit ?? 20, provider: "local", repo_root: repo }, signal));
364
451
  for (const result2 of data.results) results.push({ ...result2, repo: alias });
@@ -430,6 +517,46 @@ async function apply(ctx, config) {
430
517
  max_nodes: { type: "integer", minimum: 1 }
431
518
  }, additionalProperties: false };
432
519
  const validateVisualization = ajv.compile(visualizationSchema);
520
+ async function visualize(args, agent, signal) {
521
+ const root = await repository(agent.session.header.cwd);
522
+ signal = AbortSignal.any([signal, life.signal]);
523
+ const graphUpdate = await pool.ensure(root, agent.id, signal);
524
+ for (const path of args.seed_file ?? []) await checkedPath(root, path, true);
525
+ const command2 = ["visualize", "--repo", root];
526
+ for (const [key, value] of Object.entries(args)) {
527
+ const flag = `--${key.replaceAll("_", "-")}`;
528
+ if (Array.isArray(value)) for (const item of value) command2.push(flag, String(item));
529
+ else if (value === true) command2.push(flag);
530
+ else if (value !== void 0 && value !== false) command2.push(flag, String(value));
531
+ }
532
+ const { log } = await run(engine, command2, root, signal);
533
+ const format = String(args.format ?? "html");
534
+ return { status: "ok", path: join3(root, ".code-review-graph", format === "obsidian" ? "obsidian" : `graph.${format}`), ...format === "html" ? { sidebar: "code-review-graph" } : {}, ...graphUpdate ? { graph_update: graphUpdate } : {}, log };
535
+ }
536
+ let preparation;
537
+ const prepareDefault = (signal) => {
538
+ if (config.engineCommand !== join3(engineDir, process.platform === "win32" ? "Scripts/code-review-graph.exe" : "bin/code-review-graph") || config.engineArgs.length) {
539
+ throw new Error("A custom engine is configured. Prepare that engine, or restore the default engine configuration. / \u5F53\u524D\u914D\u7F6E\u4E86\u81EA\u5B9A\u4E49\u5F15\u64CE\uFF0C\u8BF7\u51C6\u5907\u8BE5\u5F15\u64CE\u6216\u6062\u590D\u9ED8\u8BA4\u914D\u7F6E\u3002");
540
+ }
541
+ return preparation ??= prepare(void 0, void 0, { signal: AbortSignal.any([signal, life.signal]) }).finally(() => {
542
+ preparation = void 0;
543
+ });
544
+ };
545
+ ctx.inject(["commands"], (scope) => {
546
+ const commands = scope.get("commands");
547
+ scope.effect(() => commands.register({ name: "crg-setup", description: "\u51C6\u5907\u672C\u5730\u56FE\u8C31\u5F15\u64CE / Prepare the local graph engine", handler: async ({ signal, rawInput }) => {
548
+ if (rawInput.trim()) throw new Error("/crg-setup does not need parameters / \u65E0\u9700\u586B\u5199\u53C2\u6570");
549
+ await prepareDefault(signal);
550
+ return { kind: "success", text: "\u56FE\u8C31\u5F15\u64CE\u5DF2\u51C6\u5907\u597D\u3002\u6253\u5F00\u53F3\u4FA7\u4EE3\u7801\u56FE\u8C31\uFF0C\u70B9\u51FB\u751F\u6210\u56FE\u8C31\u3002 / Engine ready. Open Code graph and click Generate graph." };
551
+ } }));
552
+ scope.effect(() => commands.register({ name: "crg-graph", description: "\u751F\u6210\uFF0F\u66F4\u65B0\u4EA4\u4E92\u4EE3\u7801\u56FE\u8C31\uFF0C\u65E0\u9700\u6A21\u578B / Generate or update the interactive code graph without a model", handler: async ({ agent, signal, rawInput }) => {
553
+ if (rawInput.trim()) throw new Error("/crg-graph does not need parameters / \u65E0\u9700\u586B\u5199\u53C2\u6570");
554
+ await repository(agent.session.header.cwd);
555
+ if (config.engineCommand === join3(engineDir, process.platform === "win32" ? "Scripts/code-review-graph.exe" : "bin/code-review-graph") && !config.engineArgs.length) await prepareDefault(signal);
556
+ const result = await visualize({ mode: "full", format: "html" }, agent, signal);
557
+ return { kind: "success", text: `\u56FE\u8C31\u5DF2\u751F\u6210 / Graph generated: ${result.path}${result.graph_update ? "\n\u89E3\u6790\u8B66\u544A / Parse warnings: " + JSON.stringify(result.graph_update) : ""}` };
558
+ } }));
559
+ });
433
560
  ctx.effect(() => ctx.tools.register({
434
561
  name: "crg_visualize",
435
562
  description: "Generate the local interactive graph for the DSH Code graph sidebar, or export HTML, JSON, GraphML, Cypher, Obsidian or SVG. Supports file, community, symbol, changed-code, flow and shortest-path views.",
@@ -438,21 +565,8 @@ async function apply(ctx, config) {
438
565
  timeoutMs: config.timeoutMs,
439
566
  execute: async (value, exec) => {
440
567
  if (!validateVisualization(value)) throw new Error(ajv.errorsText(validateVisualization.errors));
441
- const args = value;
442
- const root = await rootFor(exec);
443
- const signal = AbortSignal.any([exec.signal, life.signal]);
444
- await pool.ensure(root, exec.agent.id, signal);
445
- for (const path of args.seed_file ?? []) await checkedPath(root, path, true);
446
- const command = ["visualize", "--repo", root];
447
- for (const [key, value2] of Object.entries(args)) {
448
- const flag = `--${key.replaceAll("_", "-")}`;
449
- if (Array.isArray(value2)) for (const item of value2) command.push(flag, String(item));
450
- else if (value2 === true) command.push(flag);
451
- else if (value2 !== void 0 && value2 !== false) command.push(flag, String(value2));
452
- }
453
- const { log } = await run(engine, command, root, signal);
454
- const format = String(args.format ?? "html");
455
- return { status: "ok", path: join2(root, ".code-review-graph", format === "obsidian" ? "obsidian" : `graph.${format}`), ...format === "html" ? { sidebar: "code-review-graph" } : {}, log };
568
+ if (!exec.agent) throw new Error("Graph tools require a session-owned agent");
569
+ return visualize(value, exec.agent, exec.signal);
456
570
  }
457
571
  }));
458
572
  const promptNames = ["review_changes", "architecture_map", "debug_issue", "onboard_developer", "pre_merge_check"];
@@ -488,12 +602,12 @@ async function apply(ctx, config) {
488
602
  name: "code-review-graph",
489
603
  order: 80,
490
604
  interpolate: false,
491
- text: "Use crg_workflow to reveal task-specific local code graph tools, then get_minimal_context_tool before broader queries. Prefer minimal detail. crg_visualize opens the local graph in the Code graph sidebar. Refactors use session filesystem permissions."
605
+ text: "Use crg_workflow to reveal task-specific local code graph tools, then get_minimal_context_tool before broader queries. Prefer minimal detail. crg_visualize generates a local graph for the Code graph sidebar; users can also generate it directly from that page without a model. Refactors use session filesystem permissions."
492
606
  }));
493
607
  const skillsDir = new URL("../skills/", import.meta.url);
494
608
  for (const slug of await readdir2(skillsDir)) {
495
609
  const path = new URL(`${slug}/SKILL.md`, skillsDir);
496
- const body = await readFile(path, "utf8");
610
+ const body = await readFile2(path, "utf8");
497
611
  const content = body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").replace(/\b([a-z_]+_tool)\b/g, (raw) => names.includes(raw) ? publicName(raw) : raw);
498
612
  if (!skillWorkflow[slug]) throw new Error(`Unmapped CRG skill: ${slug}`);
499
613
  ctx.effect(() => ctx.skills.register({ name: `crg-${slug}`, description: `Code review graph: ${slug}`, source: "bundled", content: `First call crg_workflow with workflow "${skillWorkflow[slug]}". Tool names use mcp__code_review_graph__ prefixes.