@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 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
- .action(action(async (options) => print(await commands.listRoster(options.team))));
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()) {
@@ -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.OPENTURTLE_DISABLE_AUTO_UPDATE === '1' || env.OPENTURTLE_AUTO_UPDATE_WORKER === '1')
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')