@handsupmin/gc-tree 0.1.1
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/LICENSE +21 -0
- package/README.es.md +166 -0
- package/README.ja.md +166 -0
- package/README.ko.md +166 -0
- package/README.md +166 -0
- package/README.zh.md +166 -0
- package/dist/src/cli.js +360 -0
- package/dist/src/markdown.js +81 -0
- package/dist/src/onboard.js +36 -0
- package/dist/src/onboarding-protocol.js +38 -0
- package/dist/src/paths.js +28 -0
- package/dist/src/proposals.js +73 -0
- package/dist/src/provider.js +121 -0
- package/dist/src/repo-map.js +149 -0
- package/dist/src/resolve.js +38 -0
- package/dist/src/scaffold.js +199 -0
- package/dist/src/settings.js +30 -0
- package/dist/src/store.js +149 -0
- package/dist/src/types.js +1 -0
- package/dist/src/update.js +26 -0
- package/docs/concept.es.md +81 -0
- package/docs/concept.ja.md +81 -0
- package/docs/concept.ko.md +81 -0
- package/docs/concept.md +81 -0
- package/docs/concept.zh.md +81 -0
- package/docs/local-development.es.md +83 -0
- package/docs/local-development.ja.md +83 -0
- package/docs/local-development.ko.md +83 -0
- package/docs/local-development.md +83 -0
- package/docs/local-development.zh.md +83 -0
- package/docs/principles.es.md +49 -0
- package/docs/principles.ja.md +49 -0
- package/docs/principles.ko.md +49 -0
- package/docs/principles.md +50 -0
- package/docs/principles.zh.md +49 -0
- package/docs/usage.es.md +119 -0
- package/docs/usage.ja.md +119 -0
- package/docs/usage.ko.md +119 -0
- package/docs/usage.md +121 -0
- package/docs/usage.zh.md +119 -0
- package/package.json +32 -0
- package/skills/checkout/SKILL.md +17 -0
- package/skills/onboard/SKILL.md +74 -0
- package/skills/reset-gc-branch/SKILL.md +14 -0
- package/skills/resolve-context/SKILL.md +21 -0
- package/skills/update-global-context/SKILL.md +21 -0
package/README.zh.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# gc-tree
|
|
2
|
+
|
|
3
|
+
面向 AI 编码工具的分支化全局上下文层。
|
|
4
|
+
|
|
5
|
+
[English](README.md) | [한국어](README.ko.md) | [简体中文](README.zh.md) | [日本語](README.ja.md) | [Español](README.es.md)
|
|
6
|
+
|
|
7
|
+
## 介绍
|
|
8
|
+
|
|
9
|
+
`gctree` 是一个面向 AI 编码工具的轻量级 **Global Context Tree**。
|
|
10
|
+
它为长期上下文提供了一个可复用、基于文件、支持分支的明确归宿,而且可以自然接入现有工作流。
|
|
11
|
+
|
|
12
|
+
当单个 `AGENTS.md`、`CLAUDE.md` 或一段提示词已经不够时,`gctree` 可以帮助你:
|
|
13
|
+
|
|
14
|
+
- 按产品、客户或工作流隔离上下文
|
|
15
|
+
- 用 markdown 文档保存 source of truth,而不是依赖隐藏记忆
|
|
16
|
+
- 通过精简索引和 summary-first 文档更快定位当前上下文
|
|
17
|
+
- 通过偏好的 LLM CLI 完成 onboarding 与长期更新
|
|
18
|
+
- 让某个 gc-branch 只作用于真正相关的仓库
|
|
19
|
+
|
|
20
|
+
## 简要特性
|
|
21
|
+
|
|
22
|
+
- **Provider 驱动的 onboarding**
|
|
23
|
+
`gctree init` 会先询问你想使用哪种 provider 模式(`claude-code`、`codex` 或 `both`),再询问响应语言,并保存这些选择,然后给当前环境安装对应的命令表面,并为默认 `main` gc-branch 启动引导式 onboarding。
|
|
24
|
+
- **带仓库范围的 gc-branch**
|
|
25
|
+
你可以通过 `~/.gctree/branch-repo-map.json` 把某个 gc-branch 绑定到指定仓库集合,比如让 A 只作用于 B/C/D,而在 F 中忽略。
|
|
26
|
+
- **交互式范围保护**
|
|
27
|
+
如果 `gctree resolve` 发现当前仓库还没有映射到这个 gc-branch,它可以询问是仅本次继续、以后总是使用,还是在这个仓库里忽略它。
|
|
28
|
+
- **Summary-first 文档结构**
|
|
29
|
+
工具可以先读取简短摘要,只有在需要时才展开完整文档。
|
|
30
|
+
- **引导式长期更新**
|
|
31
|
+
不需要手写 JSON 文件,也可以通过同样的 provider 流程更新全局上下文。
|
|
32
|
+
|
|
33
|
+
## 安装与快速开始
|
|
34
|
+
|
|
35
|
+
### 通过 npm 安装
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g @handsupmin/gc-tree
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
如果你只想临时运行一次,也可以:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx @handsupmin/gc-tree init
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **包名:** `@handsupmin/gc-tree`
|
|
48
|
+
- **CLI 命令:** `gctree`
|
|
49
|
+
- **要求:** Node.js 20+
|
|
50
|
+
如果要从源码开发,请查看 [docs/local-development.md](docs/local-development.md)。
|
|
51
|
+
|
|
52
|
+
### 快速开始
|
|
53
|
+
|
|
54
|
+
#### 1) 初始化 gc-tree
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
gctree init
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
这个命令会:
|
|
61
|
+
|
|
62
|
+
- 创建 `~/.gctree`
|
|
63
|
+
- 创建默认 `main` gc-branch
|
|
64
|
+
- 让你选择 provider 模式(`claude-code`、`codex` 或 `both`)
|
|
65
|
+
- 如果你选择 `both`,再选择这次 onboarding 要由哪个 provider 启动
|
|
66
|
+
- 让你选择语言(`English`、`Korean`,或手动输入语言)
|
|
67
|
+
- 把 provider 模式、实际 onboarding provider 和语言保存到 `~/.gctree/settings.json`
|
|
68
|
+
- 为当前环境安装对应的命令表面
|
|
69
|
+
- 当 `main` 仍为空时,为当前 gc-branch 启动引导式 onboarding
|
|
70
|
+
|
|
71
|
+
#### 2) 解析当前上下文
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
gctree resolve --query "auth token rotation"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
如果当前仓库不在这个 gc-branch 的映射范围内,`gctree` 可以让你选择:
|
|
78
|
+
|
|
79
|
+
1. 这次先继续
|
|
80
|
+
2. 以后这个仓库总是使用这个 gc-branch
|
|
81
|
+
3. 在这个仓库里忽略这个 gc-branch
|
|
82
|
+
|
|
83
|
+
如果选择 2 或 3,`~/.gctree/branch-repo-map.json` 会被更新。
|
|
84
|
+
|
|
85
|
+
#### 3) 需要独立上下文时创建新的 gc-branch
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
gctree checkout -b client-b
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`checkout -b` 会创建一个**新的空 gc-branch**,不会复制已有分支里的文档。
|
|
92
|
+
|
|
93
|
+
#### 4) 为一个空的 gc-branch 执行 onboarding
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
gctree onboard
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### 5) 后续执行长期上下文更新
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
gctree update-global-context
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
简短别名:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
gctree update-gc
|
|
109
|
+
gctree ugc
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
如果实际工作后发现某个仓库确实属于当前 gc-branch,更自然的流程是:
|
|
113
|
+
|
|
114
|
+
1. 先把这个仓库加入 branch repo map
|
|
115
|
+
2. 再用 `gctree update-global-context` 补充这个仓库是什么、为什么重要等长期上下文
|
|
116
|
+
|
|
117
|
+
#### 6) 重新 onboarding 前先重置 gc-branch
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
gctree reset-gc-branch --branch client-b --yes
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 运行时可见的 provider 命令
|
|
124
|
+
|
|
125
|
+
scaffold 之后,在运行时可见的命令是:
|
|
126
|
+
|
|
127
|
+
- **Codex:** `$gc-onboard`, `$gc-update-global-context`
|
|
128
|
+
- **Claude Code:** `/gc-onboard`, `/gc-update-global-context`
|
|
129
|
+
|
|
130
|
+
这些命令在开始收集或更新长期上下文之前,都应该先明确说明当前激活的是哪个 gc-branch,并且除非用户明确要求切换,否则要始终使用保存下来的语言。
|
|
131
|
+
|
|
132
|
+
### 核心命令速览
|
|
133
|
+
|
|
134
|
+
| 目标 | 命令 |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| 初始化 gc-tree 并选择 provider | `gctree init` |
|
|
137
|
+
| 确认当前 gc-branch | `gctree status` |
|
|
138
|
+
| 搜索当前上下文 | `gctree resolve --query "..."` |
|
|
139
|
+
| 查看仓库范围规则 | `gctree repo-map` |
|
|
140
|
+
| 为 gc-branch 显式设置仓库包含/排除 | `gctree set-repo-scope --branch <name> --include` / `--exclude` |
|
|
141
|
+
| 创建或切换 gc-branch | `gctree checkout <branch>` / `gctree checkout -b <branch>` |
|
|
142
|
+
| 为一个空的 gc-branch 执行 onboarding | `gctree onboard` |
|
|
143
|
+
| 为当前 gc-branch 执行长期更新 | `gctree update-global-context` / `gctree update-gc` / `gctree ugc` |
|
|
144
|
+
| 重新 onboarding 前重置 gc-branch | `gctree reset-gc-branch --branch <name> --yes` |
|
|
145
|
+
| 手动为其他环境安装 scaffold | `gctree scaffold --host codex --target /path/to/repo` |
|
|
146
|
+
|
|
147
|
+
## 文档
|
|
148
|
+
|
|
149
|
+
详细文档位于 [`docs/`](docs) 目录下。
|
|
150
|
+
|
|
151
|
+
- **概念** — [`docs/concept.zh.md`](docs/concept.zh.md)
|
|
152
|
+
说明 `gctree` 是什么、解决什么问题,以及全局上下文层的边界。
|
|
153
|
+
- **原理** — [`docs/principles.zh.md`](docs/principles.zh.md)
|
|
154
|
+
介绍 gc-branch、仓库范围、summary-first 文档和引导式更新等原则。
|
|
155
|
+
- **使用方法** — [`docs/usage.zh.md`](docs/usage.zh.md)
|
|
156
|
+
介绍标准 CLI 流程、provider 命令、仓库范围行为和集成方式。
|
|
157
|
+
- **本地运行方法** — [`docs/local-development.zh.md`](docs/local-development.zh.md)
|
|
158
|
+
说明依赖安装、本地运行 CLI 与提交前验证方式。
|
|
159
|
+
|
|
160
|
+
## 贡献
|
|
161
|
+
|
|
162
|
+
欢迎贡献。开发流程与 PR 检查清单位于英文文档 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
163
|
+
|
|
164
|
+
## 许可证
|
|
165
|
+
|
|
166
|
+
MIT。详见 [`LICENSE`](LICENSE)。
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { stdin } from 'node:process';
|
|
4
|
+
import { onboardBranch } from './onboard.js';
|
|
5
|
+
import { buildProviderLaunchPlan, maybeLaunchProvider, promptLanguageSelection, promptLaunchProviderSelection, promptProviderSelection, } from './provider.js';
|
|
6
|
+
import { DEFAULT_BRANCH, resolveHome } from './paths.js';
|
|
7
|
+
import { branchRepoMapPath, branchScopeStatus, detectCurrentRepoId, promptResolveScopeDecision, readBranchRepoMap, resolveBranchForRepo, setRepoScopeForBranch, } from './repo-map.js';
|
|
8
|
+
import { resolveContext } from './resolve.js';
|
|
9
|
+
import { scaffoldHostIntegration } from './scaffold.js';
|
|
10
|
+
import { requirePreferredProvider, writeSettings, readSettings } from './settings.js';
|
|
11
|
+
import { checkoutBranch, initHome, listBranches, readHead, resetBranchContext, statusForBranch, ensureBranchExists, isBranchContextEmpty } from './store.js';
|
|
12
|
+
import { updateBranchContext } from './update.js';
|
|
13
|
+
function readArg(flag) {
|
|
14
|
+
const index = process.argv.indexOf(flag);
|
|
15
|
+
return index === -1 ? undefined : process.argv[index + 1];
|
|
16
|
+
}
|
|
17
|
+
function hasFlag(flag) {
|
|
18
|
+
return process.argv.includes(flag);
|
|
19
|
+
}
|
|
20
|
+
function usage() {
|
|
21
|
+
console.error(`Usage:
|
|
22
|
+
gctree init [--home DIR] [--provider <claude-code|codex|both>] [--language TEXT] [--target DIR] [--no-launch]
|
|
23
|
+
gctree checkout <branch> [--home DIR]
|
|
24
|
+
gctree checkout -b <branch> [--home DIR]
|
|
25
|
+
gctree branches [--home DIR]
|
|
26
|
+
gctree repo-map [--home DIR]
|
|
27
|
+
gctree set-repo-scope --branch NAME [--repo NAME] [--cwd DIR] (--include|--exclude) [--home DIR]
|
|
28
|
+
gctree status [--home DIR] [--cwd DIR]
|
|
29
|
+
gctree onboard [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
|
|
30
|
+
gctree reset-gc-branch [--home DIR] [--branch NAME] --yes
|
|
31
|
+
gctree resolve --query TEXT [--home DIR] [--branch NAME] [--cwd DIR]
|
|
32
|
+
gctree update-global-context [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
|
|
33
|
+
gctree update-gc [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
|
|
34
|
+
gctree ugc [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
|
|
35
|
+
gctree scaffold --host <codex|claude-code|both> [--target DIR] [--force]
|
|
36
|
+
|
|
37
|
+
Internal commands:
|
|
38
|
+
gctree __apply-onboarding --input FILE [--home DIR] [--branch NAME]
|
|
39
|
+
gctree __apply-update --input FILE [--home DIR] [--branch NAME]
|
|
40
|
+
`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
async function readJsonFile(path) {
|
|
44
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
45
|
+
}
|
|
46
|
+
function normalizeProvider(value) {
|
|
47
|
+
if (!value)
|
|
48
|
+
return undefined;
|
|
49
|
+
if (value === 'codex' || value === 'claude-code')
|
|
50
|
+
return value;
|
|
51
|
+
throw new Error(`unsupported provider: ${value}`);
|
|
52
|
+
}
|
|
53
|
+
function normalizeProviderMode(value) {
|
|
54
|
+
if (!value)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (value === 'codex' || value === 'claude-code' || value === 'both')
|
|
57
|
+
return value;
|
|
58
|
+
throw new Error(`unsupported provider mode: ${value}`);
|
|
59
|
+
}
|
|
60
|
+
async function resolvePreferredProvider(home, explicitProvider) {
|
|
61
|
+
return normalizeProvider(explicitProvider) || (await requirePreferredProvider(home));
|
|
62
|
+
}
|
|
63
|
+
async function ensureScaffold({ providerMode, targetDir, force = false, }) {
|
|
64
|
+
const hosts = providerMode === 'both' ? ['claude-code', 'codex'] : [providerMode];
|
|
65
|
+
const combined = { hosts: providerMode, target_dir: targetDir, written: [], skipped_existing: [] };
|
|
66
|
+
for (const host of hosts) {
|
|
67
|
+
const result = await scaffoldHostIntegration({ host, targetDir, force });
|
|
68
|
+
combined.written.push(...result.written);
|
|
69
|
+
combined.skipped_existing.push(...result.skipped_existing);
|
|
70
|
+
}
|
|
71
|
+
return combined;
|
|
72
|
+
}
|
|
73
|
+
async function maybePromptProviderMode(explicitProvider) {
|
|
74
|
+
const provider = normalizeProviderMode(explicitProvider);
|
|
75
|
+
if (provider)
|
|
76
|
+
return provider;
|
|
77
|
+
if (!stdin.isTTY)
|
|
78
|
+
return 'claude-code';
|
|
79
|
+
return promptProviderSelection();
|
|
80
|
+
}
|
|
81
|
+
async function maybePromptLanguage(explicitLanguage) {
|
|
82
|
+
if (explicitLanguage?.trim())
|
|
83
|
+
return explicitLanguage.trim();
|
|
84
|
+
if (!stdin.isTTY)
|
|
85
|
+
return 'English';
|
|
86
|
+
return promptLanguageSelection();
|
|
87
|
+
}
|
|
88
|
+
async function resolveLaunchProvider(providerMode) {
|
|
89
|
+
if (providerMode === 'both') {
|
|
90
|
+
if (!stdin.isTTY)
|
|
91
|
+
return 'claude-code';
|
|
92
|
+
return promptLaunchProviderSelection();
|
|
93
|
+
}
|
|
94
|
+
return providerMode;
|
|
95
|
+
}
|
|
96
|
+
async function launchGuidedFlow({ provider, providerMode, preferredLanguage, targetDir, gcBranch, command, noLaunch, }) {
|
|
97
|
+
const plan = buildProviderLaunchPlan({ provider, providerMode, preferredLanguage, targetDir, gcBranch, command });
|
|
98
|
+
if (noLaunch)
|
|
99
|
+
return plan;
|
|
100
|
+
return maybeLaunchProvider(plan);
|
|
101
|
+
}
|
|
102
|
+
async function main() {
|
|
103
|
+
const rawCommand = process.argv[2];
|
|
104
|
+
if (!rawCommand || rawCommand === '--help' || rawCommand === 'help')
|
|
105
|
+
usage();
|
|
106
|
+
const command = rawCommand === 'update-gc' || rawCommand === 'ugc' ? 'update-global-context' : rawCommand;
|
|
107
|
+
const home = resolveHome(readArg('--home'));
|
|
108
|
+
switch (command) {
|
|
109
|
+
case 'init': {
|
|
110
|
+
const providerMode = await maybePromptProviderMode(readArg('--provider'));
|
|
111
|
+
const provider = await resolveLaunchProvider(providerMode);
|
|
112
|
+
const preferredLanguage = await maybePromptLanguage(readArg('--language'));
|
|
113
|
+
const targetDir = readArg('--target') || process.cwd();
|
|
114
|
+
const result = await initHome(home);
|
|
115
|
+
const settings = await writeSettings({
|
|
116
|
+
home,
|
|
117
|
+
providerMode,
|
|
118
|
+
preferredProvider: provider,
|
|
119
|
+
preferredLanguage,
|
|
120
|
+
});
|
|
121
|
+
const scaffold = await ensureScaffold({ providerMode, targetDir, force: hasFlag('--force') });
|
|
122
|
+
const launch = (await isBranchContextEmpty(home, result.gc_branch))
|
|
123
|
+
? await launchGuidedFlow({
|
|
124
|
+
provider,
|
|
125
|
+
providerMode,
|
|
126
|
+
preferredLanguage,
|
|
127
|
+
targetDir,
|
|
128
|
+
gcBranch: result.gc_branch,
|
|
129
|
+
command: 'gc-onboard',
|
|
130
|
+
noLaunch: hasFlag('--no-launch'),
|
|
131
|
+
})
|
|
132
|
+
: null;
|
|
133
|
+
console.log(JSON.stringify({
|
|
134
|
+
...result,
|
|
135
|
+
provider_mode: settings.provider_mode,
|
|
136
|
+
preferred_provider: settings.preferred_provider,
|
|
137
|
+
preferred_language: settings.preferred_language,
|
|
138
|
+
scaffold,
|
|
139
|
+
launch,
|
|
140
|
+
}, null, 2));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
case 'checkout': {
|
|
144
|
+
const create = hasFlag('-b');
|
|
145
|
+
const branch = create ? process.argv[4] : process.argv[3];
|
|
146
|
+
if (!branch)
|
|
147
|
+
usage();
|
|
148
|
+
const result = await checkoutBranch(home, branch, create);
|
|
149
|
+
console.log(JSON.stringify(result, null, 2));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
case 'branches': {
|
|
153
|
+
const result = await listBranches(home);
|
|
154
|
+
console.log(JSON.stringify(result, null, 2));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
case 'repo-map': {
|
|
158
|
+
const mapping = await readBranchRepoMap(home);
|
|
159
|
+
console.log(JSON.stringify({ path: branchRepoMapPath(home), mapping }, null, 2));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
case 'set-repo-scope': {
|
|
163
|
+
const gcBranch = readArg('--branch');
|
|
164
|
+
if (!gcBranch)
|
|
165
|
+
usage();
|
|
166
|
+
await ensureBranchExists(home, gcBranch);
|
|
167
|
+
const repo = readArg('--repo') || (await detectCurrentRepoId(readArg('--cwd') || process.cwd()));
|
|
168
|
+
if (!repo) {
|
|
169
|
+
throw new Error('could not determine the current repo. Pass --repo explicitly or run this inside a git repository.');
|
|
170
|
+
}
|
|
171
|
+
const include = hasFlag('--include');
|
|
172
|
+
const exclude = hasFlag('--exclude');
|
|
173
|
+
if (include === exclude) {
|
|
174
|
+
throw new Error('set-repo-scope requires exactly one of --include or --exclude.');
|
|
175
|
+
}
|
|
176
|
+
const mapping = await setRepoScopeForBranch({
|
|
177
|
+
home,
|
|
178
|
+
branch: gcBranch,
|
|
179
|
+
repo,
|
|
180
|
+
mode: include ? 'include' : 'exclude',
|
|
181
|
+
});
|
|
182
|
+
console.log(JSON.stringify({
|
|
183
|
+
path: branchRepoMapPath(home),
|
|
184
|
+
branch: gcBranch,
|
|
185
|
+
repo,
|
|
186
|
+
mode: include ? 'include' : 'exclude',
|
|
187
|
+
mapping,
|
|
188
|
+
}, null, 2));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
case 'status': {
|
|
192
|
+
const gcBranch = (await readHead(home)) || DEFAULT_BRANCH;
|
|
193
|
+
const result = await statusForBranch(home, gcBranch);
|
|
194
|
+
const settings = await readSettings(home);
|
|
195
|
+
const mapping = await readBranchRepoMap(home);
|
|
196
|
+
const currentRepo = await detectCurrentRepoId(readArg('--cwd') || process.cwd());
|
|
197
|
+
console.log(JSON.stringify({
|
|
198
|
+
...result,
|
|
199
|
+
provider_mode: settings?.provider_mode || null,
|
|
200
|
+
preferred_provider: settings?.preferred_provider || null,
|
|
201
|
+
preferred_language: settings?.preferred_language || null,
|
|
202
|
+
current_repo: currentRepo,
|
|
203
|
+
branch_repo_map_path: branchRepoMapPath(home),
|
|
204
|
+
repo_scope_status: branchScopeStatus(mapping, gcBranch, currentRepo),
|
|
205
|
+
}, null, 2));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
case 'onboard': {
|
|
209
|
+
const gcBranch = readArg('--branch') || (await readHead(home)) || DEFAULT_BRANCH;
|
|
210
|
+
await ensureBranchExists(home, gcBranch);
|
|
211
|
+
if (!(await isBranchContextEmpty(home, gcBranch))) {
|
|
212
|
+
throw new Error(`gc-branch is not empty: ${gcBranch}. Run \`gctree reset-gc-branch --branch ${gcBranch} --yes\` to re-onboard it, or use \`gctree update-global-context\` to make a guided durable update.`);
|
|
213
|
+
}
|
|
214
|
+
const provider = await resolvePreferredProvider(home, readArg('--provider'));
|
|
215
|
+
const settings = await readSettings(home);
|
|
216
|
+
const targetDir = readArg('--target') || process.cwd();
|
|
217
|
+
const scaffold = await ensureScaffold({ providerMode: settings?.provider_mode || provider, targetDir, force: hasFlag('--force') });
|
|
218
|
+
const launch = await launchGuidedFlow({
|
|
219
|
+
provider,
|
|
220
|
+
providerMode: settings?.provider_mode || provider,
|
|
221
|
+
preferredLanguage: settings?.preferred_language || 'English',
|
|
222
|
+
targetDir,
|
|
223
|
+
gcBranch,
|
|
224
|
+
command: 'gc-onboard',
|
|
225
|
+
noLaunch: hasFlag('--no-launch'),
|
|
226
|
+
});
|
|
227
|
+
console.log(JSON.stringify({ mode: 'guided_onboarding', gc_branch: gcBranch, preferred_provider: provider, scaffold, launch }, null, 2));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
case '__apply-onboarding': {
|
|
231
|
+
const inputPath = readArg('--input');
|
|
232
|
+
if (!inputPath)
|
|
233
|
+
usage();
|
|
234
|
+
const input = await readJsonFile(inputPath);
|
|
235
|
+
const gcBranch = readArg('--branch') || input.branch || (await readHead(home)) || DEFAULT_BRANCH;
|
|
236
|
+
if (!(await isBranchContextEmpty(home, gcBranch))) {
|
|
237
|
+
throw new Error(`gc-branch is not empty: ${gcBranch}. Run \`gctree reset-gc-branch --branch ${gcBranch} --yes\` to re-onboard it, or use \`gctree update-global-context\` instead.`);
|
|
238
|
+
}
|
|
239
|
+
const result = await onboardBranch({ home, input, branch: gcBranch });
|
|
240
|
+
console.log(JSON.stringify(result, null, 2));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
case 'reset-gc-branch': {
|
|
244
|
+
if (!hasFlag('--yes')) {
|
|
245
|
+
throw new Error('reset-gc-branch is destructive. Re-run with --yes to confirm.');
|
|
246
|
+
}
|
|
247
|
+
const gcBranch = readArg('--branch') || (await readHead(home)) || DEFAULT_BRANCH;
|
|
248
|
+
const result = await resetBranchContext(home, gcBranch);
|
|
249
|
+
console.log(JSON.stringify(result, null, 2));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
case 'resolve': {
|
|
253
|
+
const query = readArg('--query');
|
|
254
|
+
if (!query)
|
|
255
|
+
usage();
|
|
256
|
+
const head = (await readHead(home)) || DEFAULT_BRANCH;
|
|
257
|
+
const resolved = await resolveBranchForRepo({
|
|
258
|
+
home,
|
|
259
|
+
head,
|
|
260
|
+
explicitBranch: readArg('--branch') || undefined,
|
|
261
|
+
cwd: readArg('--cwd') || process.cwd(),
|
|
262
|
+
});
|
|
263
|
+
let gcBranch = resolved.gc_branch;
|
|
264
|
+
const currentRepo = resolved.current_repo;
|
|
265
|
+
let scopeStatus = resolved.scope_status;
|
|
266
|
+
const settings = await readSettings(home);
|
|
267
|
+
if (currentRepo && scopeStatus === 'unmapped') {
|
|
268
|
+
const decision = await promptResolveScopeDecision(gcBranch, currentRepo, settings?.preferred_language || 'English');
|
|
269
|
+
if (decision === 'always-use') {
|
|
270
|
+
await setRepoScopeForBranch({ home, branch: gcBranch, repo: currentRepo, mode: 'include' });
|
|
271
|
+
scopeStatus = 'included';
|
|
272
|
+
}
|
|
273
|
+
else if (decision === 'ignore') {
|
|
274
|
+
await setRepoScopeForBranch({ home, branch: gcBranch, repo: currentRepo, mode: 'exclude' });
|
|
275
|
+
console.log(JSON.stringify({
|
|
276
|
+
gc_branch: gcBranch,
|
|
277
|
+
query,
|
|
278
|
+
current_repo: currentRepo,
|
|
279
|
+
source: resolved.source,
|
|
280
|
+
repo_scope_status: 'excluded',
|
|
281
|
+
matches: [],
|
|
282
|
+
}, null, 2));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (currentRepo && scopeStatus === 'excluded') {
|
|
287
|
+
console.log(JSON.stringify({
|
|
288
|
+
gc_branch: gcBranch,
|
|
289
|
+
query,
|
|
290
|
+
current_repo: currentRepo,
|
|
291
|
+
source: resolved.source,
|
|
292
|
+
repo_scope_status: 'excluded',
|
|
293
|
+
matches: [],
|
|
294
|
+
}, null, 2));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const result = await resolveContext({ home, branch: gcBranch, query });
|
|
298
|
+
console.log(JSON.stringify({
|
|
299
|
+
gc_branch: result.branch,
|
|
300
|
+
query: result.query,
|
|
301
|
+
current_repo: currentRepo,
|
|
302
|
+
source: resolved.source,
|
|
303
|
+
repo_scope_status: scopeStatus,
|
|
304
|
+
matches: result.matches,
|
|
305
|
+
}, null, 2));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
case 'update-global-context': {
|
|
309
|
+
const gcBranch = readArg('--branch') || (await readHead(home)) || DEFAULT_BRANCH;
|
|
310
|
+
await ensureBranchExists(home, gcBranch);
|
|
311
|
+
if (await isBranchContextEmpty(home, gcBranch)) {
|
|
312
|
+
throw new Error(`gc-branch is empty: ${gcBranch}. Run \`gctree onboard --branch ${gcBranch}\` to create its context first.`);
|
|
313
|
+
}
|
|
314
|
+
const provider = await resolvePreferredProvider(home, readArg('--provider'));
|
|
315
|
+
const settings = await readSettings(home);
|
|
316
|
+
const targetDir = readArg('--target') || process.cwd();
|
|
317
|
+
const scaffold = await ensureScaffold({ providerMode: settings?.provider_mode || provider, targetDir, force: hasFlag('--force') });
|
|
318
|
+
const launch = await launchGuidedFlow({
|
|
319
|
+
provider,
|
|
320
|
+
providerMode: settings?.provider_mode || provider,
|
|
321
|
+
preferredLanguage: settings?.preferred_language || 'English',
|
|
322
|
+
targetDir,
|
|
323
|
+
gcBranch,
|
|
324
|
+
command: 'gc-update-global-context',
|
|
325
|
+
noLaunch: hasFlag('--no-launch'),
|
|
326
|
+
});
|
|
327
|
+
console.log(JSON.stringify({ mode: 'guided_update', gc_branch: gcBranch, preferred_provider: provider, scaffold, launch }, null, 2));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
case '__apply-update': {
|
|
331
|
+
const inputPath = readArg('--input');
|
|
332
|
+
if (!inputPath)
|
|
333
|
+
usage();
|
|
334
|
+
const input = await readJsonFile(inputPath);
|
|
335
|
+
const gcBranch = readArg('--branch') || input.branch || (await readHead(home)) || DEFAULT_BRANCH;
|
|
336
|
+
const result = await updateBranchContext({ home, input, branch: gcBranch });
|
|
337
|
+
console.log(JSON.stringify(result, null, 2));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
case 'scaffold': {
|
|
341
|
+
const host = normalizeProviderMode(readArg('--host'));
|
|
342
|
+
if (!host)
|
|
343
|
+
usage();
|
|
344
|
+
const targetDir = readArg('--target') || process.cwd();
|
|
345
|
+
const result = await ensureScaffold({
|
|
346
|
+
providerMode: host,
|
|
347
|
+
targetDir,
|
|
348
|
+
force: hasFlag('--force'),
|
|
349
|
+
});
|
|
350
|
+
console.log(JSON.stringify(result, null, 2));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
default:
|
|
354
|
+
usage();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
await main().catch((error) => {
|
|
358
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
359
|
+
process.exit(1);
|
|
360
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export function slugify(value) {
|
|
2
|
+
const normalized = String(value || '')
|
|
3
|
+
.trim()
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
6
|
+
.replace(/-+/g, '-')
|
|
7
|
+
.replace(/^-|-$/g, '');
|
|
8
|
+
return normalized || 'doc';
|
|
9
|
+
}
|
|
10
|
+
export function ensureSummary(summary) {
|
|
11
|
+
const trimmed = String(summary || '').trim();
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
throw new Error('summary is required for every source-of-truth doc');
|
|
14
|
+
}
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
17
|
+
export function renderDocMarkdown(doc) {
|
|
18
|
+
const summary = ensureSummary(doc.summary);
|
|
19
|
+
const body = String(doc.body || '').trim();
|
|
20
|
+
return [
|
|
21
|
+
`# ${doc.title.trim()}`,
|
|
22
|
+
'',
|
|
23
|
+
'## Summary',
|
|
24
|
+
'',
|
|
25
|
+
summary,
|
|
26
|
+
'',
|
|
27
|
+
...(doc.tags && doc.tags.length > 0
|
|
28
|
+
? ['## Tags', '', ...doc.tags.map((tag) => `- ${tag}`), '']
|
|
29
|
+
: []),
|
|
30
|
+
'## Details',
|
|
31
|
+
'',
|
|
32
|
+
body || '(no details yet)',
|
|
33
|
+
'',
|
|
34
|
+
].join('\n');
|
|
35
|
+
}
|
|
36
|
+
export function renderIndexMarkdown(input) {
|
|
37
|
+
const lines = [
|
|
38
|
+
'# gc-tree Index',
|
|
39
|
+
'',
|
|
40
|
+
`- gc-branch: ${input.branch}`,
|
|
41
|
+
`- summary: ${input.branchSummary.trim()}`,
|
|
42
|
+
'',
|
|
43
|
+
];
|
|
44
|
+
if (input.docs.length === 0) {
|
|
45
|
+
lines.push('- No source docs yet.', '');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
for (const doc of input.docs.sort((a, b) => a.title.localeCompare(b.title))) {
|
|
49
|
+
lines.push(`- ${doc.title} -> ${doc.path}`);
|
|
50
|
+
}
|
|
51
|
+
lines.push('');
|
|
52
|
+
}
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
export function parseIndexEntries(indexContent) {
|
|
56
|
+
return String(indexContent || '')
|
|
57
|
+
.split(/\r?\n/)
|
|
58
|
+
.map((line) => line.trim())
|
|
59
|
+
.filter((line) => /^- .+ -> .+$/.test(line) && !line.startsWith('- gc-branch:') && !line.startsWith('- summary:'))
|
|
60
|
+
.map((line) => {
|
|
61
|
+
const body = line.slice(2);
|
|
62
|
+
const [title, path] = body.split('->').map((part) => part.trim());
|
|
63
|
+
return { title, path };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export function extractSummary(markdown) {
|
|
67
|
+
const match = String(markdown || '').match(/## Summary\s+([\s\S]*?)(?:\n## |$)/);
|
|
68
|
+
return match?.[1]?.trim() || '';
|
|
69
|
+
}
|
|
70
|
+
export function extractExcerpt(markdown, query) {
|
|
71
|
+
const content = String(markdown || '').replace(/\s+/g, ' ').trim();
|
|
72
|
+
if (!content)
|
|
73
|
+
return '';
|
|
74
|
+
const normalized = query.toLowerCase();
|
|
75
|
+
const index = content.toLowerCase().indexOf(normalized);
|
|
76
|
+
if (index === -1)
|
|
77
|
+
return content.slice(0, 140);
|
|
78
|
+
const start = Math.max(0, index - 40);
|
|
79
|
+
const end = Math.min(content.length, index + normalized.length + 80);
|
|
80
|
+
return content.slice(start, end).trim();
|
|
81
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdir, readdir, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { renderDocMarkdown } from './markdown.js';
|
|
4
|
+
import { branchDocsDir, DEFAULT_BRANCH } from './paths.js';
|
|
5
|
+
import { ensureBranchExists, updateBranchMeta, writeIndexFromDocs } from './store.js';
|
|
6
|
+
export async function onboardBranch({ home, input, branch, }) {
|
|
7
|
+
const targetBranch = branch || input.branch || DEFAULT_BRANCH;
|
|
8
|
+
await ensureBranchExists(home, targetBranch);
|
|
9
|
+
await mkdir(branchDocsDir(home, targetBranch), { recursive: true });
|
|
10
|
+
const existing = await readdir(branchDocsDir(home, targetBranch)).catch(() => []);
|
|
11
|
+
await Promise.all(existing
|
|
12
|
+
.filter((file) => file.endsWith('.md'))
|
|
13
|
+
.map((file) => unlink(join(branchDocsDir(home, targetBranch), file))));
|
|
14
|
+
const written = [];
|
|
15
|
+
for (const doc of input.docs) {
|
|
16
|
+
const relativePath = `${doc.slug ? doc.slug : doc.title}`;
|
|
17
|
+
const fileName = `${relativePath
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
21
|
+
.replace(/-+/g, '-')
|
|
22
|
+
.replace(/^-|-$/g, '') || 'doc'}.md`;
|
|
23
|
+
const fullPath = join(branchDocsDir(home, targetBranch), fileName);
|
|
24
|
+
await writeFile(fullPath, renderDocMarkdown(doc), 'utf8');
|
|
25
|
+
written.push(fullPath);
|
|
26
|
+
}
|
|
27
|
+
if (input.branchSummary?.trim()) {
|
|
28
|
+
await updateBranchMeta(home, targetBranch, { summary: input.branchSummary.trim() });
|
|
29
|
+
}
|
|
30
|
+
const index = await writeIndexFromDocs(home, targetBranch);
|
|
31
|
+
return {
|
|
32
|
+
gc_branch: targetBranch,
|
|
33
|
+
docs_written: written,
|
|
34
|
+
index_path: index.index_path,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function onboardingProtocolLines() {
|
|
2
|
+
return [
|
|
3
|
+
'This is **global context onboarding**, not repo-local onboarding.',
|
|
4
|
+
'Ask **one question at a time** and keep using the saved workflow language all the way through.',
|
|
5
|
+
'Wait for the user\'s first answer before you inspect docs, repos, directories, or files on your own.',
|
|
6
|
+
'Start by asking the user to paste or share organized docs or reference material if they have any; otherwise ask what kind of work they mainly do.',
|
|
7
|
+
'Do **not** start with a repo scan, a company guess, or a broad hypothesis built from directories like `~/sources`.',
|
|
8
|
+
'Do **not** ask for a full information dump up front.',
|
|
9
|
+
'If docs, reference material, or reference paths are provided, read those first, summarize your understanding back, ask whether that summary is correct before continuing, and skip any questions the docs already answered well.',
|
|
10
|
+
'If no docs are available, continue from the user\'s own description first and ask for the most relevant repo, path, or file only when needed.',
|
|
11
|
+
'Only inspect docs, repos, directories, or files after the user has pointed you at them or confirmed they matter.',
|
|
12
|
+
'When you inspect, dig only through the user-provided material and the minimum related files needed for the next question.',
|
|
13
|
+
'Do **not** scan broad directories or inspect every source file; prefer docs, READMEs, summaries, and a few pointed paths first.',
|
|
14
|
+
'Then ask whether anything important is still missing or worth adding before you continue into deeper discovery.',
|
|
15
|
+
'When you do present a hypothesis, offer structured numbered confirmations such as: 1. This is basically correct. 2. Part of it is wrong. I will explain what differs. 3. This is the wrong frame. I will explain what you should inspect or how this gc-branch should be approached.',
|
|
16
|
+
'If the user picks 2 or 3, ask only for the delta, correction, or better pointer instead of restarting from scratch.',
|
|
17
|
+
'Do **not** start by asking what one repository does.',
|
|
18
|
+
'Only ask the user who they are, what kind of person they are in the organization, and what work they usually own or lead after the provided docs or description still leave real gaps.',
|
|
19
|
+
'Ask next for one core recurring work type only when the provided docs or description still do not make the work types clear, then ask whether there are more work types to capture.',
|
|
20
|
+
'For each work type, ask how that work shows up day to day.',
|
|
21
|
+
'Only after the work types are clear should you ask which repositories are involved in each work type.',
|
|
22
|
+
'For each repository, ask what role it plays in the work, what directories or files matter most, what the actual workflow is, and what hidden conventions, glossary terms, or boundaries matter.',
|
|
23
|
+
'If a relevant repo, path, or doc is available locally and the user has pointed you to it, inspect it and present a lightweight hypothesis before asking what is correct or wrong about that frame.',
|
|
24
|
+
'Ask whether there are additional repositories for the current work type before moving on.',
|
|
25
|
+
'After repository coverage, ask for company/domain glossary terms and acronyms that should become durable context.',
|
|
26
|
+
'Then ask which verification commands should be treated as defaults for this kind of work.',
|
|
27
|
+
'Synthesize the interview into durable docs such as a role/profile summary, work types, repository roles, glossary, and verification defaults.',
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
export function onboardingCompletionLines() {
|
|
31
|
+
return [
|
|
32
|
+
'After applying the onboarding docs, explicitly list which durable docs were saved.',
|
|
33
|
+
'Then summarize what you now understand from the saved docs instead of stopping at the filenames alone.',
|
|
34
|
+
'Ask whether that final summary matches the user\'s reality, and capture any corrections before you wrap up.',
|
|
35
|
+
'Ask whether anything else should be saved while the context is still fresh.',
|
|
36
|
+
'End with remaining risks, blind spots, or details that still need confirmation, then remind the user that future changes belong in `gctree update-global-context`.',
|
|
37
|
+
];
|
|
38
|
+
}
|