flavor-code 1.2.11 → 1.2.13

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 (129) hide show
  1. package/README.md +36 -35
  2. package/README.zh-CN.md +36 -35
  3. package/dist/{app-MXWPZBEU.js → app-Z3YWFIBB.js} +5 -3
  4. package/dist/astgraph/db.mjs +237 -0
  5. package/dist/astgraph/extract.mjs +239 -0
  6. package/dist/astgraph/flavor-plugin.json +25 -0
  7. package/dist/astgraph/grammars.mjs +68 -0
  8. package/dist/astgraph/index.js +301 -0
  9. package/dist/astgraph/indexer.mjs +175 -0
  10. package/dist/astgraph/query.mjs +165 -0
  11. package/dist/astgraph/resolve.mjs +93 -0
  12. package/dist/astgraph/vendor/tree-sitter-javascript.wasm +0 -0
  13. package/dist/astgraph/vendor/tree-sitter-tsx.wasm +0 -0
  14. package/dist/astgraph/vendor/tree-sitter-typescript.wasm +0 -0
  15. package/dist/astgraph/vendor/tree-sitter.js +3980 -0
  16. package/dist/astgraph/vendor/tree-sitter.wasm +0 -0
  17. package/dist/astgraph/vendor/zod/LICENSE +21 -0
  18. package/dist/astgraph/vendor/zod/index.js +4 -0
  19. package/dist/astgraph/vendor/zod/locales/index.js +1 -0
  20. package/dist/astgraph/vendor/zod/locales/package.json +7 -0
  21. package/dist/astgraph/vendor/zod/package.json +135 -0
  22. package/dist/astgraph/vendor/zod/v4/classic/checks.js +1 -0
  23. package/dist/astgraph/vendor/zod/v4/classic/coerce.js +17 -0
  24. package/dist/astgraph/vendor/zod/v4/classic/compat.js +31 -0
  25. package/dist/astgraph/vendor/zod/v4/classic/errors.js +48 -0
  26. package/dist/astgraph/vendor/zod/v4/classic/external.js +20 -0
  27. package/dist/astgraph/vendor/zod/v4/classic/from-json-schema.js +599 -0
  28. package/dist/astgraph/vendor/zod/v4/classic/index.js +4 -0
  29. package/dist/astgraph/vendor/zod/v4/classic/iso.js +30 -0
  30. package/dist/astgraph/vendor/zod/v4/classic/package.json +7 -0
  31. package/dist/astgraph/vendor/zod/v4/classic/parse.js +15 -0
  32. package/dist/astgraph/vendor/zod/v4/classic/schemas.js +1395 -0
  33. package/dist/astgraph/vendor/zod/v4/core/api.js +1087 -0
  34. package/dist/astgraph/vendor/zod/v4/core/checks.js +575 -0
  35. package/dist/astgraph/vendor/zod/v4/core/core.js +78 -0
  36. package/dist/astgraph/vendor/zod/v4/core/doc.js +35 -0
  37. package/dist/astgraph/vendor/zod/v4/core/errors.js +185 -0
  38. package/dist/astgraph/vendor/zod/v4/core/index.js +16 -0
  39. package/dist/astgraph/vendor/zod/v4/core/json-schema-generator.js +95 -0
  40. package/dist/astgraph/vendor/zod/v4/core/json-schema-processors.js +601 -0
  41. package/dist/astgraph/vendor/zod/v4/core/json-schema.js +1 -0
  42. package/dist/astgraph/vendor/zod/v4/core/package.json +7 -0
  43. package/dist/astgraph/vendor/zod/v4/core/parse.js +93 -0
  44. package/dist/astgraph/vendor/zod/v4/core/regexes.js +139 -0
  45. package/dist/astgraph/vendor/zod/v4/core/registries.js +51 -0
  46. package/dist/astgraph/vendor/zod/v4/core/schemas.js +2239 -0
  47. package/dist/astgraph/vendor/zod/v4/core/standard-schema.js +1 -0
  48. package/dist/astgraph/vendor/zod/v4/core/to-json-schema.js +448 -0
  49. package/dist/astgraph/vendor/zod/v4/core/util.js +674 -0
  50. package/dist/astgraph/vendor/zod/v4/core/versions.js +5 -0
  51. package/dist/astgraph/vendor/zod/v4/index.js +3 -0
  52. package/dist/astgraph/vendor/zod/v4/locales/ar.js +106 -0
  53. package/dist/astgraph/vendor/zod/v4/locales/az.js +105 -0
  54. package/dist/astgraph/vendor/zod/v4/locales/be.js +156 -0
  55. package/dist/astgraph/vendor/zod/v4/locales/bg.js +120 -0
  56. package/dist/astgraph/vendor/zod/v4/locales/ca.js +107 -0
  57. package/dist/astgraph/vendor/zod/v4/locales/cs.js +111 -0
  58. package/dist/astgraph/vendor/zod/v4/locales/da.js +115 -0
  59. package/dist/astgraph/vendor/zod/v4/locales/de.js +108 -0
  60. package/dist/astgraph/vendor/zod/v4/locales/el.js +109 -0
  61. package/dist/astgraph/vendor/zod/v4/locales/en.js +113 -0
  62. package/dist/astgraph/vendor/zod/v4/locales/eo.js +109 -0
  63. package/dist/astgraph/vendor/zod/v4/locales/es.js +132 -0
  64. package/dist/astgraph/vendor/zod/v4/locales/fa.js +114 -0
  65. package/dist/astgraph/vendor/zod/v4/locales/fi.js +112 -0
  66. package/dist/astgraph/vendor/zod/v4/locales/fr-CA.js +107 -0
  67. package/dist/astgraph/vendor/zod/v4/locales/fr.js +125 -0
  68. package/dist/astgraph/vendor/zod/v4/locales/he.js +214 -0
  69. package/dist/astgraph/vendor/zod/v4/locales/hr.js +122 -0
  70. package/dist/astgraph/vendor/zod/v4/locales/hu.js +108 -0
  71. package/dist/astgraph/vendor/zod/v4/locales/hy.js +147 -0
  72. package/dist/astgraph/vendor/zod/v4/locales/id.js +106 -0
  73. package/dist/astgraph/vendor/zod/v4/locales/index.js +52 -0
  74. package/dist/astgraph/vendor/zod/v4/locales/is.js +109 -0
  75. package/dist/astgraph/vendor/zod/v4/locales/it.js +108 -0
  76. package/dist/astgraph/vendor/zod/v4/locales/ja.js +107 -0
  77. package/dist/astgraph/vendor/zod/v4/locales/ka.js +112 -0
  78. package/dist/astgraph/vendor/zod/v4/locales/kh.js +5 -0
  79. package/dist/astgraph/vendor/zod/v4/locales/km.js +110 -0
  80. package/dist/astgraph/vendor/zod/v4/locales/ko.js +111 -0
  81. package/dist/astgraph/vendor/zod/v4/locales/lt.js +203 -0
  82. package/dist/astgraph/vendor/zod/v4/locales/mk.js +109 -0
  83. package/dist/astgraph/vendor/zod/v4/locales/ms.js +107 -0
  84. package/dist/astgraph/vendor/zod/v4/locales/nl.js +110 -0
  85. package/dist/astgraph/vendor/zod/v4/locales/no.js +108 -0
  86. package/dist/astgraph/vendor/zod/v4/locales/ota.js +109 -0
  87. package/dist/astgraph/vendor/zod/v4/locales/package.json +7 -0
  88. package/dist/astgraph/vendor/zod/v4/locales/pl.js +109 -0
  89. package/dist/astgraph/vendor/zod/v4/locales/ps.js +114 -0
  90. package/dist/astgraph/vendor/zod/v4/locales/pt.js +108 -0
  91. package/dist/astgraph/vendor/zod/v4/locales/ro.js +119 -0
  92. package/dist/astgraph/vendor/zod/v4/locales/ru.js +156 -0
  93. package/dist/astgraph/vendor/zod/v4/locales/sl.js +109 -0
  94. package/dist/astgraph/vendor/zod/v4/locales/sv.js +110 -0
  95. package/dist/astgraph/vendor/zod/v4/locales/ta.js +110 -0
  96. package/dist/astgraph/vendor/zod/v4/locales/th.js +110 -0
  97. package/dist/astgraph/vendor/zod/v4/locales/tr.js +105 -0
  98. package/dist/astgraph/vendor/zod/v4/locales/ua.js +5 -0
  99. package/dist/astgraph/vendor/zod/v4/locales/uk.js +108 -0
  100. package/dist/astgraph/vendor/zod/v4/locales/ur.js +110 -0
  101. package/dist/astgraph/vendor/zod/v4/locales/uz.js +110 -0
  102. package/dist/astgraph/vendor/zod/v4/locales/vi.js +108 -0
  103. package/dist/astgraph/vendor/zod/v4/locales/yo.js +107 -0
  104. package/dist/astgraph/vendor/zod/v4/locales/zh-CN.js +109 -0
  105. package/dist/astgraph/vendor/zod/v4/locales/zh-TW.js +107 -0
  106. package/dist/astgraph/vendor/zod/v4/mini/checks.js +1 -0
  107. package/dist/astgraph/vendor/zod/v4/mini/coerce.js +22 -0
  108. package/dist/astgraph/vendor/zod/v4/mini/external.js +14 -0
  109. package/dist/astgraph/vendor/zod/v4/mini/index.js +3 -0
  110. package/dist/astgraph/vendor/zod/v4/mini/iso.js +34 -0
  111. package/dist/astgraph/vendor/zod/v4/mini/package.json +7 -0
  112. package/dist/astgraph/vendor/zod/v4/mini/parse.js +1 -0
  113. package/dist/astgraph/vendor/zod/v4/mini/schemas.js +961 -0
  114. package/dist/astgraph/vendor/zod/v4/package.json +7 -0
  115. package/dist/{chunk-EY6KE7L2.js → chunk-G6Q2Y2A7.js} +1 -1
  116. package/dist/{chunk-OT5XIN6G.js → chunk-Y5MZDG7I.js} +161 -18
  117. package/dist/cli.js +3 -3
  118. package/dist/desktop/main.js +161 -18
  119. package/dist/desktop-renderer/assets/{index-sgEm4kjf.js → index-CCnSpff0.js} +1 -1
  120. package/dist/desktop-renderer/index.html +1 -1
  121. package/dist/init/project.d.ts +8 -0
  122. package/dist/models/structured.d.ts +14 -0
  123. package/dist/permissions/engine.d.ts +2 -0
  124. package/dist/plugins/types.d.ts +2 -2
  125. package/dist/sdk/index.js +2 -2
  126. package/dist/tools/shell.d.ts +1 -1
  127. package/dist/tools/types.d.ts +2 -0
  128. package/dist/ui/session.d.ts +4 -1
  129. package/package.json +5 -3
package/README.md CHANGED
@@ -35,6 +35,7 @@ Flavor Code connects to OpenAI, Anthropic, or compatible services and works with
35
35
  | 🧭 | **Controlled progress on complex tasks** | Task plans, sub-agents, steering, follow-ups, `/loop`, and `/goal` |
36
36
  | ⏪ | **Traceable, resumable results** | Full timeline, checkpoints, rewind, traces, diffs, and failure audits |
37
37
  | 🧠 | **Local long-term context** | Memory, Skills, plugins, and project guides stored on your machine |
38
+ | 🔎 | **Code graph navigation** | A local AST code-graph index (`.flavor/astgraph/`) powers `ast_search`/`ast_callers`/`ast_impact` queries for precise symbol lookup and reachability tracing |
38
39
  | 🎨 | **E2E requirement-to-delivery** | From a rough requirement or a design export to a delivered product: PRD, interactive prototype, visual implementation, API integration, autonomous acceptance, and scored delivery (Electron only) |
39
40
  | 🛡️ | **Clear permission boundaries** | Independent control over read, write, Shell, network, and destructive actions; Docker supported |
40
41
 
@@ -152,41 +153,41 @@ Common commands:
152
153
  | `/memory`, `/remember`, `/forget`, `/forget-cold` | Manage long-term memory; `/forget-cold` purges cold entries and their files |
153
154
  | `/mcp` | View and manage MCP servers |
154
155
  | `/loop <goal>` | Run an autonomous loop with verification |
155
- | `/goal <objective>` | Run the plan, execute, adversarial-review workflow |
156
- | `/pals`, `/chat`, `/co-work` | Discover and collaborate with other local CLI instances |
157
- | `/audit` | View tool failure audits |
158
-
159
- You can submit steering or queue follow-ups while a run is in progress; once the current model response finishes, the task picks up new instructions at safe boundaries.
160
-
161
- #### CLI pals and cross-project work
162
-
163
- Interactive CLI instances on the same Windows or macOS user account can collaborate over local-only IPC (Windows named pipes or Unix sockets; no TCP fallback). Give each window a memorable alias:
164
-
165
- ```bash
166
- # terminal A, in project A
167
- flavor --pal-name A
168
-
169
- # terminal B, in project B
170
- flavor --pal-name B
171
- ```
172
-
173
- Useful commands:
174
-
175
- ```text
176
- /pals # aliases and per-process UUIDs
177
- /pals --verbose # also show project paths and timestamps
178
- /pals rename api # rename this active instance
179
- /chat B Update the API and tests # deliver to B and start its agent safely
180
- /co-work B Upgrade B, then adapt A # negotiate one plan before parallel work
181
- /co-work status [co-work-uuid]
182
- /co-work cancel <co-work-uuid> [reason]
183
- ```
184
-
185
- `/chat` is bidirectional and task-oriented. If B is idle, the attributed message starts a normal model turn; if B is already running, it becomes steering, or a follow-up when another local submission is pending. Remote text is converted to a safe non-slash prompt, so `/exit`-like text is not dispatched as a local command. B can answer with `/chat A ...`.
186
-
187
- `/co-work` first places both agents in planning and waits for both to accept the same hashed plan and declare READY. Early READY intents are retained, and only the broker's exactly-once START event opens parallel execution. Each agent works only in its own project, receives only its assigned tasks, and reports bounded completion evidence. The broker-selected integration owner verifies all assertions and emits END or FAIL through `CoWorkIntegrate`. Communication uses authenticated, bounded local IPC with no TCP listener; peer input cannot approve tools or access the other workspace. UUID/alias routing and the protocol already support a third active client; durable artifact exchange, broker-restart journaling/recovery, and large-group coordination are later hardening work. See the [CLI pals specification](./docs/specs/2026-08-14-cli-pals-cowork.md).
188
-
189
- ### Electron Desktop
156
+ | `/goal <objective>` | Run the plan, execute, adversarial-review workflow |
157
+ | `/pals`, `/chat`, `/co-work` | Discover and collaborate with other local CLI instances |
158
+ | `/audit` | View tool failure audits |
159
+
160
+ You can submit steering or queue follow-ups while a run is in progress; once the current model response finishes, the task picks up new instructions at safe boundaries.
161
+
162
+ #### CLI pals and cross-project work
163
+
164
+ Interactive CLI instances on the same Windows or macOS user account can collaborate over local-only IPC (Windows named pipes or Unix sockets; no TCP fallback). Give each window a memorable alias:
165
+
166
+ ```bash
167
+ # terminal A, in project A
168
+ flavor --pal-name A
169
+
170
+ # terminal B, in project B
171
+ flavor --pal-name B
172
+ ```
173
+
174
+ Useful commands:
175
+
176
+ ```text
177
+ /pals # aliases and per-process UUIDs
178
+ /pals --verbose # also show project paths and timestamps
179
+ /pals rename api # rename this active instance
180
+ /chat B Update the API and tests # deliver to B and start its agent safely
181
+ /co-work B Upgrade B, then adapt A # negotiate one plan before parallel work
182
+ /co-work status [co-work-uuid]
183
+ /co-work cancel <co-work-uuid> [reason]
184
+ ```
185
+
186
+ `/chat` is bidirectional and task-oriented. If B is idle, the attributed message starts a normal model turn; if B is already running, it becomes steering, or a follow-up when another local submission is pending. Remote text is converted to a safe non-slash prompt, so `/exit`-like text is not dispatched as a local command. B can answer with `/chat A ...`.
187
+
188
+ `/co-work` first places both agents in planning and waits for both to accept the same hashed plan and declare READY. Early READY intents are retained, and only the broker's exactly-once START event opens parallel execution. Each agent works only in its own project, receives only its assigned tasks, and reports bounded completion evidence. The broker-selected integration owner verifies all assertions and emits END or FAIL through `CoWorkIntegrate`. Communication uses authenticated, bounded local IPC with no TCP listener; peer input cannot approve tools or access the other workspace. UUID/alias routing and the protocol already support a third active client; durable artifact exchange, broker-restart journaling/recovery, and large-group coordination are later hardening work. See the [CLI pals specification](./docs/specs/2026-08-14-cli-pals-cowork.md).
189
+
190
+ ### Electron Desktop
190
191
 
191
192
  ```bash
192
193
  npm run desktop:dev # dev mode
package/README.zh-CN.md CHANGED
@@ -35,6 +35,7 @@ Flavor Code 接入 OpenAI、Anthropic 或兼容服务,在受控工作区内使
35
35
  | 🧭 | **复杂任务可控推进** | 任务计划、子 Agent、steering、follow-up、`/loop` 和 `/goal` |
36
36
  | ⏪ | **结果可追溯、可恢复** | 完整时间线、checkpoint、rewind、trace、Diff 和失败审计 |
37
37
  | 🧠 | **本地长期上下文** | 记忆、Skill、插件和项目指南均保存在本机 |
38
+ | 🔎 | **代码图导航** | 本地 AST 代码图索引(`.flavor/astgraph/`),通过 `ast_search`/`ast_callers`/`ast_impact` 等查询精确定位符号、追踪可达性 |
38
39
  | 🎨 | **E2E 需求到交付** | 从粗需求或设计稿到可交付产品:PRD、交互原型、视觉还原、接口联调、自主验收与评分交付(仅 Electron) |
39
40
  | 🛡️ | **明确的权限边界** | 分别控制读、写、Shell、网络和破坏性操作,也可使用 Docker |
40
41
 
@@ -152,41 +153,41 @@ OAuth PKCE 的运行时行为与配置约定见 [PKCE 规范](./docs/specs/pkce-
152
153
  | `/memory`、`/remember`、`/forget`、`/forget-cold` | 管理长期记忆;`/forget-cold` 清空 cold 记忆及其文件 |
153
154
  | `/mcp` | 查看和管理 MCP 服务 |
154
155
  | `/loop <goal>` | 运行带验证的自治循环 |
155
- | `/goal <objective>` | 运行规划、执行、对抗审查流程 |
156
- | `/pals`、`/chat`、`/co-work` | 发现并协作其他本机 CLI 实例 |
157
- | `/audit` | 查看工具失败审计 |
158
-
159
- 运行中可以提交 steering 或排队 follow-up;当前模型响应结束后,任务会在安全边界处接收新指令。
160
-
161
- #### CLI Pals 与跨项目协作
162
-
163
- 同一 Windows 或 macOS 用户下的交互式 CLI 可以通过纯本地 IPC 协作(Windows named pipe 或 Unix socket,不回退到 TCP)。先给每个窗口一个容易识别的别名:
164
-
165
- ```bash
166
- # 终端 A,位于项目 A
167
- flavor --pal-name A
168
-
169
- # 终端 B,位于项目 B
170
- flavor --pal-name B
171
- ```
172
-
173
- 常用命令:
174
-
175
- ```text
176
- /pals # 查看别名和每进程 UUID
177
- /pals --verbose # 额外显示项目路径和时间
178
- /pals rename api # 重命名当前活动实例
179
- /chat B 更新 API 和测试 # 投递给 B,并安全启动其 Agent
180
- /co-work B 先升级 B,再兼容 A # 先协商同一计划,再并行开工
181
- /co-work status [co-work-uuid]
182
- /co-work cancel <co-work-uuid> [reason]
183
- ```
184
-
185
- `/chat` 支持双向任务通信。B 空闲时,带来源标识的消息会启动正常模型回合;B 正在运行时,消息会成为 steering;已有本地提交待运行时则成为 follow-up。远端文本会转换成安全的非斜杠 prompt,因此 `/exit` 一类文本不会被当成本地命令分派。B 可以用 `/chat A ...` 回复。
186
-
187
- `/co-work` 会先让双方进入规划,等待双方接受同一个哈希计划并声明 READY;较早的 READY 意图会被保留,只有 broker 恰好一次的 START 事件才会放行并行执行。每个 Agent 只在自己的项目内工作,只接收分配给自己的任务,并提交有界的完成证据。broker 指定的集成负责人会检查所有断言,再通过 `CoWorkIntegrate` 广播 END 或 FAIL。通信使用经过认证、有大小上限的本机 IPC,不开放 TCP 监听;peer 输入不能代替本机工具审批,也不能访问另一工作区。UUID/别名路由和协议已能支持第三个活动实例;持久化成果物交换、broker 重启日志与恢复、大规模多方协调属于后续强化。详见 [CLI Pals 规范](./docs/specs/2026-08-14-cli-pals-cowork.md)。
188
-
189
- ### Electron 桌面端
156
+ | `/goal <objective>` | 运行规划、执行、对抗审查流程 |
157
+ | `/pals`、`/chat`、`/co-work` | 发现并协作其他本机 CLI 实例 |
158
+ | `/audit` | 查看工具失败审计 |
159
+
160
+ 运行中可以提交 steering 或排队 follow-up;当前模型响应结束后,任务会在安全边界处接收新指令。
161
+
162
+ #### CLI Pals 与跨项目协作
163
+
164
+ 同一 Windows 或 macOS 用户下的交互式 CLI 可以通过纯本地 IPC 协作(Windows named pipe 或 Unix socket,不回退到 TCP)。先给每个窗口一个容易识别的别名:
165
+
166
+ ```bash
167
+ # 终端 A,位于项目 A
168
+ flavor --pal-name A
169
+
170
+ # 终端 B,位于项目 B
171
+ flavor --pal-name B
172
+ ```
173
+
174
+ 常用命令:
175
+
176
+ ```text
177
+ /pals # 查看别名和每进程 UUID
178
+ /pals --verbose # 额外显示项目路径和时间
179
+ /pals rename api # 重命名当前活动实例
180
+ /chat B 更新 API 和测试 # 投递给 B,并安全启动其 Agent
181
+ /co-work B 先升级 B,再兼容 A # 先协商同一计划,再并行开工
182
+ /co-work status [co-work-uuid]
183
+ /co-work cancel <co-work-uuid> [reason]
184
+ ```
185
+
186
+ `/chat` 支持双向任务通信。B 空闲时,带来源标识的消息会启动正常模型回合;B 正在运行时,消息会成为 steering;已有本地提交待运行时则成为 follow-up。远端文本会转换成安全的非斜杠 prompt,因此 `/exit` 一类文本不会被当成本地命令分派。B 可以用 `/chat A ...` 回复。
187
+
188
+ `/co-work` 会先让双方进入规划,等待双方接受同一个哈希计划并声明 READY;较早的 READY 意图会被保留,只有 broker 恰好一次的 START 事件才会放行并行执行。每个 Agent 只在自己的项目内工作,只接收分配给自己的任务,并提交有界的完成证据。broker 指定的集成负责人会检查所有断言,再通过 `CoWorkIntegrate` 广播 END 或 FAIL。通信使用经过认证、有大小上限的本机 IPC,不开放 TCP 监听;peer 输入不能代替本机工具审批,也不能访问另一工作区。UUID/别名路由和协议已能支持第三个活动实例;持久化成果物交换、broker 重启日志与恢复、大规模多方协调属于后续强化。详见 [CLI Pals 规范](./docs/specs/2026-08-14-cli-pals-cowork.md)。
189
+
190
+ ### Electron 桌面端
190
191
 
191
192
  ```bash
192
193
  npm run desktop:dev # 开发模式
@@ -15,7 +15,7 @@ import {
15
15
  packageVersion,
16
16
  redactErrorText,
17
17
  transcriptReducer
18
- } from "./chunk-OT5XIN6G.js";
18
+ } from "./chunk-Y5MZDG7I.js";
19
19
  import "./chunk-XFCJXRJ2.js";
20
20
  import {
21
21
  Box_default,
@@ -1249,8 +1249,10 @@ function buildSlashCandidates(commands, plugins, skills) {
1249
1249
  for (const command of commands) {
1250
1250
  candidates.set(command.name, { name: command.name, kind: "command", description: command.description });
1251
1251
  }
1252
- for (const name of plugins) {
1253
- if (!candidates.has(name)) candidates.set(name, { name, kind: "plugin" });
1252
+ for (const plugin of plugins) {
1253
+ if (!candidates.has(plugin.name)) {
1254
+ candidates.set(plugin.name, plugin.description === void 0 ? { name: plugin.name, kind: "plugin" } : { name: plugin.name, kind: "plugin", description: plugin.description });
1255
+ }
1254
1256
  }
1255
1257
  for (const skill of skills) {
1256
1258
  if (!candidates.has(skill.name)) {
@@ -0,0 +1,237 @@
1
+ // astgraph graph database — SQLite-backed node/edge graph (codegraph-inspired).
2
+ // Uses node:sqlite with WAL mode so concurrent readers never block on a writer.
3
+
4
+ import { mkdirSync } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ import { DatabaseSync } from "node:sqlite";
7
+
8
+ const SCHEMA = `
9
+ CREATE TABLE IF NOT EXISTS schema_versions (
10
+ version INTEGER PRIMARY KEY,
11
+ applied_at INTEGER NOT NULL,
12
+ description TEXT
13
+ );
14
+
15
+ CREATE TABLE IF NOT EXISTS files (
16
+ path TEXT PRIMARY KEY,
17
+ content_hash TEXT NOT NULL,
18
+ language TEXT NOT NULL,
19
+ size INTEGER NOT NULL,
20
+ modified_at INTEGER NOT NULL DEFAULT 0,
21
+ indexed_at INTEGER NOT NULL DEFAULT 0,
22
+ node_count INTEGER NOT NULL DEFAULT 0
23
+ );
24
+
25
+ CREATE TABLE IF NOT EXISTS nodes (
26
+ id TEXT PRIMARY KEY,
27
+ kind TEXT NOT NULL,
28
+ name TEXT NOT NULL,
29
+ qualified_name TEXT NOT NULL,
30
+ file_path TEXT NOT NULL,
31
+ language TEXT NOT NULL,
32
+ start_line INTEGER NOT NULL,
33
+ end_line INTEGER NOT NULL,
34
+ signature TEXT,
35
+ docstring TEXT,
36
+ is_exported INTEGER NOT NULL DEFAULT 0,
37
+ is_async INTEGER NOT NULL DEFAULT 0
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS edges (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ source TEXT NOT NULL,
43
+ target TEXT NOT NULL,
44
+ kind TEXT NOT NULL,
45
+ line INTEGER,
46
+ col INTEGER,
47
+ FOREIGN KEY (source) REFERENCES nodes(id) ON DELETE CASCADE,
48
+ FOREIGN KEY (target) REFERENCES nodes(id) ON DELETE CASCADE
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS unresolved_refs (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ from_node_id TEXT,
54
+ reference_name TEXT NOT NULL,
55
+ reference_kind TEXT NOT NULL,
56
+ line INTEGER NOT NULL,
57
+ col INTEGER NOT NULL,
58
+ file_path TEXT NOT NULL DEFAULT '',
59
+ module_specifier TEXT,
60
+ status TEXT NOT NULL DEFAULT 'pending',
61
+ FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE
62
+ );
63
+
64
+ CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
65
+ CREATE INDEX IF NOT EXISTS idx_nodes_lower_name ON nodes(lower(name));
66
+ CREATE INDEX IF NOT EXISTS idx_nodes_file_path ON nodes(file_path);
67
+ CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);
68
+ CREATE INDEX IF NOT EXISTS idx_edges_source_kind ON edges(source, kind);
69
+ CREATE INDEX IF NOT EXISTS idx_edges_target_kind ON edges(target, kind);
70
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_edges_identity
71
+ ON edges(source, target, kind, IFNULL(line, -1), IFNULL(col, -1));
72
+ CREATE INDEX IF NOT EXISTS idx_unresolved_from ON unresolved_refs(from_node_id);
73
+ CREATE INDEX IF NOT EXISTS idx_unresolved_name ON unresolved_refs(reference_name);
74
+
75
+ CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
76
+ name,
77
+ qualified_name,
78
+ signature,
79
+ docstring,
80
+ content='nodes',
81
+ content_rowid='rowid'
82
+ );
83
+
84
+ CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
85
+ INSERT INTO nodes_fts(rowid, name, qualified_name, signature, docstring)
86
+ VALUES (NEW.rowid, NEW.name, NEW.qualified_name, NEW.signature, NEW.docstring);
87
+ END;
88
+
89
+ CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
90
+ INSERT INTO nodes_fts(nodes_fts, rowid, name, qualified_name, signature, docstring)
91
+ VALUES ('delete', OLD.rowid, OLD.name, OLD.qualified_name, OLD.signature, OLD.docstring);
92
+ END;
93
+
94
+ CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN
95
+ INSERT INTO nodes_fts(nodes_fts, rowid, name, qualified_name, signature, docstring)
96
+ VALUES ('delete', OLD.rowid, OLD.name, OLD.qualified_name, OLD.signature, OLD.docstring);
97
+ INSERT INTO nodes_fts(rowid, name, qualified_name, signature, docstring)
98
+ VALUES (NEW.rowid, NEW.name, NEW.qualified_name, NEW.signature, NEW.docstring);
99
+ END;
100
+
101
+ CREATE TABLE IF NOT EXISTS project_metadata (
102
+ key TEXT PRIMARY KEY,
103
+ value TEXT NOT NULL,
104
+ updated_at INTEGER NOT NULL
105
+ );
106
+ `;
107
+
108
+ /** Open (creating when absent) the graph database at `path`. Returns a DatabaseSync. */
109
+ export function openDb(path) {
110
+ if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
111
+ const db = new DatabaseSync(path);
112
+ db.exec("PRAGMA journal_mode = WAL");
113
+ db.exec("PRAGMA foreign_keys = ON");
114
+ db.exec("PRAGMA synchronous = NORMAL");
115
+ db.exec(SCHEMA);
116
+ const version = db.prepare("SELECT COUNT(*) AS count FROM schema_versions").get();
117
+ if (version.count === 0) {
118
+ db.prepare("INSERT INTO schema_versions(version, applied_at, description) VALUES (?, ?, ?)")
119
+ .run(1, Date.now(), "Initial astgraph schema");
120
+ }
121
+ return db;
122
+ }
123
+
124
+ /** Insert or replace the tracking record for one source file. */
125
+ export function upsertFileRecord(db, record) {
126
+ db.prepare(`
127
+ INSERT INTO files(path, content_hash, language, size, modified_at, indexed_at, node_count)
128
+ VALUES (?, ?, ?, ?, ?, ?, ?)
129
+ ON CONFLICT(path) DO UPDATE SET
130
+ content_hash = excluded.content_hash,
131
+ language = excluded.language,
132
+ size = excluded.size,
133
+ modified_at = excluded.modified_at,
134
+ indexed_at = excluded.indexed_at,
135
+ node_count = excluded.node_count
136
+ `).run(
137
+ record.path, record.contentHash, record.language, record.size,
138
+ record.modifiedAt ?? Date.now(), record.indexedAt ?? Date.now(), record.nodeCount ?? 0,
139
+ );
140
+ }
141
+
142
+ /** Read the tracking record for one source file, or undefined when absent. */
143
+ export function getFileRecord(db, path) {
144
+ const row = db.prepare("SELECT * FROM files WHERE path = ?").get(path);
145
+ if (row === undefined) return undefined;
146
+ return {
147
+ path: row.path, contentHash: row.content_hash, language: row.language,
148
+ size: row.size, modifiedAt: row.modified_at, indexedAt: row.indexed_at,
149
+ nodeCount: row.node_count,
150
+ };
151
+ }
152
+
153
+ /** Delete a file record; nodes cascade (and with them edges + refs + FTS rows). */
154
+ export function deleteFileRecord(db, path) {
155
+ db.prepare("DELETE FROM nodes WHERE file_path = ?").run(path);
156
+ db.prepare("DELETE FROM files WHERE path = ?").run(path);
157
+ }
158
+
159
+ /**
160
+ * Replace every node belonging to a file. Runs inside one transaction; the FTS
161
+ * triggers keep nodes_fts in sync.
162
+ */
163
+ export function replaceNodes(db, filePath, nodes) {
164
+ const insert = db.prepare(`
165
+ INSERT OR REPLACE INTO nodes(
166
+ id, kind, name, qualified_name, file_path, language,
167
+ start_line, end_line, signature, docstring, is_exported, is_async
168
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
169
+ `);
170
+ const clear = db.prepare("DELETE FROM nodes WHERE file_path = ?");
171
+ db.exec("BEGIN");
172
+ try {
173
+ clear.run(filePath);
174
+ for (const node of nodes) {
175
+ insert.run(
176
+ node.id, node.kind, node.name, node.qualifiedName, node.filePath, node.language,
177
+ node.startLine, node.endLine, node.signature ?? null, node.docstring ?? null,
178
+ node.isExported ? 1 : 0, node.isAsync ? 1 : 0,
179
+ );
180
+ }
181
+ db.exec("COMMIT");
182
+ } catch (error) {
183
+ db.exec("ROLLBACK");
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ /** Insert one edge; duplicate (source, target, kind, line, col) rows are ignored. */
189
+ export function insertEdge(db, edge) {
190
+ db.prepare(`
191
+ INSERT OR IGNORE INTO edges(source, target, kind, line, col) VALUES (?, ?, ?, ?, ?)
192
+ `).run(edge.source, edge.target, edge.kind, edge.line ?? null, edge.col ?? null);
193
+ }
194
+
195
+ /** Delete every edge whose source OR target belongs to a file. */
196
+ export function deleteEdgesForFile(db, filePath) {
197
+ db.prepare(`
198
+ DELETE FROM edges WHERE source IN (SELECT id FROM nodes WHERE file_path = ?)
199
+ OR target IN (SELECT id FROM nodes WHERE file_path = ?)
200
+ `).run(filePath, filePath);
201
+ }
202
+
203
+ /** Insert one unresolved cross-file reference (status 'pending'). */
204
+ export function insertUnresolvedRef(db, ref) {
205
+ db.prepare(`
206
+ INSERT INTO unresolved_refs(from_node_id, reference_name, reference_kind, line, col, file_path, module_specifier, status)
207
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')
208
+ `).run(ref.fromNodeId, ref.referenceName, ref.referenceKind, ref.line, ref.col, ref.filePath, ref.moduleSpecifier ?? null);
209
+ }
210
+
211
+ /** Delete every unresolved reference originating from a file. */
212
+ export function deleteUnresolvedRefsForFile(db, filePath) {
213
+ db.prepare("DELETE FROM unresolved_refs WHERE file_path = ?").run(filePath);
214
+ }
215
+
216
+ /** Summary counters used by /ast status. */
217
+ export function stats(db) {
218
+ const files = db.prepare("SELECT COUNT(*) AS count FROM files").get().count;
219
+ const nodes = db.prepare("SELECT COUNT(*) AS count FROM nodes").get().count;
220
+ const edges = db.prepare("SELECT COUNT(*) AS count FROM edges").get().count;
221
+ const unresolved = db.prepare("SELECT COUNT(*) AS count FROM unresolved_refs WHERE status = 'pending'").get().count;
222
+ return { files, nodes, edges, unresolved };
223
+ }
224
+
225
+ /** Store a provenance metadata value. */
226
+ export function setMetadata(db, key, value) {
227
+ db.prepare(`
228
+ INSERT INTO project_metadata(key, value, updated_at) VALUES (?, ?, ?)
229
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
230
+ `).run(key, value, Date.now());
231
+ }
232
+
233
+ /** Read a provenance metadata value, or undefined when absent. */
234
+ export function getMetadata(db, key) {
235
+ const row = db.prepare("SELECT value FROM project_metadata WHERE key = ?").get(key);
236
+ return row?.value;
237
+ }
@@ -0,0 +1,239 @@
1
+ // astgraph TypeScript/JavaScript structural extractor.
2
+ // Walks the tree-sitter syntax tree and produces:
3
+ // nodes — functions, classes, methods, interfaces, type aliases, variables
4
+ // edges — intra-file calls / extends / implements
5
+ // refs — cross-file references (imports + unresolved calls/heritage) resolved later
6
+ //
7
+ // Lines are 1-based in all outputs so they match editor/Read tool coordinates.
8
+
9
+ import { parseSource } from "./grammars.mjs";
10
+
11
+ const DECLARATION_KINDS = new Map([
12
+ ["function_declaration", "function"],
13
+ ["class_declaration", "class"],
14
+ ["interface_declaration", "interface"],
15
+ ["type_alias_declaration", "type"],
16
+ ]);
17
+
18
+ /** Extract the doc text from a `/** ... */` or `// ...` comment node. */
19
+ function docstringFrom(commentNode) {
20
+ if (commentNode === null || commentNode.type !== "comment") return undefined;
21
+ const text = commentNode.text;
22
+ if (text.startsWith("/**") || text.startsWith("/*!")) {
23
+ return text
24
+ .replace(/^\/\*+[!*]?/, "")
25
+ .replace(/\*+\/$/, "")
26
+ .split("\n")
27
+ .map((line) => line.replace(/^\s*\*+\s?/, "").trim())
28
+ .filter((line) => line.length > 0)
29
+ .join("\n") || undefined;
30
+ }
31
+ if (text.startsWith("//")) return text.replace(/^\/\/+\s?/, "").trim() || undefined;
32
+ return undefined;
33
+ }
34
+
35
+ function firstLine(text, limit = 240) {
36
+ const line = text.split("\n")[0].trim();
37
+ return line.length > limit ? `${line.slice(0, limit)}…` : line;
38
+ }
39
+
40
+ function signatureOf(node) {
41
+ const text = node.text;
42
+ const brace = text.indexOf("{");
43
+ const head = brace >= 0 ? text.slice(0, brace) : text.split("\n")[0];
44
+ return firstLine(head.replace(/\s+/g, " "));
45
+ }
46
+
47
+ function nameOf(node) {
48
+ const nameNode = node.childForFieldName("name");
49
+ return nameNode === null ? undefined : nameNode.text;
50
+ }
51
+
52
+ function isAsync(node) {
53
+ return node.children.some((child) => child.type === "async");
54
+ }
55
+
56
+ export async function extract(source, filePath, language) {
57
+ const tree = await parseSource(source, language);
58
+
59
+ /** @type {Array<object>} */
60
+ const nodes = [];
61
+ /** @type {Array<object>} */
62
+ const edges = [];
63
+ /** @type {Array<object>} */
64
+ const refs = [];
65
+ /** Map local symbol name → node id (methods registered as name only for call matching). */
66
+ const localNames = new Map();
67
+ /** Map imported binding name → module specifier. */
68
+ const imports = new Map();
69
+ /** Map declaration AST node id → node record, for enclosing-scope lookup.
70
+ * Keyed by numeric node.id because web-tree-sitter returns fresh node object
71
+ * instances on every navigation (parent/childForFieldName), so object identity
72
+ * cannot be used as a Map key. */
73
+ const scopeByNodeId = new Map();
74
+
75
+ const makeNode = (astNode, kind, name, qualifiedName, exported, extra = {}) => {
76
+ // The docstring lives on the top-level statement's preceding sibling (comments
77
+ // attach to program/export_statement, not to the inner declaration node).
78
+ const docSource = extra.docSource ?? astNode;
79
+ const record = {
80
+ id: `${filePath}#${qualifiedName}`,
81
+ kind,
82
+ name,
83
+ qualifiedName,
84
+ filePath,
85
+ language,
86
+ startLine: astNode.startPosition.row + 1,
87
+ endLine: astNode.endPosition.row + 1,
88
+ signature: signatureOf(astNode),
89
+ docstring: docstringFrom(docSource.previousSibling),
90
+ isExported: exported,
91
+ isAsync: extra.isAsync ?? false,
92
+ ...extra,
93
+ };
94
+ delete record.docSource;
95
+ nodes.push(record);
96
+ scopeByNodeId.set(astNode.id, record);
97
+ if (!localNames.has(name)) localNames.set(name, record.id);
98
+ return record;
99
+ };
100
+
101
+ const collectDeclaration = (statement, exported) => {
102
+ const inner = statement.type === "export_statement" ? statement.namedChildren.find((child) => DECLARATION_KINDS.has(child.type) || child.type === "lexical_declaration" || child.type === "abstract_class_declaration") : statement;
103
+ const target = inner ?? statement;
104
+ const kind = DECLARATION_KINDS.get(target.type);
105
+ if (kind !== undefined) {
106
+ const name = nameOf(target);
107
+ if (name !== undefined) {
108
+ // Comments attach to the top-level statement (export_statement / declaration),
109
+ // so the docstring is read from the statement's preceding sibling.
110
+ makeNode(target, kind, name, name, exported, {
111
+ ...(kind === "function" ? { isAsync: isAsync(target) } : {}),
112
+ docSource: statement,
113
+ });
114
+ if (target.type === "class_declaration" || target.type === "abstract_class_declaration") collectClassMembers(target, exported);
115
+ collectHeritage(target, exported);
116
+ }
117
+ return;
118
+ }
119
+ if (target.type === "lexical_declaration") {
120
+ for (const declarator of target.namedChildren.filter((child) => child.type === "variable_declarator")) {
121
+ const name = declarator.childForFieldName("name")?.text;
122
+ const value = declarator.childForFieldName("value");
123
+ if (name === undefined || value === null) continue;
124
+ const isFunction = value.type === "arrow_function" || value.type === "function_expression" || value.type === "function";
125
+ makeNode(declarator, isFunction ? "function" : "variable", name, name, exported, {
126
+ isAsync: isFunction && value.children.some((child) => child.type === "async"),
127
+ docSource: statement,
128
+ });
129
+ }
130
+ }
131
+ };
132
+
133
+ const collectClassMembers = (classNode, classExported) => {
134
+ const className = nameOf(classNode);
135
+ const body = classNode.childForFieldName("body");
136
+ if (body === null) return;
137
+ for (const member of body.namedChildren) {
138
+ if (member.type !== "method_definition") continue;
139
+ const name = member.childForFieldName("name")?.text;
140
+ if (name === undefined) continue;
141
+ const qualified = `${className}.${name}`;
142
+ makeNode(member, "method", name, qualified, classExported, { isAsync: isAsync(member) });
143
+ }
144
+ };
145
+
146
+ const collectHeritage = (classNode, _exported) => {
147
+ const heritage = classNode.namedChildren.find((child) => child.type === "class_heritage");
148
+ if (heritage === undefined) return;
149
+ const sourceName = nameOf(classNode);
150
+ if (sourceName === undefined) return;
151
+ const sourceId = `${filePath}#${sourceName}`;
152
+ for (const clause of heritage.namedChildren) {
153
+ const kind = clause.type === "extends_clause" ? "extends" : clause.type === "implements_clause" ? "implements" : undefined;
154
+ if (kind === undefined) continue;
155
+ for (const typeNode of clause.namedChildren.filter((child) => child.type === "type_identifier" || child.type === "identifier")) {
156
+ const typeName = typeNode.text;
157
+ if (localNames.has(typeName)) {
158
+ edges.push({ source: sourceId, target: localNames.get(typeName), kind, line: typeNode.startPosition.row + 1, col: typeNode.startPosition.column });
159
+ } else {
160
+ refs.push({
161
+ fromNodeId: sourceId, referenceName: typeName, referenceKind: kind,
162
+ line: typeNode.startPosition.row + 1, col: typeNode.startPosition.column,
163
+ filePath, moduleSpecifier: undefined,
164
+ });
165
+ }
166
+ }
167
+ }
168
+ };
169
+
170
+ // Pass 1 — top-level declarations and imports.
171
+ for (const statement of tree.rootNode.namedChildren) {
172
+ if (statement.type === "import_statement") {
173
+ const sourceNode = statement.childForFieldName("source");
174
+ const moduleSpecifier = sourceNode === null ? undefined : sourceNode.text.replace(/^["']|["']$/g, "");
175
+ for (const clause of statement.namedChildren.filter((child) => child.type === "import_clause")) {
176
+ for (const named of clause.namedChildren.filter((child) => child.type === "named_imports")) {
177
+ for (const specifier of named.namedChildren.filter((child) => child.type === "import_specifier")) {
178
+ const nameNode = specifier.childForFieldName("name");
179
+ if (nameNode === null) continue;
180
+ const importedName = nameNode.text;
181
+ imports.set(importedName, moduleSpecifier);
182
+ refs.push({
183
+ fromNodeId: null, referenceName: importedName, referenceKind: "import",
184
+ line: nameNode.startPosition.row + 1, col: nameNode.startPosition.column,
185
+ filePath, moduleSpecifier,
186
+ });
187
+ }
188
+ }
189
+ const defaultName = clause.namedChildren.find((child) => child.type === "identifier");
190
+ if (defaultName !== undefined) {
191
+ imports.set(defaultName.text, moduleSpecifier);
192
+ refs.push({
193
+ fromNodeId: null, referenceName: defaultName.text, referenceKind: "import",
194
+ line: defaultName.startPosition.row + 1, col: defaultName.startPosition.column,
195
+ filePath, moduleSpecifier,
196
+ });
197
+ }
198
+ }
199
+ continue;
200
+ }
201
+ collectDeclaration(statement, statement.type === "export_statement");
202
+ }
203
+
204
+ // Pass 2 — call expressions anywhere in the file.
205
+ const enclosingScope = (node) => {
206
+ let cursor = node.parent;
207
+ while (cursor !== null) {
208
+ const record = scopeByNodeId.get(cursor.id);
209
+ if (record !== undefined) return record;
210
+ cursor = cursor.parent;
211
+ }
212
+ return undefined;
213
+ };
214
+
215
+ const visitCalls = (node) => {
216
+ if (node.type === "call_expression") {
217
+ const callee = node.childForFieldName("function");
218
+ if (callee !== null && callee.type === "identifier") {
219
+ const name = callee.text;
220
+ const scope = enclosingScope(node);
221
+ if (scope !== undefined) {
222
+ if (localNames.has(name) && localNames.get(name) !== scope.id) {
223
+ edges.push({ source: scope.id, target: localNames.get(name), kind: "calls", line: callee.startPosition.row + 1, col: callee.startPosition.column });
224
+ } else if (!localNames.has(name)) {
225
+ refs.push({
226
+ fromNodeId: scope.id, referenceName: name, referenceKind: "call",
227
+ line: callee.startPosition.row + 1, col: callee.startPosition.column,
228
+ filePath, moduleSpecifier: imports.get(name),
229
+ });
230
+ }
231
+ }
232
+ }
233
+ }
234
+ for (const child of node.namedChildren) visitCalls(child);
235
+ };
236
+ visitCalls(tree.rootNode);
237
+
238
+ return { nodes, edges, refs, imports: Object.fromEntries(imports) };
239
+ }