@openturtle/cli 0.3.3 → 0.3.4
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/README.md +32 -11
- package/RELEASE_RUNBOOK.md +193 -0
- package/dist/commands/collaboration.js +88 -4
- package/dist/commands/local-knowledge.js +129 -0
- package/dist/core/auth.js +4 -2
- package/dist/core/auto-update.js +12 -1
- package/dist/index.js +6 -1
- package/dist/installers/agents.js +2 -34
- package/dist/local-knowledge/index.js +398 -0
- package/dist/local-knowledge/library.js +328 -0
- package/dist/local-knowledge/root.js +5 -0
- package/package.json +4 -2
- package/skill/openturtle-cli/SKILL.md +81 -0
package/README.md
CHANGED
|
@@ -53,7 +53,9 @@ ot projects list
|
|
|
53
53
|
ot roster list
|
|
54
54
|
ot context get --project <project-id>
|
|
55
55
|
ot knowledge search "部署"
|
|
56
|
+
ot knowledge local search "部署"
|
|
56
57
|
ot meetings list --project <project-id>
|
|
58
|
+
ot work summary --project <project-id>
|
|
57
59
|
ot worklogs record --summary "完成联调" --type progress
|
|
58
60
|
ot report draft --today
|
|
59
61
|
```
|
|
@@ -80,12 +82,19 @@ ot todos progress <todo-id> --message "完成接口联调,等待验收"
|
|
|
80
82
|
ot worklogs record --summary "完成 Qoder CLI 接入验证" --type progress
|
|
81
83
|
```
|
|
82
84
|
|
|
83
|
-
###
|
|
85
|
+
### 聚合搜索知识
|
|
84
86
|
|
|
85
87
|
```bash
|
|
86
88
|
ot knowledge search "日报"
|
|
87
89
|
```
|
|
88
90
|
|
|
91
|
+
默认同时搜索云端知识和本地全文索引。只搜索本地:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
ot knowledge search "日报" --local-only
|
|
95
|
+
ot knowledge local search "日报"
|
|
96
|
+
```
|
|
97
|
+
|
|
89
98
|
### 上传录音并处理会议
|
|
90
99
|
|
|
91
100
|
```bash
|
|
@@ -184,7 +193,6 @@ ot auth logout
|
|
|
184
193
|
- CLI 项目状态只写 `.openturtle-cli/`
|
|
185
194
|
- CLI 全局状态只写 `~/.openturtle/cli/`
|
|
186
195
|
- 不要创建或写入项目 `.openturtle/`
|
|
187
|
-
- 不要覆盖 Desktop 的 `openturtle-knowledge`
|
|
188
196
|
- 不要覆盖 Desktop 的 `openturtle-automations`
|
|
189
197
|
- 不要覆盖旧的 `openturtle-todo`
|
|
190
198
|
|
|
@@ -327,13 +335,13 @@ ot uninstall --target all
|
|
|
327
335
|
|
|
328
336
|
### 6. Target 安装矩阵
|
|
329
337
|
|
|
330
|
-
| Target | 安装内容
|
|
331
|
-
| ------ |
|
|
332
|
-
| Claude | Skill
|
|
333
|
-
| Codex | Skill、`AGENTS.md` managed note
|
|
334
|
-
| Cursor | Cursor Rule
|
|
335
|
-
| Qoder | Skill
|
|
336
|
-
| Kiro | Steering
|
|
338
|
+
| Target | 安装内容 |
|
|
339
|
+
| ------ | ------------------------------- |
|
|
340
|
+
| Claude | Skill |
|
|
341
|
+
| Codex | Skill、`AGENTS.md` managed note |
|
|
342
|
+
| Cursor | Cursor Rule |
|
|
343
|
+
| Qoder | Skill |
|
|
344
|
+
| Kiro | Steering |
|
|
337
345
|
|
|
338
346
|
### 7. CLI 接入模型
|
|
339
347
|
|
|
@@ -348,6 +356,9 @@ ot projects list --keyword "OpenTurtle"
|
|
|
348
356
|
ot projects resolve "OpenTurtle"
|
|
349
357
|
ot projects show <project-id> --members
|
|
350
358
|
ot roster list
|
|
359
|
+
ot roster list --project <project-id>
|
|
360
|
+
ot work summary --project <project-id>
|
|
361
|
+
ot digest get --project <project-id> --window this_week
|
|
351
362
|
ot context get --project <project-id> --query "本周风险" --section risk
|
|
352
363
|
ot context item meeting <meeting-id> --project <project-id>
|
|
353
364
|
```
|
|
@@ -381,6 +392,8 @@ ot todos review <todo-id> --body-file review-decision.json --dry-run
|
|
|
381
392
|
```bash
|
|
382
393
|
ot knowledge list
|
|
383
394
|
ot knowledge search "日报"
|
|
395
|
+
ot knowledge search "日报" --cloud-only
|
|
396
|
+
ot knowledge search "日报" --local-only
|
|
384
397
|
ot knowledge show <knowledge-id>
|
|
385
398
|
ot knowledge relations <knowledge-id>
|
|
386
399
|
ot knowledge save --body-file knowledge.json --dry-run
|
|
@@ -397,9 +410,16 @@ ot knowledge attachment-download <knowledge-id> <attachment-id> --output ./desig
|
|
|
397
410
|
ot knowledge sources --path docs
|
|
398
411
|
ot knowledge sources --q "WPS"
|
|
399
412
|
ot knowledge source docs/guide.md
|
|
413
|
+
ot knowledge local list
|
|
414
|
+
ot knowledge local search "部署规范"
|
|
415
|
+
ot knowledge local read <asset-id>
|
|
416
|
+
ot knowledge local import ./docs --dry-run
|
|
417
|
+
ot knowledge local create --body-file local-note.json --dry-run
|
|
418
|
+
ot knowledge local update <asset-id> --body-file local-note.json --dry-run
|
|
419
|
+
ot knowledge local remove <asset-id> --dry-run
|
|
400
420
|
```
|
|
401
421
|
|
|
402
|
-
|
|
422
|
+
`ot knowledge search` 返回 `{ query, cloud, local }` 聚合结果;`--cloud-only` 和 `--local-only` 可限定范围。知识对象是可复用的结论、规范、经验、决策沉淀和操作手册。来源文件是知识背后的原文、附件或仓库文档,本地知识索引可脱离 Desktop 使用。
|
|
403
423
|
|
|
404
424
|
会议:
|
|
405
425
|
|
|
@@ -476,7 +496,6 @@ ot report draft --today
|
|
|
476
496
|
### 10. AI Agent 禁止事项
|
|
477
497
|
|
|
478
498
|
- 不要写项目 `.openturtle/`
|
|
479
|
-
- 不要覆盖 `openturtle-knowledge`
|
|
480
499
|
- 不要覆盖 `openturtle-automations`
|
|
481
500
|
- 不要覆盖旧 `openturtle-todo`
|
|
482
501
|
- 不要删除用户已有 Agent 配置
|
|
@@ -536,6 +555,8 @@ npm pack --dry-run
|
|
|
536
555
|
|
|
537
556
|
### 13. npm 自动发布
|
|
538
557
|
|
|
558
|
+
完整的校验、Trusted Publisher、人工兜底、npm 二次认证、远端 `master` 对齐和 Skill 刷新检查见 [RELEASE_RUNBOOK.md](./RELEASE_RUNBOOK.md)。
|
|
559
|
+
|
|
539
560
|
`.github/workflows/cli-npm-release.yml` 会在 `master` 的 CLI 文件发生变化时运行测试和构建,
|
|
540
561
|
然后以仓库版本为最低版本、基于 npm 已发布版本自动递增 patch,发布公开包 `@openturtle/cli`。
|
|
541
562
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# OpenTurtle CLI Release Runbook
|
|
2
|
+
|
|
3
|
+
This runbook publishes `@openturtle/cli` to the public npm registry and verifies that the installed `ot` command and companion Agent guidance are usable outside the source repository.
|
|
4
|
+
|
|
5
|
+
## Release Boundaries
|
|
6
|
+
|
|
7
|
+
- Package: `@openturtle/cli`
|
|
8
|
+
- Binary: `ot`
|
|
9
|
+
- Registry: `https://registry.npmjs.org/`
|
|
10
|
+
- Source: `packages/frontend/apps/cli`
|
|
11
|
+
- Release branch: `master`
|
|
12
|
+
- Workflow: `.github/workflows/cli-npm-release.yml`
|
|
13
|
+
- Authentication: npm Trusted Publisher in CI; npm browser authentication for manual fallback
|
|
14
|
+
|
|
15
|
+
The package is CLI + Skill/Rule/Steering only. It must not ship the removed OpenTurtle MCP server, Hook commands, Git Hook installer, or history watcher.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
Run from the repository root unless a command says otherwise.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node --version
|
|
23
|
+
pnpm --version
|
|
24
|
+
npm --version
|
|
25
|
+
git status --short
|
|
26
|
+
npm view @openturtle/cli version --registry=https://registry.npmjs.org
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requirements:
|
|
30
|
+
|
|
31
|
+
- Node.js satisfies `packages/frontend/apps/cli/package.json#engines`.
|
|
32
|
+
- Dependencies are installed from the repository lockfile.
|
|
33
|
+
- CLI changes are isolated from unrelated worktree changes.
|
|
34
|
+
- The intended release commit is present on both GitLab and GitHub `master` before declaring source delivery complete.
|
|
35
|
+
- npm Trusted Publisher points to `aka-danielZhang/openturtle-os` and `cli-npm-release.yml`.
|
|
36
|
+
|
|
37
|
+
Never stage unrelated Desktop, backend, mobile, generated local state, or `.openturtle-cli/` files with a CLI release.
|
|
38
|
+
|
|
39
|
+
## Required Validation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd packages/frontend
|
|
43
|
+
pnpm install --frozen-lockfile
|
|
44
|
+
pnpm --filter @openturtle/cli test
|
|
45
|
+
pnpm --filter @openturtle/cli build
|
|
46
|
+
|
|
47
|
+
cd apps/cli
|
|
48
|
+
npm pack --dry-run --json --registry=https://registry.npmjs.org
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The tarball must contain `dist/index.js`, runtime modules, `README.md`, and `package.json`. It must not contain any of these removed runtime files:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
dist/mcp/server.js
|
|
55
|
+
dist/commands/hook.js
|
|
56
|
+
dist/installers/git.js
|
|
57
|
+
dist/watchers/importers.js
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The build script deletes `dist` before TypeScript compilation so removed source files cannot survive as stale package artifacts.
|
|
61
|
+
|
|
62
|
+
## Normal Release: GitHub Actions
|
|
63
|
+
|
|
64
|
+
The normal path is a push to GitHub `master` that changes the CLI package or root lockfile.
|
|
65
|
+
|
|
66
|
+
1. Merge the validated CLI commit into `master` without including unrelated files.
|
|
67
|
+
2. Push `master` to GitLab and GitHub.
|
|
68
|
+
3. Confirm `.github/workflows/cli-npm-release.yml` starts on GitHub.
|
|
69
|
+
4. Wait for test, build, version resolution, package inspection, and publish steps to complete.
|
|
70
|
+
5. Verify the published package from npm, not only the workflow status.
|
|
71
|
+
|
|
72
|
+
The workflow treats the repository package version as a floor. If npm already has that version or a newer one, `nextReleaseVersion()` increments the published patch version. The workflow does not require `NPM_TOKEN` or `NODE_AUTH_TOKEN`.
|
|
73
|
+
|
|
74
|
+
Verification:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm view @openturtle/cli version dist.tarball dist.shasum --json \
|
|
78
|
+
--registry=https://registry.npmjs.org
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Do not declare completion while the workflow is queued, running, or blocked by GitHub billing. Use the manual fallback when CI cannot start and the release is required immediately.
|
|
82
|
+
|
|
83
|
+
## Manual Fallback
|
|
84
|
+
|
|
85
|
+
Use this only when Trusted Publisher CI is unavailable or when an explicitly requested release must happen before the `master` merge.
|
|
86
|
+
|
|
87
|
+
1. Confirm npm login and the currently published version:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm whoami --registry=https://registry.npmjs.org
|
|
91
|
+
npm view @openturtle/cli version --registry=https://registry.npmjs.org
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
2. Set `packages/frontend/apps/cli/package.json` to an unused stable version.
|
|
95
|
+
3. Run the required validation above.
|
|
96
|
+
4. Publish from the package directory:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cd packages/frontend/apps/cli
|
|
100
|
+
npm publish --access public --registry=https://registry.npmjs.org
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
5. Complete npm's browser authentication when prompted. A successful terminal result contains:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
+ @openturtle/cli@<version>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
6. Verify the exact published version and tarball metadata:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm view @openturtle/cli@<version> version dist.tarball dist.shasum --json \
|
|
113
|
+
--registry=https://registry.npmjs.org
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
After a manual publish, still merge and push the exact source to both `master` branches. A later CI run may publish the next patch because the workflow always chooses an unused version.
|
|
117
|
+
|
|
118
|
+
## Installed Package Smoke Test
|
|
119
|
+
|
|
120
|
+
Test the public package from a temporary directory, not only from the repository:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
release_tmp="$(mktemp -d)"
|
|
124
|
+
cd "$release_tmp"
|
|
125
|
+
pnpm dlx @openturtle/cli@<version> --help
|
|
126
|
+
pnpm dlx @openturtle/cli@<version> --json doctor
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
For a global-install smoke test:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npm install --global @openturtle/cli@<version> \
|
|
133
|
+
--registry=https://registry.npmjs.org
|
|
134
|
+
command -v ot
|
|
135
|
+
ot --version
|
|
136
|
+
ot --json doctor
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`doctor` may report missing authentication, but it must return stable JSON rather than crash.
|
|
140
|
+
|
|
141
|
+
## Skill Refresh Verification
|
|
142
|
+
|
|
143
|
+
`ot install` records Agent installations in `~/.openturtle/cli/installations.json`. Both `ot update` and the startup auto-update worker reinstall the recorded Skill, Rule, or Steering after npm updates.
|
|
144
|
+
|
|
145
|
+
Verify at least one installed target:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
ot install --target codex --scope user
|
|
149
|
+
ot update --check
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
After an actual version update, confirm the installed Skill contains the new direct CLI guidance:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
rg -n "ot --json doctor|ot \.\.\. --json" ~/.codex/skills/openturtle-cli/SKILL.md
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Installation and refresh must also remove only the legacy OpenTurtle-managed `ot-cli` MCP and `OPENTURTLE-CLI` Hook entries. User-owned MCP servers, Hooks, rules, and settings must remain intact.
|
|
159
|
+
|
|
160
|
+
## Source Delivery Verification
|
|
161
|
+
|
|
162
|
+
Before closing the release, compare the intended commit with both remotes:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
git ls-remote --heads gitlab master
|
|
166
|
+
git ls-remote --heads origin master
|
|
167
|
+
git log -1 --oneline master
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
If the remotes differ, inspect ancestry and merge rather than force-pushing:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
git merge-base --is-ancestor <gitlab-master> <release-commit>
|
|
174
|
+
git merge-base --is-ancestor <github-master> <release-commit>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Report all of the following:
|
|
178
|
+
|
|
179
|
+
- Published npm version and tarball shasum
|
|
180
|
+
- Test/build/package inspection result
|
|
181
|
+
- GitLab `master` commit
|
|
182
|
+
- GitHub `master` commit
|
|
183
|
+
- Workflow result or the reason manual fallback was used
|
|
184
|
+
- Installed `ot --version` and Skill refresh result
|
|
185
|
+
|
|
186
|
+
## Failure Recovery
|
|
187
|
+
|
|
188
|
+
- `npm publish` reports an existing version: choose a new stable version; never reuse or overwrite an npm version.
|
|
189
|
+
- Browser authentication expires: rerun `npm publish` and complete the newly generated npm authentication URL.
|
|
190
|
+
- CI cannot start because of billing: use the manual fallback, then still merge the source to `master`.
|
|
191
|
+
- Package contains removed files: ensure the build cleans `dist`, rebuild, and repeat `npm pack --dry-run --json`.
|
|
192
|
+
- Skill is stale after update: inspect `~/.openturtle/cli/installations.json`, rerun `ot install --target <agent> --scope <scope>`, then verify the installed Skill/Rule/Steering.
|
|
193
|
+
- One remote `master` is ahead: fetch both remotes, inspect commits, merge in a clean worktree, and push without force.
|
|
@@ -29,17 +29,73 @@ export function createCollaborationCommands(client = new ApiClient()) {
|
|
|
29
29
|
candidates: exact ? [] : items,
|
|
30
30
|
};
|
|
31
31
|
},
|
|
32
|
-
async listRoster(teamId) {
|
|
32
|
+
async listRoster(teamId, projectId) {
|
|
33
33
|
const team = teamId ? { id: teamId } : await client.get('/api/teams/me');
|
|
34
34
|
const resolvedTeamId = String(team.id || teamId || '');
|
|
35
35
|
if (!resolvedTeamId)
|
|
36
36
|
throw new Error('Current team response did not include an id');
|
|
37
|
-
const [members, profile, rosterProfile] = await Promise.all([
|
|
37
|
+
const [members, profile, rosterProfile, projectMembers] = await Promise.all([
|
|
38
38
|
client.get(`/api/teams/${encodeURIComponent(resolvedTeamId)}/members`, { page_size: 100 }),
|
|
39
39
|
client.get('/api/me/profile').catch(() => null),
|
|
40
40
|
client.get('/api/me/roster-profile').catch(() => null),
|
|
41
|
+
projectId
|
|
42
|
+
? client.get(`/api/projects/${encodeURIComponent(projectId)}/members`).catch(() => null)
|
|
43
|
+
: Promise.resolve(null),
|
|
41
44
|
]);
|
|
42
|
-
return { team, profile, roster_profile: rosterProfile, members };
|
|
45
|
+
return { team, profile, roster_profile: rosterProfile, project_id: projectId || null, members, project_members: projectMembers };
|
|
46
|
+
},
|
|
47
|
+
async getWorkSummary(options) {
|
|
48
|
+
const limit = Number(options.limit) || 20;
|
|
49
|
+
const goalId = String(options.goal || '');
|
|
50
|
+
const projectId = String(options.project || '');
|
|
51
|
+
const todos = goalId
|
|
52
|
+
? client.get(`/api/goals/${encodeURIComponent(goalId)}/todos`, {
|
|
53
|
+
status: options.status,
|
|
54
|
+
page: 1,
|
|
55
|
+
page_size: limit,
|
|
56
|
+
})
|
|
57
|
+
: client.get('/api/todos', {
|
|
58
|
+
scope: 'assigned',
|
|
59
|
+
project_id: projectId || undefined,
|
|
60
|
+
status: options.status,
|
|
61
|
+
page: 1,
|
|
62
|
+
page_size: limit,
|
|
63
|
+
});
|
|
64
|
+
const [digest, todoPage, notifications] = await Promise.all([
|
|
65
|
+
client.get('/api/goals/daily-digest/me'),
|
|
66
|
+
todos,
|
|
67
|
+
options.notifications === false
|
|
68
|
+
? Promise.resolve(null)
|
|
69
|
+
: client.get('/api/notifications', { is_read: false, page: 1, page_size: limit }),
|
|
70
|
+
]);
|
|
71
|
+
return {
|
|
72
|
+
project_id: projectId || null,
|
|
73
|
+
goal_id: goalId || null,
|
|
74
|
+
digest,
|
|
75
|
+
todos: todoPage,
|
|
76
|
+
notifications,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
getProjectDigest: (projectId, window) => client.get('/api/digest', { project_id: projectId, window }),
|
|
80
|
+
async runOrgSyncSynthesis(options, dryRun = false) {
|
|
81
|
+
const scope = String(options.scope || 'active-project');
|
|
82
|
+
const projectId = String(options.project || '');
|
|
83
|
+
if (scope !== 'all' && !projectId)
|
|
84
|
+
throw new Error('--project is required unless --scope all is used');
|
|
85
|
+
const body = listQuery({
|
|
86
|
+
project_id: scope === 'all' ? undefined : projectId,
|
|
87
|
+
limit: options.limit,
|
|
88
|
+
updated_after: options.updatedAfter,
|
|
89
|
+
});
|
|
90
|
+
if (dryRun)
|
|
91
|
+
return dryRunRequest('POST', '/api/orgsync-synthesis/run', body);
|
|
92
|
+
const result = await client.post('/api/orgsync-synthesis/run', body);
|
|
93
|
+
return {
|
|
94
|
+
scope: scope === 'all' ? 'all' : 'active_project',
|
|
95
|
+
project_id: scope === 'all' ? null : projectId,
|
|
96
|
+
updated_after: options.updatedAfter || null,
|
|
97
|
+
result,
|
|
98
|
+
};
|
|
43
99
|
},
|
|
44
100
|
resolveContext: (body) => client.post('/api/project-context/resolve', body),
|
|
45
101
|
getContextItem: (sourceType, sourceId, options) => client.get(`/api/project-context/items/${encodeURIComponent(sourceType)}/${encodeURIComponent(sourceId)}`, listQuery(options)),
|
|
@@ -127,7 +183,35 @@ export function registerCollaborationCommands(program, groups, callbacks) {
|
|
|
127
183
|
.description('Current team roster')
|
|
128
184
|
.command('list')
|
|
129
185
|
.option('--team <id>')
|
|
130
|
-
.
|
|
186
|
+
.option('--project <id>', 'include project roles and membership')
|
|
187
|
+
.action(action(async (options) => print(await commands.listRoster(options.team, options.project))));
|
|
188
|
+
program
|
|
189
|
+
.command('work')
|
|
190
|
+
.description('Current user action summary')
|
|
191
|
+
.command('summary')
|
|
192
|
+
.option('--project <id>')
|
|
193
|
+
.option('--goal <id>')
|
|
194
|
+
.option('--status <status>')
|
|
195
|
+
.option('--limit <number>', 'maximum todos and notifications', Number)
|
|
196
|
+
.option('--no-notifications')
|
|
197
|
+
.action(action(async (options) => print(await commands.getWorkSummary(options))));
|
|
198
|
+
program
|
|
199
|
+
.command('digest')
|
|
200
|
+
.description('Project execution digest')
|
|
201
|
+
.command('get')
|
|
202
|
+
.option('--project <id>')
|
|
203
|
+
.option('--window <window>', 'today|yesterday|this_week|last_week|this_month', 'today')
|
|
204
|
+
.action(action(async (options) => print(await commands.getProjectDigest(options.project, options.window))));
|
|
205
|
+
program
|
|
206
|
+
.command('orgsync')
|
|
207
|
+
.description('Generate reviewable organization knowledge candidates')
|
|
208
|
+
.command('synthesize')
|
|
209
|
+
.option('--scope <scope>', 'active-project|all', 'active-project')
|
|
210
|
+
.option('--project <id>')
|
|
211
|
+
.option('--limit <number>', 'maximum recent sessions', Number, 20)
|
|
212
|
+
.option('--updated-after <iso-time>')
|
|
213
|
+
.option('--dry-run')
|
|
214
|
+
.action(action(async (options) => print(await commands.runOrgSyncSynthesis(options, Boolean(options.dryRun)))));
|
|
131
215
|
const context = program.command('context').description('Viewer-scoped project context');
|
|
132
216
|
context
|
|
133
217
|
.command('get')
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readJsonBody } from '../core/input.js';
|
|
2
|
+
import { createLocalKnowledgeEntry, getLocalKnowledgeAsset, importLocalKnowledgePaths, listLocalKnowledgeAssets, removeLocalKnowledgeAsset, updateLocalKnowledgeEntry, } from '../local-knowledge/library.js';
|
|
3
|
+
import { readLocalKnowledgeIndexedContent, searchLocalKnowledgeIndex, } from '../local-knowledge/index.js';
|
|
4
|
+
import { localKnowledgeStoreRoot } from '../local-knowledge/root.js';
|
|
5
|
+
export function createLocalKnowledgeCommands(root = localKnowledgeStoreRoot()) {
|
|
6
|
+
return {
|
|
7
|
+
root,
|
|
8
|
+
list: () => listLocalKnowledgeAssets(root),
|
|
9
|
+
async search(query, limit = 20, matchMode = 'any') {
|
|
10
|
+
const assets = await listLocalKnowledgeAssets(root);
|
|
11
|
+
const hits = await searchLocalKnowledgeIndex(assets, query, Math.max(1, Math.min(limit, 100)), root, matchMode);
|
|
12
|
+
const byId = new Map(assets.map((asset) => [asset.id, asset]));
|
|
13
|
+
const items = hits.flatMap((hit) => {
|
|
14
|
+
const asset = byId.get(hit.assetId);
|
|
15
|
+
return asset ? [{ ...asset, matches: hit.matches }] : [];
|
|
16
|
+
});
|
|
17
|
+
return { root, items, count: items.length };
|
|
18
|
+
},
|
|
19
|
+
async read(assetId, truncate = 200_000) {
|
|
20
|
+
const asset = getLocalKnowledgeAsset(assetId, root);
|
|
21
|
+
if (!asset)
|
|
22
|
+
throw new Error('Local knowledge asset was not found');
|
|
23
|
+
const indexed = await readLocalKnowledgeIndexedContent(asset, Math.max(1, Math.min(truncate, 1_000_000)), root);
|
|
24
|
+
return {
|
|
25
|
+
...asset,
|
|
26
|
+
text: indexed.content,
|
|
27
|
+
content_available: indexed.contentAvailable,
|
|
28
|
+
truncated: indexed.truncated,
|
|
29
|
+
index_status: indexed.indexStatus,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
importPaths(paths, dryRun = false) {
|
|
33
|
+
if (dryRun)
|
|
34
|
+
return Promise.resolve(localDryRun(root, 'import', { paths }));
|
|
35
|
+
return importLocalKnowledgePaths(paths, root);
|
|
36
|
+
},
|
|
37
|
+
create(input, dryRun = false) {
|
|
38
|
+
if (dryRun)
|
|
39
|
+
return Promise.resolve(localDryRun(root, 'create', { input }));
|
|
40
|
+
return createLocalKnowledgeEntry(input, root);
|
|
41
|
+
},
|
|
42
|
+
update(assetId, input, dryRun = false) {
|
|
43
|
+
if (dryRun)
|
|
44
|
+
return Promise.resolve(localDryRun(root, 'update', { asset_id: assetId, input }));
|
|
45
|
+
return updateLocalKnowledgeEntry(assetId, input, root);
|
|
46
|
+
},
|
|
47
|
+
remove(assetId, dryRun = false) {
|
|
48
|
+
if (dryRun)
|
|
49
|
+
return Promise.resolve(localDryRun(root, 'remove', { asset_id: assetId }));
|
|
50
|
+
return removeLocalKnowledgeAsset(assetId, root);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function searchKnowledgeEverywhere(query, options, fetchCloud, root = localKnowledgeStoreRoot()) {
|
|
55
|
+
if (options.cloudOnly && options.localOnly)
|
|
56
|
+
throw new Error('Use --cloud-only or --local-only, not both');
|
|
57
|
+
const local = createLocalKnowledgeCommands(root);
|
|
58
|
+
const [cloudResult, localResult] = await Promise.all([
|
|
59
|
+
options.localOnly ? Promise.resolve(null) : fetchCloud(),
|
|
60
|
+
options.cloudOnly
|
|
61
|
+
? Promise.resolve(null)
|
|
62
|
+
: local.search(query, Number(options.limit) || 20, normalizeMatchMode(options.match)),
|
|
63
|
+
]);
|
|
64
|
+
return {
|
|
65
|
+
query,
|
|
66
|
+
cloud: cloudResult,
|
|
67
|
+
local: localResult,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function registerLocalKnowledgeCommands(knowledge, callbacks) {
|
|
71
|
+
const commands = createLocalKnowledgeCommands();
|
|
72
|
+
const local = knowledge.command('local').description('Local indexed knowledge available without Desktop');
|
|
73
|
+
const action = (fn) => (...args) => {
|
|
74
|
+
Promise.resolve(fn(...args)).catch(callbacks.fatal);
|
|
75
|
+
};
|
|
76
|
+
local.command('list').action(action(async () => callbacks.print(await commands.list())));
|
|
77
|
+
local
|
|
78
|
+
.command('search <query>')
|
|
79
|
+
.option('--limit <number>', 'maximum results', Number, 20)
|
|
80
|
+
.option('--match <mode>', 'any|all|exact', 'any')
|
|
81
|
+
.action(action(async (query, options) => callbacks.print(await commands.search(query, options.limit, normalizeMatchMode(options.match)))));
|
|
82
|
+
local
|
|
83
|
+
.command('read <assetId>')
|
|
84
|
+
.option('--truncate <characters>', 'maximum extracted characters', Number, 200_000)
|
|
85
|
+
.action(action(async (assetId, options) => callbacks.print(await commands.read(assetId, options.truncate))));
|
|
86
|
+
local
|
|
87
|
+
.command('import <paths...>')
|
|
88
|
+
.option('--dry-run')
|
|
89
|
+
.action(action(async (paths, options) => callbacks.print(await commands.importPaths(paths, Boolean(options.dryRun)))));
|
|
90
|
+
local
|
|
91
|
+
.command('create')
|
|
92
|
+
.requiredOption('--body-file <path>', 'JSON body file, or - for stdin')
|
|
93
|
+
.option('--dry-run')
|
|
94
|
+
.action(action(async (options) => callbacks.print(await commands.create(asEntryInput(await readJsonBody(options.bodyFile)), Boolean(options.dryRun)))));
|
|
95
|
+
local
|
|
96
|
+
.command('update <assetId>')
|
|
97
|
+
.requiredOption('--body-file <path>', 'JSON body file, or - for stdin')
|
|
98
|
+
.option('--dry-run')
|
|
99
|
+
.action(action(async (assetId, options) => callbacks.print(await commands.update(assetId, asEntryInput(await readJsonBody(options.bodyFile)), Boolean(options.dryRun)))));
|
|
100
|
+
local
|
|
101
|
+
.command('remove <assetId>')
|
|
102
|
+
.option('--dry-run')
|
|
103
|
+
.action(action(async (assetId, options) => callbacks.print(await commands.remove(assetId, Boolean(options.dryRun)))));
|
|
104
|
+
}
|
|
105
|
+
function normalizeMatchMode(value) {
|
|
106
|
+
return value === 'all' || value === 'exact' ? value : 'any';
|
|
107
|
+
}
|
|
108
|
+
function asEntryInput(value) {
|
|
109
|
+
const kind = value.kind;
|
|
110
|
+
if (kind !== 'note' && kind !== 'decision' && kind !== 'fact' && kind !== 'meeting') {
|
|
111
|
+
throw new Error('Local knowledge kind must be note, decision, fact, or meeting');
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
title: String(value.title || ''),
|
|
115
|
+
body: String(value.body || ''),
|
|
116
|
+
kind,
|
|
117
|
+
projectId: value.projectId == null ? undefined : String(value.projectId),
|
|
118
|
+
status: String(value.status || 'draft'),
|
|
119
|
+
attachments: Array.isArray(value.attachments)
|
|
120
|
+
? value.attachments.map((item) => {
|
|
121
|
+
const attachment = item;
|
|
122
|
+
return { name: String(attachment.name || ''), path: String(attachment.path || '') };
|
|
123
|
+
})
|
|
124
|
+
: undefined,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function localDryRun(root, operation, input) {
|
|
128
|
+
return { dry_run: true, local_knowledge: { root, operation, ...input } };
|
|
129
|
+
}
|
package/dist/core/auth.js
CHANGED
|
@@ -7,13 +7,15 @@ export function authPath(homeDir = os.homedir()) {
|
|
|
7
7
|
return path.join(cliGlobalDir(homeDir), 'auth.json');
|
|
8
8
|
}
|
|
9
9
|
export function readAuth(homeDir = os.homedir()) {
|
|
10
|
-
const envToken = process.env.OPENTURTLE_TOKEN;
|
|
11
|
-
const envServer = process.env.OPENTURTLE_SERVER_URL || process.env.SERVER_URL;
|
|
10
|
+
const envToken = process.env.OPENTURTLE_API_TOKEN || process.env.OPENTURTLE_TOKEN;
|
|
11
|
+
const envServer = process.env.OPENTURTLE_API_BASE_URL || process.env.OPENTURTLE_SERVER_URL || process.env.SERVER_URL;
|
|
12
|
+
const envTeamId = process.env.OPENTURTLE_TEAM_ID;
|
|
12
13
|
const config = readJsonFile(authPath(homeDir), {});
|
|
13
14
|
return {
|
|
14
15
|
...config,
|
|
15
16
|
...(envServer ? { server: envServer } : {}),
|
|
16
17
|
...(envToken ? { token: envToken } : {}),
|
|
18
|
+
...(envTeamId ? { teamId: envTeamId } : {}),
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
21
|
export function writeAuth(config, homeDir = os.homedir()) {
|
package/dist/core/auto-update.js
CHANGED
|
@@ -40,6 +40,15 @@ export function npmUpdateArgs() {
|
|
|
40
40
|
];
|
|
41
41
|
}
|
|
42
42
|
export async function updateCli(currentVersion, options = {}, dependencies = {}) {
|
|
43
|
+
if ((dependencies.env || process.env).OPENTURTLE_CLI_DISTRIBUTION === 'desktop') {
|
|
44
|
+
return {
|
|
45
|
+
current: currentVersion,
|
|
46
|
+
latest: null,
|
|
47
|
+
available: false,
|
|
48
|
+
updated: false,
|
|
49
|
+
reason: 'desktop_managed',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
43
52
|
const latest = await fetchLatestVersion(dependencies.fetchImpl);
|
|
44
53
|
if (!latest) {
|
|
45
54
|
return { current: currentVersion, latest: null, available: false, updated: false, reason: 'not_published' };
|
|
@@ -71,7 +80,9 @@ export function scheduleAutomaticUpdate(currentVersion, dependencies = {}) {
|
|
|
71
80
|
try {
|
|
72
81
|
const env = dependencies.env || process.env;
|
|
73
82
|
const argv = dependencies.argv || process.argv;
|
|
74
|
-
if (env.
|
|
83
|
+
if (env.OPENTURTLE_CLI_DISTRIBUTION === 'desktop' ||
|
|
84
|
+
env.OPENTURTLE_DISABLE_AUTO_UPDATE === '1' ||
|
|
85
|
+
env.OPENTURTLE_AUTO_UPDATE_WORKER === '1')
|
|
75
86
|
return false;
|
|
76
87
|
if (argv[2] === 'update' || !isManagedNpmInstall(dependencies.packageRoot))
|
|
77
88
|
return false;
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { registerCollaborationCommands } from './commands/collaboration.js';
|
|
5
|
+
import { registerLocalKnowledgeCommands, searchKnowledgeEverywhere } from './commands/local-knowledge.js';
|
|
5
6
|
import { doctor, draftDailyReport, createResourceCommands } from './commands/resources.js';
|
|
6
7
|
import { ApiClient, ApiError } from './core/api-client.js';
|
|
7
8
|
import { scheduleAutomaticUpdate, updateCli } from './core/auto-update.js';
|
|
@@ -233,8 +234,11 @@ knowledge
|
|
|
233
234
|
.option('--person <name-or-id>')
|
|
234
235
|
.option('--scope <scope>', 'team|managed|participating|assigned', 'team')
|
|
235
236
|
.option('--limit <number>', 'maximum results', Number)
|
|
237
|
+
.option('--match <mode>', 'local match mode: any|all|exact', 'any')
|
|
238
|
+
.option('--cloud-only')
|
|
239
|
+
.option('--local-only')
|
|
236
240
|
.option('--json')
|
|
237
|
-
.action(action(async (keyword, options) => print(await resources.searchKnowledge(keyword, options), options.json)));
|
|
241
|
+
.action(action(async (keyword, options) => print(await searchKnowledgeEverywhere(keyword, options, () => resources.searchKnowledge(keyword, options)), options.json)));
|
|
238
242
|
knowledge
|
|
239
243
|
.command('sources')
|
|
240
244
|
.option('--path <path>')
|
|
@@ -245,6 +249,7 @@ knowledge
|
|
|
245
249
|
.command('source <path>')
|
|
246
250
|
.option('--json')
|
|
247
251
|
.action(action(async (filePath, options) => print(await resources.readKnowledgeSource(filePath), options.json)));
|
|
252
|
+
registerLocalKnowledgeCommands(knowledge, { print, fatal });
|
|
248
253
|
const worklogs = program.command('worklogs');
|
|
249
254
|
worklogs
|
|
250
255
|
.command('list')
|