deepseek-coordination 0.14.2 → 0.14.3

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.
package/lib/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { appendFileSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
3
+ import { readdir } from "node:fs/promises";
3
4
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
4
5
  import Schema$1 from "@deepseek-ai/schemastery";
5
6
  import { Service } from "@deepseek-ai/cordis";
@@ -15803,6 +15804,11 @@ const ZH = {
15803
15804
  rebuildDone: "重建完成:{files} 个文件,{nodes} 个节点,用时 {ms} 毫秒",
15804
15805
  rebuildPartial: "已重建(部分)",
15805
15806
  actionFailed: "操作失败({code})",
15807
+ indexWorkspace: "索引工作区",
15808
+ indexHint: "工作区还没有索引。索引后可浏览文件、大纲与依赖关系。",
15809
+ indexBusy: "正在索引工作区,请稍候",
15810
+ indexDone: "索引完成:{indexed} 个文件,{nodes} 个节点,用时 {ms} 毫秒",
15811
+ indexPartial: "已索引部分文件(达到上限)",
15806
15812
  tabs: {
15807
15813
  files: "文件",
15808
15814
  outline: "大纲",
@@ -15899,6 +15905,7 @@ const ZH = {
15899
15905
  fit: "适应画布",
15900
15906
  resetLayout: "重置节点位置",
15901
15907
  rebuilding: "正在重建…",
15908
+ indexWorkspace: "扫描工作区文件并建立索引(只读)",
15902
15909
  agentStatus: "代理状态",
15903
15910
  tabAgents: "查看本会话代理",
15904
15911
  agentExpand: "展开子代理",
@@ -16777,6 +16784,11 @@ const COPY_BY_LOCALE = {
16777
16784
  rebuildDone: "Rebuilt {files} files and {nodes} nodes in {ms} ms",
16778
16785
  rebuildPartial: "Rebuilt partially",
16779
16786
  actionFailed: "Action failed ({code})",
16787
+ indexWorkspace: "Index workspace",
16788
+ indexHint: "This workspace has no index yet. Index it to browse files, outlines and dependencies.",
16789
+ indexBusy: "Indexing the workspace; please wait.",
16790
+ indexDone: "Indexed {indexed} files and {nodes} nodes in {ms} ms",
16791
+ indexPartial: "Indexed partially (limit reached)",
16780
16792
  tabs: {
16781
16793
  files: "Files",
16782
16794
  outline: "Outline",
@@ -16873,6 +16885,7 @@ const COPY_BY_LOCALE = {
16873
16885
  fit: "Fit to canvas",
16874
16886
  resetLayout: "Reset node positions",
16875
16887
  rebuilding: "Rebuilding…",
16888
+ indexWorkspace: "Scan workspace files and build the index (read-only)",
16876
16889
  agentStatus: "Agent status",
16877
16890
  tabAgents: "View this session’s agents",
16878
16891
  agentExpand: "Expand sub-agents",
@@ -68563,6 +68576,18 @@ var require_underline = /* @__PURE__ */ __commonJSMin(((exports) => {
68563
68576
  * No Office byte is ever written to disk, no external process is started and
68564
68577
  * no converted body is stored or cached.
68565
68578
  *
68579
+ * # Bounded workspace indexing (`POST /graph/index`)
68580
+ *
68581
+ * A fresh profile has an empty graph: the file tools index only what they are
68582
+ * asked to parse. {@link indexWorkspaceFiles} is the scan half of the explicit
68583
+ * indexing entry point and is still pure orchestration — the host injects a
68584
+ * read-only {@link WorkspaceIndexIo} that binds it to `node:fs`, `ParseService`
68585
+ * and `GraphStore`/`ChainEngine`. The scan never descends into
68586
+ * `node_modules`/`.git`/`dist`/`.tmp`, never attempts a binary extension, is
68587
+ * capped at {@link MAX_INDEX_FILES} files / {@link INDEX_TIME_LIMIT_MS} ms and
68588
+ * reports `truncated` when a bound stopped it. {@link WorkspaceIndexGate}
68589
+ * serializes concurrent runs (the route answers 409 `BUSY`).
68590
+ *
68566
68591
  * # Two-level cache (dependencies / imports / intra)
68567
68592
  *
68568
68593
  * Per-file facts (`imports`, `exports`, `deps`) are resolved through:
@@ -71390,6 +71415,173 @@ const Color = {
71390
71415
  Gray: 1,
71391
71416
  Black: 2
71392
71417
  };
71418
+ /** Directories the scan never descends into (matched case-insensitively). */
71419
+ const INDEX_SKIP_DIRECTORIES = [
71420
+ "node_modules",
71421
+ ".git",
71422
+ "dist",
71423
+ ".tmp"
71424
+ ];
71425
+ /**
71426
+ * Extensions the scan accepts, mirroring the registered text parsers.
71427
+ *
71428
+ * Deliberately an allowlist: shared `.json`/`.yaml`/`.xml` extensions have no
71429
+ * registered parser (the text layer refuses to guess), and binary documents
71430
+ * (`.docx`, `.pdf`, images, archives) stay out by construction.
71431
+ */
71432
+ const INDEX_FILE_EXTENSIONS = [
71433
+ ".ts",
71434
+ ".tsx",
71435
+ ".mts",
71436
+ ".cts",
71437
+ ".js",
71438
+ ".jsx",
71439
+ ".mjs",
71440
+ ".cjs",
71441
+ ".java",
71442
+ ".py",
71443
+ ".pyi",
71444
+ ".pyw",
71445
+ ".rs",
71446
+ ".php",
71447
+ ".c",
71448
+ ".h",
71449
+ ".cc",
71450
+ ".cpp",
71451
+ ".hpp",
71452
+ ".cxx",
71453
+ ".hh",
71454
+ ".md",
71455
+ ".markdown",
71456
+ ".mdx",
71457
+ ".tex",
71458
+ ".ltx",
71459
+ ".latex",
71460
+ ".book",
71461
+ ".css",
71462
+ ".html",
71463
+ ".htm",
71464
+ ".csv",
71465
+ ".tsv"
71466
+ ];
71467
+ /** True when a workspace-relative path has an extension the text layer parses. */
71468
+ function isIndexableWorkspaceFile(path) {
71469
+ const extension = extensionOf(path);
71470
+ return extension.length > 0 && INDEX_FILE_EXTENSIONS.includes(extension);
71471
+ }
71472
+ function isSkippedWorkspaceDirectory(name) {
71473
+ return INDEX_SKIP_DIRECTORIES.includes(name.toLowerCase());
71474
+ }
71475
+ function joinWorkspacePath(directory, name) {
71476
+ return directory.length === 0 ? name : `${directory}/${name}`;
71477
+ }
71478
+ /**
71479
+ * Scan the workspace and index every accepted text file.
71480
+ *
71481
+ * Breadth-first and deterministic: each directory's entries are sorted by
71482
+ * code-point name before they are visited, so a capped run always indexes the
71483
+ * same prefix of the workspace. `indexed` counts successful parses, `skipped`
71484
+ * counts files that were seen but not indexed (extension or parse failure);
71485
+ * directories are never counted. A directory that cannot be listed is skipped
71486
+ * without aborting the scan.
71487
+ */
71488
+ async function indexWorkspaceFiles(io, options = {}) {
71489
+ const maxFiles = Math.max(0, Math.floor(options.maxFiles ?? 200));
71490
+ const timeLimitMs = Math.max(0, Math.floor(options.timeLimitMs ?? 1e4));
71491
+ const maxDepth = Math.max(1, Math.floor(options.maxDepth ?? 8));
71492
+ const now = options.now ?? Date.now;
71493
+ const startedAt = now();
71494
+ let indexed = 0;
71495
+ let skipped = 0;
71496
+ let nodes = 0;
71497
+ let attempted = 0;
71498
+ let truncated = false;
71499
+ const pending = [{
71500
+ path: "",
71501
+ depth: 0
71502
+ }];
71503
+ scan: while (pending.length > 0) {
71504
+ if (now() - startedAt >= timeLimitMs) {
71505
+ truncated = true;
71506
+ break;
71507
+ }
71508
+ const directory = pending.shift();
71509
+ let entries;
71510
+ try {
71511
+ entries = await io.readDirectory(directory.path);
71512
+ } catch {
71513
+ continue;
71514
+ }
71515
+ const ordered = [...entries].sort((left, right) => compareText(left.name, right.name));
71516
+ for (const entry of ordered) {
71517
+ if (now() - startedAt >= timeLimitMs) {
71518
+ truncated = true;
71519
+ break scan;
71520
+ }
71521
+ if (entry.directory) {
71522
+ if (directory.depth + 1 >= maxDepth) continue;
71523
+ if (isSkippedWorkspaceDirectory(entry.name)) continue;
71524
+ pending.push({
71525
+ path: joinWorkspacePath(directory.path, entry.name),
71526
+ depth: directory.depth + 1
71527
+ });
71528
+ continue;
71529
+ }
71530
+ const path = joinWorkspacePath(directory.path, entry.name);
71531
+ if (!isIndexableWorkspaceFile(path)) {
71532
+ skipped += 1;
71533
+ continue;
71534
+ }
71535
+ if (attempted >= maxFiles) {
71536
+ truncated = true;
71537
+ break scan;
71538
+ }
71539
+ attempted += 1;
71540
+ try {
71541
+ const count = await io.indexFile(path);
71542
+ indexed += 1;
71543
+ nodes += Number.isFinite(count) && count > 0 ? Math.floor(count) : 0;
71544
+ } catch {
71545
+ skipped += 1;
71546
+ }
71547
+ }
71548
+ }
71549
+ return {
71550
+ indexed,
71551
+ skipped,
71552
+ nodes,
71553
+ durationMs: Math.max(0, now() - startedAt),
71554
+ truncated
71555
+ };
71556
+ }
71557
+ /**
71558
+ * Serialize workspace indexes in one host process.
71559
+ *
71560
+ * The flag flips synchronously before the task's first `await`, so a second
71561
+ * request in the same tick answers BUSY instead of queuing behind the scan.
71562
+ * The `finally` reset means a thrown task never leaves the gate stuck.
71563
+ */
71564
+ var WorkspaceIndexGate = class {
71565
+ active = false;
71566
+ isBusy() {
71567
+ return this.active;
71568
+ }
71569
+ async run(task) {
71570
+ if (this.active) return {
71571
+ ok: false,
71572
+ busy: true
71573
+ };
71574
+ this.active = true;
71575
+ try {
71576
+ return {
71577
+ ok: true,
71578
+ value: await task()
71579
+ };
71580
+ } finally {
71581
+ this.active = false;
71582
+ }
71583
+ }
71584
+ };
71393
71585
  //#endregion
71394
71586
  //#region src/index.ts
71395
71587
  const name = "deepseek-coordination";
@@ -71674,6 +71866,58 @@ async function rebuildGraph(service) {
71674
71866
  };
71675
71867
  }
71676
71868
  /**
71869
+ * Serializes `POST /graph/index`: the flag flips before the first await, so a
71870
+ * concurrent request answers 409 `BUSY` instead of queuing behind the scan.
71871
+ */
71872
+ const indexGate = new WorkspaceIndexGate();
71873
+ /**
71874
+ * Explicit indexing entry point for a profile whose graph is still empty.
71875
+ *
71876
+ * The file tools only index the paths they are asked to parse, so a fresh
71877
+ * workspace legitimately has no graph and the panels show their empty state.
71878
+ * This scan walks the workspace instead: the bounds (depth ≤ 8, accepted
71879
+ * extensions, skipped directories, 200 files / 10 s) live beside the pure
71880
+ * walker in `host/graph-view.ts`; this adapter only binds it to the same
71881
+ * services {@link rebuildGraph} uses.
71882
+ *
71883
+ * Read-only: directories are listed, accepted files are read through
71884
+ * `ParseService`'s budgeted read path, and only the coordination database's own
71885
+ * graph rows are rewritten. No build, command or execution runs, no workspace
71886
+ * file is written, and symlinks are not followed.
71887
+ */
71888
+ async function indexWorkspace(service) {
71889
+ const root = resolveToolWorkspaceRoot(service);
71890
+ const parser = new ParseService({ workspaceRoot: root });
71891
+ const store = new GraphStore(service.db, { workspaceRoot: root });
71892
+ const registry = new NodeRegistry({ persist: { saveNode: (record) => store.saveNode(record) } });
71893
+ const chain = new ChainEngine({ persist: {
71894
+ saveNode: (node) => store.saveChainNode(node),
71895
+ saveEdge: (edge) => store.saveEdge(edge),
71896
+ deleteEdge: (edge) => store.deleteEdge(edge)
71897
+ } });
71898
+ const result = await indexWorkspaceFiles({
71899
+ async readDirectory(path) {
71900
+ const absolute = path.length === 0 ? root : join(root, path);
71901
+ return (await readdir(absolute, { withFileTypes: true })).filter((entry) => !entry.isSymbolicLink() && (entry.isDirectory() || entry.isFile())).map((entry) => ({
71902
+ name: entry.name,
71903
+ directory: entry.isDirectory()
71904
+ }));
71905
+ },
71906
+ async indexFile(path) {
71907
+ const parsed = parser.parse({ path });
71908
+ return store.transaction(() => {
71909
+ store.beginParse(path);
71910
+ const registered = registry.register(parsed);
71911
+ chain.index(parsed);
71912
+ store.saveSnapshot(parsed, { nodeCount: registered.length });
71913
+ return registered.length;
71914
+ });
71915
+ }
71916
+ });
71917
+ resetGraphCache();
71918
+ return result;
71919
+ }
71920
+ /**
71677
71921
  * Nest a flat dotted override map, then merge it over the deployment config.
71678
71922
  *
71679
71923
  * Stored overrides win over the profile patch: the operator changed those paths
@@ -72220,6 +72464,24 @@ function apply(context, configInput) {
72220
72464
  }
72221
72465
  return;
72222
72466
  }
72467
+ if (path === "/graph/index" && method === "POST") {
72468
+ const outcome = await indexGate.run(() => indexWorkspace(service));
72469
+ if (!outcome.ok) {
72470
+ json(response, {
72471
+ ok: false,
72472
+ error: {
72473
+ code: "BUSY",
72474
+ message: "a workspace index is already running"
72475
+ }
72476
+ }, 409);
72477
+ return;
72478
+ }
72479
+ json(response, {
72480
+ ok: true,
72481
+ value: outcome.value
72482
+ });
72483
+ return;
72484
+ }
72223
72485
  if (path === "/graph/rebuild" && method === "POST") {
72224
72486
  if (rebuildInFlight) {
72225
72487
  json(response, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepseek-coordination",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "description": "A dual-face Cordis plugin for DeepSeek Harness: the host half owns a coordination SQLite database, seven model-facing tools and an HTTP surface; the web half mounts five composer dropdowns, a settings catalog and the service-level takeover dialog.",
5
5
  "keywords": [
6
6
  "deepseek-harness",