@telepat/ideon 0.1.14 → 0.1.15
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 +35 -33
- package/README.zh-CN.md +116 -0
- package/dist/ideon.js +476 -324
- package/package.json +1 -1
- package/README.zh-Hans.md +0 -114
package/README.md
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center"><img src="./ideon-logo.webp" width="128" alt="Ideon"></p>
|
|
2
|
+
<h1 align="center">Ideon</h1>
|
|
3
|
+
<p align="center"><em>One idea. Endless formats.</em></p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://docs.telepat.io/ideon">📖 Docs</a>
|
|
7
|
+
· <a href="./README.md">🇺🇸 English</a>
|
|
8
|
+
· <a href="./README.zh-CN.md">🇨🇳 简体中文</a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://github.com/telepat-io/ideon/actions/workflows/ci.yml"><img src="https://github.com/telepat-io/ideon/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build"></a>
|
|
13
|
+
<a href="https://codecov.io/gh/telepat-io/ideon"><img src="https://codecov.io/gh/telepat-io/ideon/graph/badge.svg" alt="Codecov"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/@telepat/ideon"><img src="https://img.shields.io/npm/v/@telepat/ideon" alt="npm"></a>
|
|
15
|
+
<a href="https://github.com/telepat-io/ideon/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow.svg" alt="License"></a>
|
|
16
|
+
</p>
|
|
2
17
|
|
|
3
18
|
Ideon is an AI content writer that turns one idea into publish-ready content across multiple formats, styles, and channels.
|
|
4
19
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
[](https://github.com/telepat-io/ideon/actions/workflows/ci.yml)
|
|
8
|
-
[](https://codecov.io/gh/telepat-io/ideon)
|
|
9
|
-
[](https://www.npmjs.com/package/@telepat/ideon)
|
|
10
|
-
[](https://docs.telepat.io/ideon)
|
|
11
|
-
[](https://github.com/telepat-io/ideon/blob/main/LICENSE)
|
|
12
|
-
|
|
13
|
-
## Why Teams Use Ideon
|
|
20
|
+
## What It Solves
|
|
14
21
|
|
|
15
22
|
Ideon helps teams move from idea to publishable content faster, with less manual rewriting between channels.
|
|
16
23
|
|
|
@@ -48,15 +55,6 @@ Expected outcome:
|
|
|
48
55
|
- OpenRouter API key
|
|
49
56
|
- Replicate API token
|
|
50
57
|
|
|
51
|
-
## Core Capabilities
|
|
52
|
-
|
|
53
|
-
- Multi-format writing from one idea: article, blog, newsletter, Reddit, LinkedIn, X thread, X post, landing-page copy.
|
|
54
|
-
- Style control per run: choose one voice and apply it consistently across all outputs.
|
|
55
|
-
- Research and enrichment: generate planning briefs and add relevant links via link enrichment.
|
|
56
|
-
- Image generation: render cover and inline visuals for article-focused runs.
|
|
57
|
-
- Iteration workflows: rerun with different targets/styles, resume interrupted jobs, and use job files for repeatable execution.
|
|
58
|
-
- Local review: preview generated outputs and assets in a browser before publishing.
|
|
59
|
-
|
|
60
58
|
## How It Works
|
|
61
59
|
|
|
62
60
|
Ideon runs a staged writing pipeline: planning, drafting, image prompt expansion, image rendering, channel output generation, and optional link enrichment.
|
|
@@ -74,14 +72,18 @@ ideon write --job ./job.json
|
|
|
74
72
|
ideon write resume
|
|
75
73
|
ideon delete my-article-slug
|
|
76
74
|
ideon preview --no-open
|
|
77
|
-
ideon mcp serve
|
|
78
|
-
ideon agent status --json
|
|
79
75
|
```
|
|
80
76
|
|
|
81
|
-
|
|
77
|
+
## Using With AI Agents
|
|
78
|
+
|
|
79
|
+
Ideon is built for agentic workflows:
|
|
82
80
|
|
|
83
|
-
-
|
|
84
|
-
-
|
|
81
|
+
- **MCP server** — `ideon mcp serve` exposes 5 tools over stdio for content generation, resume, deletion, and config management. Compatible with Claude Code, ChatGPT, Gemini, and any generic MCP host.
|
|
82
|
+
- **Agent runtime registration** — `ideon agent install <runtime>` registers integration profiles for supported platforms. Check status with `ideon agent status --json`.
|
|
83
|
+
- **Non-interactive mode** — `ideon write --no-interactive ...` removes all prompts for CI and automation.
|
|
84
|
+
- **Machine-readable config** — `ideon config list --json` and `ideon config get <key> --json` for agent inspection.
|
|
85
|
+
- **Skill package** — Install `ideon-cli-skill/` into your agent host for a full lifecycle skill covering install, setup, operations, and debugging.
|
|
86
|
+
- **Agent docs** — [For Agents](https://docs.telepat.io/ideon/for-agents) covers MCP servers, skills, and maintenance.
|
|
85
87
|
|
|
86
88
|
## Security And Trust
|
|
87
89
|
|
|
@@ -94,18 +96,18 @@ To report a security issue, open a private report through the repository securit
|
|
|
94
96
|
|
|
95
97
|
## Documentation And Support
|
|
96
98
|
|
|
97
|
-
- Documentation site
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
- Repository
|
|
104
|
-
- npm package
|
|
99
|
+
- [Documentation site](https://docs.telepat.io/ideon)
|
|
100
|
+
- [Quickstart](https://docs.telepat.io/ideon/getting-started/quickstart)
|
|
101
|
+
- [CLI reference](https://docs.telepat.io/ideon/reference/cli-reference)
|
|
102
|
+
- [Configuration guide](https://docs.telepat.io/ideon/guides/configuration)
|
|
103
|
+
- [Troubleshooting](https://docs.telepat.io/ideon/guides/troubleshooting)
|
|
104
|
+
- [For Agents](https://docs.telepat.io/ideon/for-agents)
|
|
105
|
+
- [Repository](https://github.com/telepat-io/ideon)
|
|
106
|
+
- [npm package](https://www.npmjs.com/package/@telepat/ideon)
|
|
105
107
|
|
|
106
108
|
## Contributing
|
|
107
109
|
|
|
108
|
-
Contributions are welcome. Start with
|
|
110
|
+
Contributions are welcome. Start with [Development](https://docs.telepat.io/ideon/contributing/development) for setup, workflow, and quality gates, then follow [Releasing and Docs Deploy](https://docs.telepat.io/ideon/contributing/releasing-and-docs-deploy) for release and docs deployment details.
|
|
109
111
|
|
|
110
112
|
For user-facing documentation changes, update both English and Simplified Chinese content in the same change.
|
|
111
113
|
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<p align="center"><img src="./ideon-logo.webp" width="128" alt="Ideon"></p>
|
|
2
|
+
<h1 align="center">Ideon</h1>
|
|
3
|
+
<p align="center"><em>一个想法,无限格式。</em></p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://docs.telepat.io/ideon">📖 文档</a>
|
|
7
|
+
· <a href="./README.md">🇺🇸 English</a>
|
|
8
|
+
· <a href="./README.zh-CN.md">🇨🇳 简体中文</a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://github.com/telepat-io/ideon/actions/workflows/ci.yml"><img src="https://github.com/telepat-io/ideon/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build"></a>
|
|
13
|
+
<a href="https://codecov.io/gh/telepat-io/ideon"><img src="https://codecov.io/gh/telepat-io/ideon/graph/badge.svg" alt="Codecov"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/@telepat/ideon"><img src="https://img.shields.io/npm/v/@telepat/ideon" alt="npm"></a>
|
|
15
|
+
<a href="https://github.com/telepat-io/ideon/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow.svg" alt="License"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
Ideon 是一款 AI 内容写作工具,可将一个想法转化为多格式、多风格、可发布的内容。
|
|
19
|
+
|
|
20
|
+
## 它能解决什么问题
|
|
21
|
+
|
|
22
|
+
Ideon 帮助团队更快地从想法走到可发布内容,减少跨渠道重复改写的工作量。
|
|
23
|
+
|
|
24
|
+
一次运行,Ideon 可以:
|
|
25
|
+
|
|
26
|
+
- 基于同一核心想法生成多种输出类型:article、blog、newsletter、Reddit、LinkedIn、X thread、X post 等。
|
|
27
|
+
- 在所有输出中统一应用写作风格(`professional`、`friendly`、`technical`、`academic`、`opinionated`、`storytelling`)。
|
|
28
|
+
- 生成研究导向的内容 brief,补充相关链接,并在文章型任务中生成配图。
|
|
29
|
+
- 通过作业文件、可配置参数与断点恢复能力支持持续迭代。
|
|
30
|
+
|
|
31
|
+
这使 Ideon 适用于内容团队、开发者关系、产品营销、创始人以及需要按节奏进行多渠道写作的个人或团队。
|
|
32
|
+
|
|
33
|
+
## 快速开始
|
|
34
|
+
|
|
35
|
+
安装并完成第一次内容生成:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm i -g @telepat/ideon
|
|
39
|
+
ideon settings
|
|
40
|
+
ideon write "How small editorial teams can productionize AI writing" --primary article=1 --secondary x-post=1
|
|
41
|
+
ideon preview
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
预期结果:
|
|
45
|
+
|
|
46
|
+
- 在 `output/<timestamp>-<slug>/` 下生成一个运行目录。
|
|
47
|
+
- 产出一个或多个可发布的 Markdown 输出。
|
|
48
|
+
- 保存 analytics 与元数据,便于复现与回溯。
|
|
49
|
+
- 本地预览自动打开,便于检查内容、链接与资源。
|
|
50
|
+
|
|
51
|
+
## 环境要求
|
|
52
|
+
|
|
53
|
+
- Node.js 20+
|
|
54
|
+
- npm 10+
|
|
55
|
+
- OpenRouter API key
|
|
56
|
+
- Replicate API token
|
|
57
|
+
|
|
58
|
+
## 工作原理
|
|
59
|
+
|
|
60
|
+
Ideon 采用分阶段写作流水线:内容规划、正文生成、图像提示扩展、图像渲染、渠道内容生成,以及可选的链接增强。
|
|
61
|
+
|
|
62
|
+
运行时会合并 settings、环境变量、job 文件与 CLI 参数,并输出结构化产物,便于追踪与复用。
|
|
63
|
+
|
|
64
|
+
核心命令:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
ideon settings
|
|
68
|
+
ideon config list --json
|
|
69
|
+
ideon write "An article idea" --primary article=1
|
|
70
|
+
ideon write --no-interactive --idea "An article idea" --primary article=1 --style technical --length medium
|
|
71
|
+
ideon write --job ./job.json
|
|
72
|
+
ideon write resume
|
|
73
|
+
ideon delete my-article-slug
|
|
74
|
+
ideon preview --no-open
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 与 AI Agent 一起使用
|
|
78
|
+
|
|
79
|
+
Ideon 专为智能体工作流打造:
|
|
80
|
+
|
|
81
|
+
- **MCP 服务器** — `ideon mcp serve` 通过 stdio 暴露 5 个工具,覆盖内容生成、恢复、删除和配置管理。兼容 Claude Code、ChatGPT、Gemini 及任何通用 MCP 主机。
|
|
82
|
+
- **Agent 运行时注册** — `ideon agent install <runtime>` 为支持的平台注册集成配置文件。使用 `ideon agent status --json` 查看状态。
|
|
83
|
+
- **非交互模式** — `ideon write --no-interactive ...` 移除所有提示,适用于 CI 和自动化场景。
|
|
84
|
+
- **机器可读配置** — `ideon config list --json` 与 `ideon config get <key> --json` 供智能体 inspection。
|
|
85
|
+
- **Skill 包** — 将 `ideon-cli-skill/` 安装到智能体主机,获得覆盖安装、配置、操作与调试的完整生命周期 skill。
|
|
86
|
+
- **Agent 文档** — [For Agents](https://docs.telepat.io/ideon/for-agents) 涵盖 MCP 服务器、skill 与维护指南。
|
|
87
|
+
|
|
88
|
+
## 安全与信任
|
|
89
|
+
|
|
90
|
+
- 默认通过 `ideon settings` 将密钥保存到系统钥匙串。
|
|
91
|
+
- 在 CI 或容器环境中,请使用 `IDEON_OPENROUTER_API_KEY` 和 `IDEON_REPLICATE_API_TOKEN`。
|
|
92
|
+
- 在无法访问钥匙串时设置 `IDEON_DISABLE_KEYTAR=true`。
|
|
93
|
+
- 生成内容来自模型输出,发布前请进行人工审阅。
|
|
94
|
+
|
|
95
|
+
如需报告安全问题,请通过仓库安全报告通道私下提交,或通过仓库 issue 渠道联系维护者并避免包含敏感细节。
|
|
96
|
+
|
|
97
|
+
## 文档与支持
|
|
98
|
+
|
|
99
|
+
- [文档站点](https://docs.telepat.io/ideon)
|
|
100
|
+
- [快速上手](https://docs.telepat.io/ideon/getting-started/quickstart)
|
|
101
|
+
- [CLI 参考](https://docs.telepat.io/ideon/reference/cli-reference)
|
|
102
|
+
- [配置指南](https://docs.telepat.io/ideon/guides/configuration)
|
|
103
|
+
- [故障排查](https://docs.telepat.io/ideon/guides/troubleshooting)
|
|
104
|
+
- [For Agents](https://docs.telepat.io/ideon/for-agents)
|
|
105
|
+
- [仓库](https://github.com/telepat-io/ideon)
|
|
106
|
+
- [npm 包](https://www.npmjs.com/package/@telepat/ideon)
|
|
107
|
+
|
|
108
|
+
## 贡献
|
|
109
|
+
|
|
110
|
+
欢迎贡献。请先阅读 [Development](https://docs.telepat.io/ideon/contributing/development)(开发环境与质量门禁),再参考 [Releasing and Docs Deploy](https://docs.telepat.io/ideon/contributing/releasing-and-docs-deploy)(发布与文档部署流程)。
|
|
111
|
+
|
|
112
|
+
如修改面向用户的文档内容,请在同一变更中同时更新 English 与 简体中文版本。
|
|
113
|
+
|
|
114
|
+
## 许可证
|
|
115
|
+
|
|
116
|
+
MIT。详见 [LICENSE](./LICENSE)。
|
package/dist/ideon.js
CHANGED
|
@@ -972,12 +972,18 @@ var writeToolInputSchema = {
|
|
|
972
972
|
intent: z3.enum(contentIntentValues).optional(),
|
|
973
973
|
length: z3.union([z3.enum(targetLengthValues), z3.coerce.number().int().positive()]).optional(),
|
|
974
974
|
dryRun: z3.boolean().optional(),
|
|
975
|
-
enrichLinks: z3.boolean().optional()
|
|
975
|
+
enrichLinks: z3.boolean().optional(),
|
|
976
|
+
link: z3.array(z3.string()).optional(),
|
|
977
|
+
unlink: z3.array(z3.string()).optional(),
|
|
978
|
+
maxLinks: z3.coerce.number().int().positive().optional()
|
|
976
979
|
};
|
|
977
980
|
var writeToolInputZodSchema = z3.object(writeToolInputSchema);
|
|
978
981
|
var writeResumeToolInputSchema = {
|
|
979
982
|
dryRun: z3.boolean().optional(),
|
|
980
|
-
enrichLinks: z3.boolean().optional()
|
|
983
|
+
enrichLinks: z3.boolean().optional(),
|
|
984
|
+
link: z3.array(z3.string()).optional(),
|
|
985
|
+
unlink: z3.array(z3.string()).optional(),
|
|
986
|
+
maxLinks: z3.coerce.number().int().positive().optional()
|
|
981
987
|
};
|
|
982
988
|
var writeResumeToolInputZodSchema = z3.object(writeResumeToolInputSchema);
|
|
983
989
|
var deleteToolInputSchema = {
|
|
@@ -993,6 +999,20 @@ var configSetToolInputSchema = {
|
|
|
993
999
|
value: z3.string()
|
|
994
1000
|
};
|
|
995
1001
|
var configSetToolInputZodSchema = z3.object(configSetToolInputSchema);
|
|
1002
|
+
var linksToolInputSchema = {
|
|
1003
|
+
slug: z3.string().min(1),
|
|
1004
|
+
mode: z3.enum(["fresh", "append"]).optional(),
|
|
1005
|
+
link: z3.array(z3.string()).optional(),
|
|
1006
|
+
unlink: z3.array(z3.string()).optional(),
|
|
1007
|
+
maxLinks: z3.coerce.number().int().positive().optional()
|
|
1008
|
+
};
|
|
1009
|
+
var linksToolInputZodSchema = z3.object(linksToolInputSchema);
|
|
1010
|
+
var configListToolInputSchema = {};
|
|
1011
|
+
var configListToolInputZodSchema = z3.object(configListToolInputSchema);
|
|
1012
|
+
var configUnsetToolInputSchema = {
|
|
1013
|
+
key: z3.enum(configKeys)
|
|
1014
|
+
};
|
|
1015
|
+
var configUnsetToolInputZodSchema = z3.object(configUnsetToolInputSchema);
|
|
996
1016
|
var ideonToolContracts = [
|
|
997
1017
|
{
|
|
998
1018
|
name: "ideon_write",
|
|
@@ -1003,6 +1023,23 @@ var ideonToolContracts = [
|
|
|
1003
1023
|
length: [...targetLengthValues]
|
|
1004
1024
|
}
|
|
1005
1025
|
},
|
|
1026
|
+
{
|
|
1027
|
+
name: "ideon_write_resume",
|
|
1028
|
+
required: [],
|
|
1029
|
+
enums: {}
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
name: "ideon_delete",
|
|
1033
|
+
required: ["slug"],
|
|
1034
|
+
enums: {}
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
name: "ideon_links",
|
|
1038
|
+
required: ["slug"],
|
|
1039
|
+
enums: {
|
|
1040
|
+
mode: ["fresh", "append"]
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1006
1043
|
{
|
|
1007
1044
|
name: "ideon_config_set",
|
|
1008
1045
|
required: ["key", "value"],
|
|
@@ -1016,6 +1053,18 @@ var ideonToolContracts = [
|
|
|
1016
1053
|
enums: {
|
|
1017
1054
|
key: [...configKeys]
|
|
1018
1055
|
}
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
name: "ideon_config_list",
|
|
1059
|
+
required: [],
|
|
1060
|
+
enums: {}
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
name: "ideon_config_unset",
|
|
1064
|
+
required: ["key"],
|
|
1065
|
+
enums: {
|
|
1066
|
+
key: [...configKeys]
|
|
1067
|
+
}
|
|
1019
1068
|
}
|
|
1020
1069
|
];
|
|
1021
1070
|
|
|
@@ -1305,7 +1354,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
1305
1354
|
// package.json
|
|
1306
1355
|
var package_default = {
|
|
1307
1356
|
name: "@telepat/ideon",
|
|
1308
|
-
version: "0.1.
|
|
1357
|
+
version: "0.1.15",
|
|
1309
1358
|
description: "CLI for generating rich articles and images from ideas.",
|
|
1310
1359
|
type: "module",
|
|
1311
1360
|
repository: {
|
|
@@ -6005,79 +6054,341 @@ function parsePipelineCustomLinks(rawLinks, unlinks) {
|
|
|
6005
6054
|
return Array.from(result.values());
|
|
6006
6055
|
}
|
|
6007
6056
|
|
|
6008
|
-
// src/cli/commands/
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6057
|
+
// src/cli/commands/links.ts
|
|
6058
|
+
import { readFile as readFile6, stat as stat3 } from "fs/promises";
|
|
6059
|
+
import path9 from "path";
|
|
6060
|
+
async function runLinksCommand(options, dependencies = {}) {
|
|
6061
|
+
const slug = normalizeSlug2(options.slug);
|
|
6062
|
+
const mode = normalizeMode(options.mode);
|
|
6063
|
+
const cwd2 = dependencies.cwd ?? process.cwd();
|
|
6064
|
+
const log = dependencies.log ?? ((message) => console.log(message));
|
|
6065
|
+
const resolved = await resolveRunInput({
|
|
6066
|
+
idea: `Enrich links for ${slug}`
|
|
6067
|
+
});
|
|
6068
|
+
const markdownPath = await resolveMarkdownPathForSlug2(resolved.config.settings, slug, cwd2);
|
|
6069
|
+
const frontmatter = await readFrontmatter(markdownPath);
|
|
6070
|
+
const fileId = path9.parse(markdownPath).name;
|
|
6071
|
+
const articleTitle = frontmatter.title ?? toTitleFromSlug(slug);
|
|
6072
|
+
const articleDescription = frontmatter.description ?? "";
|
|
6073
|
+
const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
|
|
6074
|
+
if (!openRouterApiKey) {
|
|
6075
|
+
throw new ReportedError(
|
|
6076
|
+
"Missing OpenRouter API key. Run `ideon settings` to configure credentials or set IDEON_OPENROUTER_API_KEY."
|
|
6077
|
+
);
|
|
6013
6078
|
}
|
|
6014
|
-
const
|
|
6015
|
-
|
|
6016
|
-
|
|
6079
|
+
const openRouter = new OpenRouterClient(openRouterApiKey);
|
|
6080
|
+
const linksPath = resolveLinksPath(markdownPath);
|
|
6081
|
+
const existing = await readExistingLinks(linksPath);
|
|
6082
|
+
const updatedCustomLinks = resolveCustomLinks(existing?.customLinks ?? [], options.links ?? [], options.unlinks ?? []);
|
|
6083
|
+
const effectiveMaxLinks = options.maxLinks;
|
|
6084
|
+
const linksResult = await enrichLinks({
|
|
6085
|
+
markdownFiles: [{ markdownPath, fileId, contentType: "article" }],
|
|
6086
|
+
articleTitle,
|
|
6087
|
+
articleDescription,
|
|
6088
|
+
openRouter,
|
|
6089
|
+
settings: resolved.config.settings,
|
|
6090
|
+
dryRun: false,
|
|
6091
|
+
customLinks: updatedCustomLinks,
|
|
6092
|
+
maxLinks: effectiveMaxLinks,
|
|
6093
|
+
onItemProgress(event) {
|
|
6094
|
+
logProgress(event, log);
|
|
6095
|
+
}
|
|
6096
|
+
});
|
|
6097
|
+
const generatedLinks = linksResult[0]?.links ?? [];
|
|
6098
|
+
const mergedGeneratedLinks = mode === "append" ? mergeLinks(existing?.links ?? [], generatedLinks) : generatedLinks;
|
|
6099
|
+
const appendedCount = Math.max(0, mergedGeneratedLinks.length - (existing?.links.length ?? 0));
|
|
6100
|
+
await writeLinksFile(markdownPath, {
|
|
6101
|
+
version: 2,
|
|
6102
|
+
customLinks: updatedCustomLinks,
|
|
6103
|
+
links: mergedGeneratedLinks
|
|
6104
|
+
});
|
|
6105
|
+
const relativeMarkdownPath = formatRelativePath2(cwd2, markdownPath);
|
|
6106
|
+
const relativeLinksPath = formatRelativePath2(cwd2, linksPath);
|
|
6107
|
+
if (mode === "fresh") {
|
|
6108
|
+
const replaced = existing ? "Replaced existing links." : "Created links sidecar.";
|
|
6109
|
+
log(`Enriched links for "${slug}".`);
|
|
6110
|
+
log(`${replaced} Saved ${generatedLinks.length} generated + ${updatedCustomLinks.length} custom links to ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6111
|
+
return;
|
|
6017
6112
|
}
|
|
6018
|
-
const
|
|
6019
|
-
|
|
6113
|
+
const baseCount = existing?.links.length ?? 0;
|
|
6114
|
+
const verb = existing ? "Appended and deduplicated links." : "Created links sidecar.";
|
|
6115
|
+
log(`Enriched links for "${slug}".`);
|
|
6116
|
+
log(`${verb} Base ${baseCount}, added ${appendedCount}, total ${mergedGeneratedLinks.length} generated + ${updatedCustomLinks.length} custom in ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6117
|
+
}
|
|
6118
|
+
function normalizeMode(rawMode) {
|
|
6119
|
+
const normalized = rawMode.trim().toLowerCase();
|
|
6120
|
+
if (normalized === "fresh" || normalized === "append") {
|
|
6121
|
+
return normalized;
|
|
6122
|
+
}
|
|
6123
|
+
throw new ReportedError(`Unsupported --mode value "${rawMode}". Use "fresh" or "append".`);
|
|
6124
|
+
}
|
|
6125
|
+
function normalizeSlug2(rawSlug) {
|
|
6126
|
+
const slug = rawSlug.trim();
|
|
6127
|
+
if (!slug) {
|
|
6128
|
+
throw new ReportedError("Slug cannot be empty. Pass the generated article slug, for example `ideon links my-article-slug`.");
|
|
6129
|
+
}
|
|
6130
|
+
if (slug.toLowerCase().endsWith(".md")) {
|
|
6131
|
+
throw new ReportedError(`Expected a slug, not a markdown filename: ${slug}. Pass the slug without .md.`);
|
|
6132
|
+
}
|
|
6133
|
+
if (slug === "." || slug === ".." || /[/\\]/.test(slug)) {
|
|
6134
|
+
throw new ReportedError(`Invalid slug "${slug}". Pass the article slug only, without any path separators.`);
|
|
6135
|
+
}
|
|
6136
|
+
return slug;
|
|
6137
|
+
}
|
|
6138
|
+
async function resolveMarkdownPathForSlug2(settings, slug, cwd2) {
|
|
6139
|
+
const outputPaths = resolveOutputPaths(settings, cwd2);
|
|
6140
|
+
const directPath = path9.join(outputPaths.markdownOutputDir, `${slug}.md`);
|
|
6141
|
+
if (await isReadableFile(directPath)) {
|
|
6142
|
+
return directPath;
|
|
6143
|
+
}
|
|
6144
|
+
const markdownFiles = await listMarkdownFilesRecursively(outputPaths.markdownOutputDir);
|
|
6145
|
+
const matches = [];
|
|
6146
|
+
for (const candidate of markdownFiles) {
|
|
6147
|
+
if (path9.basename(candidate) === `${slug}.md`) {
|
|
6148
|
+
matches.push(candidate);
|
|
6149
|
+
continue;
|
|
6150
|
+
}
|
|
6151
|
+
const frontmatter = await readFrontmatter(candidate);
|
|
6152
|
+
if (frontmatter.slug === slug) {
|
|
6153
|
+
matches.push(candidate);
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
if (matches.length === 0) {
|
|
6020
6157
|
throw new ReportedError(
|
|
6021
|
-
`
|
|
6158
|
+
`Could not find article "${slug}". Expected a markdown file in ${outputPaths.markdownOutputDir} with frontmatter slug "${slug}".`
|
|
6022
6159
|
);
|
|
6023
6160
|
}
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6161
|
+
return newestPath(matches);
|
|
6162
|
+
}
|
|
6163
|
+
async function newestPath(paths) {
|
|
6164
|
+
let latestPath = paths[0];
|
|
6165
|
+
let latestMtime = 0;
|
|
6166
|
+
for (const candidate of paths) {
|
|
6167
|
+
const candidateStat = await stat3(candidate);
|
|
6168
|
+
if (candidateStat.mtimeMs >= latestMtime) {
|
|
6169
|
+
latestMtime = candidateStat.mtimeMs;
|
|
6170
|
+
latestPath = candidate;
|
|
6171
|
+
}
|
|
6172
|
+
}
|
|
6173
|
+
return latestPath;
|
|
6174
|
+
}
|
|
6175
|
+
async function readFrontmatter(markdownPath) {
|
|
6176
|
+
const markdown = await readFile6(markdownPath, "utf8");
|
|
6177
|
+
return parseFrontmatter(markdown);
|
|
6178
|
+
}
|
|
6179
|
+
function parseFrontmatter(markdown) {
|
|
6180
|
+
if (!markdown.startsWith("---\n")) {
|
|
6181
|
+
return { slug: null, title: null, description: null };
|
|
6182
|
+
}
|
|
6183
|
+
const frontmatterEnd = markdown.indexOf("\n---\n", 4);
|
|
6184
|
+
if (frontmatterEnd < 0) {
|
|
6185
|
+
return { slug: null, title: null, description: null };
|
|
6027
6186
|
}
|
|
6187
|
+
const block = markdown.slice(4, frontmatterEnd);
|
|
6028
6188
|
return {
|
|
6029
|
-
|
|
6030
|
-
|
|
6189
|
+
slug: parseFrontmatterValue(block, "slug"),
|
|
6190
|
+
title: parseFrontmatterValue(block, "title"),
|
|
6191
|
+
description: parseFrontmatterValue(block, "description")
|
|
6031
6192
|
};
|
|
6032
6193
|
}
|
|
6033
|
-
function
|
|
6034
|
-
const
|
|
6035
|
-
|
|
6036
|
-
|
|
6194
|
+
function parseFrontmatterValue(block, key) {
|
|
6195
|
+
const pattern = new RegExp(`^${key}:\\s*(.+)$`, "m");
|
|
6196
|
+
const match = block.match(pattern);
|
|
6197
|
+
if (!match || !match[1]) {
|
|
6198
|
+
return null;
|
|
6037
6199
|
}
|
|
6038
|
-
|
|
6039
|
-
|
|
6200
|
+
const rawValue = match[1].trim();
|
|
6201
|
+
if (!rawValue) {
|
|
6202
|
+
return null;
|
|
6040
6203
|
}
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
throw new ReportedError("Primary target count must be exactly 1. Use --primary <content-type=1>.");
|
|
6204
|
+
if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
|
|
6205
|
+
return rawValue.slice(1, -1);
|
|
6044
6206
|
}
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6207
|
+
return rawValue;
|
|
6208
|
+
}
|
|
6209
|
+
function toTitleFromSlug(slug) {
|
|
6210
|
+
return slug.split("-").filter((part) => part.length > 0).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
6211
|
+
}
|
|
6212
|
+
async function isReadableFile(filePath) {
|
|
6213
|
+
try {
|
|
6214
|
+
const fileStat = await stat3(filePath);
|
|
6215
|
+
return fileStat.isFile();
|
|
6216
|
+
} catch {
|
|
6217
|
+
return false;
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
async function readExistingLinks(linksPath) {
|
|
6221
|
+
try {
|
|
6222
|
+
const raw = await readFile6(linksPath, "utf8");
|
|
6223
|
+
const parsed = JSON.parse(raw);
|
|
6224
|
+
const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6225
|
+
expression: entry.expression.trim(),
|
|
6226
|
+
url: entry.url.trim(),
|
|
6227
|
+
title: typeof entry.title === "string" ? entry.title : null
|
|
6228
|
+
})) : null;
|
|
6229
|
+
if (!links) {
|
|
6230
|
+
throw new ReportedError(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
|
|
6052
6231
|
}
|
|
6053
|
-
const
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6232
|
+
const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6233
|
+
expression: entry.expression.trim(),
|
|
6234
|
+
url: entry.url.trim(),
|
|
6235
|
+
title: typeof entry.title === "string" ? entry.title : null
|
|
6236
|
+
})) : [];
|
|
6237
|
+
return {
|
|
6238
|
+
version: typeof parsed.version === "number" ? parsed.version : 1,
|
|
6239
|
+
customLinks,
|
|
6240
|
+
links
|
|
6241
|
+
};
|
|
6242
|
+
} catch (error) {
|
|
6243
|
+
if (readErrorCode2(error) === "ENOENT") {
|
|
6244
|
+
return null;
|
|
6057
6245
|
}
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6246
|
+
if (error instanceof ReportedError) {
|
|
6247
|
+
throw error;
|
|
6248
|
+
}
|
|
6249
|
+
const message = error instanceof Error ? error.message : "Unknown links sidecar read error.";
|
|
6250
|
+
throw new ReportedError(`Failed to read existing links from ${linksPath}: ${message}`);
|
|
6062
6251
|
}
|
|
6063
|
-
return [
|
|
6064
|
-
{
|
|
6065
|
-
...primary,
|
|
6066
|
-
role: "primary"
|
|
6067
|
-
},
|
|
6068
|
-
...secondaryDedupedByType.values()
|
|
6069
|
-
];
|
|
6070
6252
|
}
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
const
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6253
|
+
function mergeLinks(existingLinks, generatedLinks) {
|
|
6254
|
+
const merged = [];
|
|
6255
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6256
|
+
for (const entry of [...existingLinks, ...generatedLinks]) {
|
|
6257
|
+
const key = `${entry.expression.trim().toLowerCase()}::${entry.url.trim().toLowerCase()}`;
|
|
6258
|
+
if (seen.has(key)) {
|
|
6259
|
+
continue;
|
|
6260
|
+
}
|
|
6261
|
+
seen.add(key);
|
|
6262
|
+
merged.push(entry);
|
|
6263
|
+
}
|
|
6264
|
+
return merged;
|
|
6265
|
+
}
|
|
6266
|
+
function isValidLinkEntry(value2) {
|
|
6267
|
+
if (typeof value2 !== "object" || value2 === null) {
|
|
6268
|
+
return false;
|
|
6269
|
+
}
|
|
6270
|
+
const record = value2;
|
|
6271
|
+
return typeof record.expression === "string" && typeof record.url === "string" && (typeof record.title === "string" || record.title === null || record.title === void 0);
|
|
6272
|
+
}
|
|
6273
|
+
function readErrorCode2(error) {
|
|
6274
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
6275
|
+
return null;
|
|
6276
|
+
}
|
|
6277
|
+
const code = error.code;
|
|
6278
|
+
return typeof code === "string" ? code : null;
|
|
6279
|
+
}
|
|
6280
|
+
function formatRelativePath2(cwd2, targetPath) {
|
|
6281
|
+
const relativePath = path9.relative(cwd2, targetPath);
|
|
6282
|
+
return relativePath.length > 0 ? relativePath : targetPath;
|
|
6283
|
+
}
|
|
6284
|
+
function logProgress(event, log) {
|
|
6285
|
+
if (event.phase === "resolving-expression" || event.phase === "selecting-expressions") {
|
|
6286
|
+
return;
|
|
6287
|
+
}
|
|
6288
|
+
log(event.detail);
|
|
6289
|
+
}
|
|
6290
|
+
function parseCustomLinkFlag(raw) {
|
|
6291
|
+
const separatorIndex = raw.indexOf("->");
|
|
6292
|
+
if (separatorIndex < 0) {
|
|
6293
|
+
throw new ReportedError(`Invalid --link value "${raw}". Expected format: "expression->url".`);
|
|
6294
|
+
}
|
|
6295
|
+
const expression = raw.slice(0, separatorIndex).trim();
|
|
6296
|
+
const url = raw.slice(separatorIndex + 2).trim();
|
|
6297
|
+
if (!expression) {
|
|
6298
|
+
throw new ReportedError(`Invalid --link value "${raw}": expression (left side of ->) cannot be empty.`);
|
|
6299
|
+
}
|
|
6300
|
+
if (!url) {
|
|
6301
|
+
throw new ReportedError(`Invalid --link value "${raw}": url (right side of ->) cannot be empty.`);
|
|
6302
|
+
}
|
|
6303
|
+
return { expression, url };
|
|
6304
|
+
}
|
|
6305
|
+
function resolveCustomLinks(existing, addRaw, removeExpressions) {
|
|
6306
|
+
const result = new Map(
|
|
6307
|
+
existing.map((entry) => [entry.expression.trim().toLowerCase(), entry])
|
|
6308
|
+
);
|
|
6309
|
+
for (const raw of addRaw) {
|
|
6310
|
+
const { expression, url } = parseCustomLinkFlag(raw);
|
|
6311
|
+
result.set(expression.toLowerCase(), { expression, url, title: null });
|
|
6312
|
+
}
|
|
6313
|
+
for (const expr of removeExpressions) {
|
|
6314
|
+
result.delete(expr.trim().toLowerCase());
|
|
6315
|
+
}
|
|
6316
|
+
return Array.from(result.values());
|
|
6317
|
+
}
|
|
6318
|
+
|
|
6319
|
+
// src/cli/commands/writeTargetSpecs.ts
|
|
6320
|
+
function parseTargetSpec(spec) {
|
|
6321
|
+
const trimmed = spec.trim();
|
|
6322
|
+
if (!trimmed) {
|
|
6323
|
+
throw new ReportedError("Target spec cannot be empty. Use <content-type=count>.");
|
|
6324
|
+
}
|
|
6325
|
+
const [rawType, rawCount] = trimmed.split("=");
|
|
6326
|
+
if (!rawType || !rawCount) {
|
|
6327
|
+
throw new ReportedError(`Invalid target "${spec}". Use format content-type=count, e.g. article=1 or x-thread=3.`);
|
|
6328
|
+
}
|
|
6329
|
+
const contentType = rawType.trim();
|
|
6330
|
+
if (!contentTypeValues.includes(contentType)) {
|
|
6331
|
+
throw new ReportedError(
|
|
6332
|
+
`Unsupported content type "${contentType}". Supported values: ${contentTypeValues.join(", ")}.`
|
|
6333
|
+
);
|
|
6334
|
+
}
|
|
6335
|
+
const count = Number.parseInt(rawCount.trim(), 10);
|
|
6336
|
+
if (!Number.isFinite(count) || count <= 0) {
|
|
6337
|
+
throw new ReportedError(`Invalid count in target "${spec}". Count must be a positive integer.`);
|
|
6338
|
+
}
|
|
6339
|
+
return {
|
|
6340
|
+
contentType,
|
|
6341
|
+
count
|
|
6342
|
+
};
|
|
6343
|
+
}
|
|
6344
|
+
function parsePrimaryAndSecondarySpecs(options) {
|
|
6345
|
+
const { primarySpec, secondarySpecs } = options;
|
|
6346
|
+
if (!primarySpec && (!secondarySpecs || secondarySpecs.length === 0)) {
|
|
6347
|
+
return void 0;
|
|
6348
|
+
}
|
|
6349
|
+
if (!primarySpec) {
|
|
6350
|
+
throw new ReportedError("Missing required --primary <content-type=count>.");
|
|
6351
|
+
}
|
|
6352
|
+
const primary = parseTargetSpec(primarySpec);
|
|
6353
|
+
if (primary.count !== 1) {
|
|
6354
|
+
throw new ReportedError("Primary target count must be exactly 1. Use --primary <content-type=1>.");
|
|
6355
|
+
}
|
|
6356
|
+
const secondaryDedupedByType = /* @__PURE__ */ new Map();
|
|
6357
|
+
for (const spec of secondarySpecs ?? []) {
|
|
6358
|
+
const parsed = parseTargetSpec(spec);
|
|
6359
|
+
if (parsed.contentType === primary.contentType) {
|
|
6360
|
+
throw new ReportedError(
|
|
6361
|
+
`Content type "${parsed.contentType}" cannot be both primary and secondary in the same run.`
|
|
6362
|
+
);
|
|
6363
|
+
}
|
|
6364
|
+
const previous = secondaryDedupedByType.get(parsed.contentType);
|
|
6365
|
+
if (previous) {
|
|
6366
|
+
previous.count += parsed.count;
|
|
6367
|
+
continue;
|
|
6368
|
+
}
|
|
6369
|
+
secondaryDedupedByType.set(parsed.contentType, {
|
|
6370
|
+
...parsed,
|
|
6371
|
+
role: "secondary"
|
|
6372
|
+
});
|
|
6373
|
+
}
|
|
6374
|
+
return [
|
|
6375
|
+
{
|
|
6376
|
+
...primary,
|
|
6377
|
+
role: "primary"
|
|
6378
|
+
},
|
|
6379
|
+
...secondaryDedupedByType.values()
|
|
6380
|
+
];
|
|
6381
|
+
}
|
|
6382
|
+
|
|
6383
|
+
// src/integrations/mcp/server.ts
|
|
6384
|
+
async function startIdeonMcpServer() {
|
|
6385
|
+
const server = new McpServer({
|
|
6386
|
+
name: "ideon",
|
|
6387
|
+
version: package_default.version
|
|
6388
|
+
});
|
|
6389
|
+
server.registerTool(
|
|
6390
|
+
"ideon_write",
|
|
6391
|
+
{
|
|
6081
6392
|
title: "Ideon Write",
|
|
6082
6393
|
description: "Generate content from an idea using the Ideon pipeline.",
|
|
6083
6394
|
inputSchema: writeToolInputSchema
|
|
@@ -6093,6 +6404,7 @@ async function startIdeonMcpServer() {
|
|
|
6093
6404
|
audience: input.audience,
|
|
6094
6405
|
jobPath: input.jobPath,
|
|
6095
6406
|
style: input.style,
|
|
6407
|
+
intent: input.intent,
|
|
6096
6408
|
targetLength: input.length,
|
|
6097
6409
|
contentTargets: parsedTargets
|
|
6098
6410
|
});
|
|
@@ -6100,7 +6412,10 @@ async function startIdeonMcpServer() {
|
|
|
6100
6412
|
workingDir: cwd(),
|
|
6101
6413
|
runMode: "fresh",
|
|
6102
6414
|
dryRun: input.dryRun ?? false,
|
|
6103
|
-
enrichLinks: input.enrichLinks ??
|
|
6415
|
+
enrichLinks: input.enrichLinks ?? false,
|
|
6416
|
+
customLinks: input.link,
|
|
6417
|
+
unlinks: input.unlink,
|
|
6418
|
+
maxLinks: input.maxLinks
|
|
6104
6419
|
});
|
|
6105
6420
|
return {
|
|
6106
6421
|
content: [
|
|
@@ -6156,7 +6471,10 @@ async function startIdeonMcpServer() {
|
|
|
6156
6471
|
workingDir: cwd(),
|
|
6157
6472
|
runMode: "resume",
|
|
6158
6473
|
dryRun: input.dryRun ?? false,
|
|
6159
|
-
enrichLinks: input.enrichLinks ??
|
|
6474
|
+
enrichLinks: input.enrichLinks ?? false,
|
|
6475
|
+
customLinks: input.link,
|
|
6476
|
+
unlinks: input.unlink,
|
|
6477
|
+
maxLinks: input.maxLinks
|
|
6160
6478
|
});
|
|
6161
6479
|
return {
|
|
6162
6480
|
content: [
|
|
@@ -6215,6 +6533,48 @@ async function startIdeonMcpServer() {
|
|
|
6215
6533
|
}
|
|
6216
6534
|
}
|
|
6217
6535
|
);
|
|
6536
|
+
server.registerTool(
|
|
6537
|
+
"ideon_links",
|
|
6538
|
+
{
|
|
6539
|
+
title: "Ideon Links",
|
|
6540
|
+
description: "Run link enrichment for a previously generated article by slug.",
|
|
6541
|
+
inputSchema: linksToolInputSchema
|
|
6542
|
+
},
|
|
6543
|
+
async (input) => {
|
|
6544
|
+
try {
|
|
6545
|
+
const messages = [];
|
|
6546
|
+
await runLinksCommand(
|
|
6547
|
+
{
|
|
6548
|
+
slug: input.slug,
|
|
6549
|
+
mode: input.mode ?? "fresh",
|
|
6550
|
+
links: input.link,
|
|
6551
|
+
unlinks: input.unlink,
|
|
6552
|
+
maxLinks: input.maxLinks
|
|
6553
|
+
},
|
|
6554
|
+
{
|
|
6555
|
+
cwd: cwd(),
|
|
6556
|
+
log: (message) => {
|
|
6557
|
+
messages.push(message);
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
);
|
|
6561
|
+
return {
|
|
6562
|
+
content: [
|
|
6563
|
+
{
|
|
6564
|
+
type: "text",
|
|
6565
|
+
text: messages.length > 0 ? messages.join("\n") : `Enriched links for ${input.slug}.`
|
|
6566
|
+
}
|
|
6567
|
+
],
|
|
6568
|
+
structuredContent: {
|
|
6569
|
+
slug: input.slug,
|
|
6570
|
+
mode: input.mode ?? "fresh"
|
|
6571
|
+
}
|
|
6572
|
+
};
|
|
6573
|
+
} catch (error) {
|
|
6574
|
+
return formatToolError(error);
|
|
6575
|
+
}
|
|
6576
|
+
}
|
|
6577
|
+
);
|
|
6218
6578
|
server.registerTool(
|
|
6219
6579
|
"ideon_config_get",
|
|
6220
6580
|
{
|
|
@@ -6276,6 +6636,60 @@ async function startIdeonMcpServer() {
|
|
|
6276
6636
|
}
|
|
6277
6637
|
}
|
|
6278
6638
|
);
|
|
6639
|
+
server.registerTool(
|
|
6640
|
+
"ideon_config_list",
|
|
6641
|
+
{
|
|
6642
|
+
title: "Ideon Config List",
|
|
6643
|
+
description: "List current settings and secret availability flags.",
|
|
6644
|
+
inputSchema: configListToolInputSchema
|
|
6645
|
+
},
|
|
6646
|
+
async (_input) => {
|
|
6647
|
+
try {
|
|
6648
|
+
const result = await configList();
|
|
6649
|
+
return {
|
|
6650
|
+
content: [
|
|
6651
|
+
{
|
|
6652
|
+
type: "text",
|
|
6653
|
+
text: JSON.stringify(result, null, 2)
|
|
6654
|
+
}
|
|
6655
|
+
],
|
|
6656
|
+
structuredContent: result
|
|
6657
|
+
};
|
|
6658
|
+
} catch (error) {
|
|
6659
|
+
return formatToolError(error);
|
|
6660
|
+
}
|
|
6661
|
+
}
|
|
6662
|
+
);
|
|
6663
|
+
server.registerTool(
|
|
6664
|
+
"ideon_config_unset",
|
|
6665
|
+
{
|
|
6666
|
+
title: "Ideon Config Unset",
|
|
6667
|
+
description: "Reset a setting to its default or delete a stored secret.",
|
|
6668
|
+
inputSchema: configUnsetToolInputSchema
|
|
6669
|
+
},
|
|
6670
|
+
async (input) => {
|
|
6671
|
+
try {
|
|
6672
|
+
if (!isConfigKey(input.key)) {
|
|
6673
|
+
throw new ReportedError(`Unsupported config key: ${input.key}`);
|
|
6674
|
+
}
|
|
6675
|
+
await configUnset(input.key);
|
|
6676
|
+
return {
|
|
6677
|
+
content: [
|
|
6678
|
+
{
|
|
6679
|
+
type: "text",
|
|
6680
|
+
text: `Unset ${input.key}.`
|
|
6681
|
+
}
|
|
6682
|
+
],
|
|
6683
|
+
structuredContent: {
|
|
6684
|
+
key: input.key,
|
|
6685
|
+
updated: true
|
|
6686
|
+
}
|
|
6687
|
+
};
|
|
6688
|
+
} catch (error) {
|
|
6689
|
+
return formatToolError(error);
|
|
6690
|
+
}
|
|
6691
|
+
}
|
|
6692
|
+
);
|
|
6279
6693
|
const transport = new StdioServerTransport();
|
|
6280
6694
|
await server.connect(transport);
|
|
6281
6695
|
}
|
|
@@ -6292,268 +6706,6 @@ async function runMcpServeCommand() {
|
|
|
6292
6706
|
await startIdeonMcpServer();
|
|
6293
6707
|
}
|
|
6294
6708
|
|
|
6295
|
-
// src/cli/commands/links.ts
|
|
6296
|
-
import { readFile as readFile6, stat as stat3 } from "fs/promises";
|
|
6297
|
-
import path9 from "path";
|
|
6298
|
-
async function runLinksCommand(options, dependencies = {}) {
|
|
6299
|
-
const slug = normalizeSlug2(options.slug);
|
|
6300
|
-
const mode = normalizeMode(options.mode);
|
|
6301
|
-
const cwd2 = dependencies.cwd ?? process.cwd();
|
|
6302
|
-
const log = dependencies.log ?? ((message) => console.log(message));
|
|
6303
|
-
const resolved = await resolveRunInput({
|
|
6304
|
-
idea: `Enrich links for ${slug}`
|
|
6305
|
-
});
|
|
6306
|
-
const markdownPath = await resolveMarkdownPathForSlug2(resolved.config.settings, slug, cwd2);
|
|
6307
|
-
const frontmatter = await readFrontmatter(markdownPath);
|
|
6308
|
-
const fileId = path9.parse(markdownPath).name;
|
|
6309
|
-
const articleTitle = frontmatter.title ?? toTitleFromSlug(slug);
|
|
6310
|
-
const articleDescription = frontmatter.description ?? "";
|
|
6311
|
-
const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
|
|
6312
|
-
if (!openRouterApiKey) {
|
|
6313
|
-
throw new ReportedError(
|
|
6314
|
-
"Missing OpenRouter API key. Run `ideon settings` to configure credentials or set IDEON_OPENROUTER_API_KEY."
|
|
6315
|
-
);
|
|
6316
|
-
}
|
|
6317
|
-
const openRouter = new OpenRouterClient(openRouterApiKey);
|
|
6318
|
-
const linksPath = resolveLinksPath(markdownPath);
|
|
6319
|
-
const existing = await readExistingLinks(linksPath);
|
|
6320
|
-
const updatedCustomLinks = resolveCustomLinks(existing?.customLinks ?? [], options.links ?? [], options.unlinks ?? []);
|
|
6321
|
-
const effectiveMaxLinks = options.maxLinks;
|
|
6322
|
-
const linksResult = await enrichLinks({
|
|
6323
|
-
markdownFiles: [{ markdownPath, fileId, contentType: "article" }],
|
|
6324
|
-
articleTitle,
|
|
6325
|
-
articleDescription,
|
|
6326
|
-
openRouter,
|
|
6327
|
-
settings: resolved.config.settings,
|
|
6328
|
-
dryRun: false,
|
|
6329
|
-
customLinks: updatedCustomLinks,
|
|
6330
|
-
maxLinks: effectiveMaxLinks,
|
|
6331
|
-
onItemProgress(event) {
|
|
6332
|
-
logProgress(event, log);
|
|
6333
|
-
}
|
|
6334
|
-
});
|
|
6335
|
-
const generatedLinks = linksResult[0]?.links ?? [];
|
|
6336
|
-
const mergedGeneratedLinks = mode === "append" ? mergeLinks(existing?.links ?? [], generatedLinks) : generatedLinks;
|
|
6337
|
-
const appendedCount = Math.max(0, mergedGeneratedLinks.length - (existing?.links.length ?? 0));
|
|
6338
|
-
await writeLinksFile(markdownPath, {
|
|
6339
|
-
version: 2,
|
|
6340
|
-
customLinks: updatedCustomLinks,
|
|
6341
|
-
links: mergedGeneratedLinks
|
|
6342
|
-
});
|
|
6343
|
-
const relativeMarkdownPath = formatRelativePath2(cwd2, markdownPath);
|
|
6344
|
-
const relativeLinksPath = formatRelativePath2(cwd2, linksPath);
|
|
6345
|
-
if (mode === "fresh") {
|
|
6346
|
-
const replaced = existing ? "Replaced existing links." : "Created links sidecar.";
|
|
6347
|
-
log(`Enriched links for "${slug}".`);
|
|
6348
|
-
log(`${replaced} Saved ${generatedLinks.length} generated + ${updatedCustomLinks.length} custom links to ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6349
|
-
return;
|
|
6350
|
-
}
|
|
6351
|
-
const baseCount = existing?.links.length ?? 0;
|
|
6352
|
-
const verb = existing ? "Appended and deduplicated links." : "Created links sidecar.";
|
|
6353
|
-
log(`Enriched links for "${slug}".`);
|
|
6354
|
-
log(`${verb} Base ${baseCount}, added ${appendedCount}, total ${mergedGeneratedLinks.length} generated + ${updatedCustomLinks.length} custom in ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6355
|
-
}
|
|
6356
|
-
function normalizeMode(rawMode) {
|
|
6357
|
-
const normalized = rawMode.trim().toLowerCase();
|
|
6358
|
-
if (normalized === "fresh" || normalized === "append") {
|
|
6359
|
-
return normalized;
|
|
6360
|
-
}
|
|
6361
|
-
throw new ReportedError(`Unsupported --mode value "${rawMode}". Use "fresh" or "append".`);
|
|
6362
|
-
}
|
|
6363
|
-
function normalizeSlug2(rawSlug) {
|
|
6364
|
-
const slug = rawSlug.trim();
|
|
6365
|
-
if (!slug) {
|
|
6366
|
-
throw new ReportedError("Slug cannot be empty. Pass the generated article slug, for example `ideon links my-article-slug`.");
|
|
6367
|
-
}
|
|
6368
|
-
if (slug.toLowerCase().endsWith(".md")) {
|
|
6369
|
-
throw new ReportedError(`Expected a slug, not a markdown filename: ${slug}. Pass the slug without .md.`);
|
|
6370
|
-
}
|
|
6371
|
-
if (slug === "." || slug === ".." || /[/\\]/.test(slug)) {
|
|
6372
|
-
throw new ReportedError(`Invalid slug "${slug}". Pass the article slug only, without any path separators.`);
|
|
6373
|
-
}
|
|
6374
|
-
return slug;
|
|
6375
|
-
}
|
|
6376
|
-
async function resolveMarkdownPathForSlug2(settings, slug, cwd2) {
|
|
6377
|
-
const outputPaths = resolveOutputPaths(settings, cwd2);
|
|
6378
|
-
const directPath = path9.join(outputPaths.markdownOutputDir, `${slug}.md`);
|
|
6379
|
-
if (await isReadableFile(directPath)) {
|
|
6380
|
-
return directPath;
|
|
6381
|
-
}
|
|
6382
|
-
const markdownFiles = await listMarkdownFilesRecursively(outputPaths.markdownOutputDir);
|
|
6383
|
-
const matches = [];
|
|
6384
|
-
for (const candidate of markdownFiles) {
|
|
6385
|
-
if (path9.basename(candidate) === `${slug}.md`) {
|
|
6386
|
-
matches.push(candidate);
|
|
6387
|
-
continue;
|
|
6388
|
-
}
|
|
6389
|
-
const frontmatter = await readFrontmatter(candidate);
|
|
6390
|
-
if (frontmatter.slug === slug) {
|
|
6391
|
-
matches.push(candidate);
|
|
6392
|
-
}
|
|
6393
|
-
}
|
|
6394
|
-
if (matches.length === 0) {
|
|
6395
|
-
throw new ReportedError(
|
|
6396
|
-
`Could not find article "${slug}". Expected a markdown file in ${outputPaths.markdownOutputDir} with frontmatter slug "${slug}".`
|
|
6397
|
-
);
|
|
6398
|
-
}
|
|
6399
|
-
return newestPath(matches);
|
|
6400
|
-
}
|
|
6401
|
-
async function newestPath(paths) {
|
|
6402
|
-
let latestPath = paths[0];
|
|
6403
|
-
let latestMtime = 0;
|
|
6404
|
-
for (const candidate of paths) {
|
|
6405
|
-
const candidateStat = await stat3(candidate);
|
|
6406
|
-
if (candidateStat.mtimeMs >= latestMtime) {
|
|
6407
|
-
latestMtime = candidateStat.mtimeMs;
|
|
6408
|
-
latestPath = candidate;
|
|
6409
|
-
}
|
|
6410
|
-
}
|
|
6411
|
-
return latestPath;
|
|
6412
|
-
}
|
|
6413
|
-
async function readFrontmatter(markdownPath) {
|
|
6414
|
-
const markdown = await readFile6(markdownPath, "utf8");
|
|
6415
|
-
return parseFrontmatter(markdown);
|
|
6416
|
-
}
|
|
6417
|
-
function parseFrontmatter(markdown) {
|
|
6418
|
-
if (!markdown.startsWith("---\n")) {
|
|
6419
|
-
return { slug: null, title: null, description: null };
|
|
6420
|
-
}
|
|
6421
|
-
const frontmatterEnd = markdown.indexOf("\n---\n", 4);
|
|
6422
|
-
if (frontmatterEnd < 0) {
|
|
6423
|
-
return { slug: null, title: null, description: null };
|
|
6424
|
-
}
|
|
6425
|
-
const block = markdown.slice(4, frontmatterEnd);
|
|
6426
|
-
return {
|
|
6427
|
-
slug: parseFrontmatterValue(block, "slug"),
|
|
6428
|
-
title: parseFrontmatterValue(block, "title"),
|
|
6429
|
-
description: parseFrontmatterValue(block, "description")
|
|
6430
|
-
};
|
|
6431
|
-
}
|
|
6432
|
-
function parseFrontmatterValue(block, key) {
|
|
6433
|
-
const pattern = new RegExp(`^${key}:\\s*(.+)$`, "m");
|
|
6434
|
-
const match = block.match(pattern);
|
|
6435
|
-
if (!match || !match[1]) {
|
|
6436
|
-
return null;
|
|
6437
|
-
}
|
|
6438
|
-
const rawValue = match[1].trim();
|
|
6439
|
-
if (!rawValue) {
|
|
6440
|
-
return null;
|
|
6441
|
-
}
|
|
6442
|
-
if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
|
|
6443
|
-
return rawValue.slice(1, -1);
|
|
6444
|
-
}
|
|
6445
|
-
return rawValue;
|
|
6446
|
-
}
|
|
6447
|
-
function toTitleFromSlug(slug) {
|
|
6448
|
-
return slug.split("-").filter((part) => part.length > 0).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
6449
|
-
}
|
|
6450
|
-
async function isReadableFile(filePath) {
|
|
6451
|
-
try {
|
|
6452
|
-
const fileStat = await stat3(filePath);
|
|
6453
|
-
return fileStat.isFile();
|
|
6454
|
-
} catch {
|
|
6455
|
-
return false;
|
|
6456
|
-
}
|
|
6457
|
-
}
|
|
6458
|
-
async function readExistingLinks(linksPath) {
|
|
6459
|
-
try {
|
|
6460
|
-
const raw = await readFile6(linksPath, "utf8");
|
|
6461
|
-
const parsed = JSON.parse(raw);
|
|
6462
|
-
const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6463
|
-
expression: entry.expression.trim(),
|
|
6464
|
-
url: entry.url.trim(),
|
|
6465
|
-
title: typeof entry.title === "string" ? entry.title : null
|
|
6466
|
-
})) : null;
|
|
6467
|
-
if (!links) {
|
|
6468
|
-
throw new ReportedError(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
|
|
6469
|
-
}
|
|
6470
|
-
const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6471
|
-
expression: entry.expression.trim(),
|
|
6472
|
-
url: entry.url.trim(),
|
|
6473
|
-
title: typeof entry.title === "string" ? entry.title : null
|
|
6474
|
-
})) : [];
|
|
6475
|
-
return {
|
|
6476
|
-
version: typeof parsed.version === "number" ? parsed.version : 1,
|
|
6477
|
-
customLinks,
|
|
6478
|
-
links
|
|
6479
|
-
};
|
|
6480
|
-
} catch (error) {
|
|
6481
|
-
if (readErrorCode2(error) === "ENOENT") {
|
|
6482
|
-
return null;
|
|
6483
|
-
}
|
|
6484
|
-
if (error instanceof ReportedError) {
|
|
6485
|
-
throw error;
|
|
6486
|
-
}
|
|
6487
|
-
const message = error instanceof Error ? error.message : "Unknown links sidecar read error.";
|
|
6488
|
-
throw new ReportedError(`Failed to read existing links from ${linksPath}: ${message}`);
|
|
6489
|
-
}
|
|
6490
|
-
}
|
|
6491
|
-
function mergeLinks(existingLinks, generatedLinks) {
|
|
6492
|
-
const merged = [];
|
|
6493
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6494
|
-
for (const entry of [...existingLinks, ...generatedLinks]) {
|
|
6495
|
-
const key = `${entry.expression.trim().toLowerCase()}::${entry.url.trim().toLowerCase()}`;
|
|
6496
|
-
if (seen.has(key)) {
|
|
6497
|
-
continue;
|
|
6498
|
-
}
|
|
6499
|
-
seen.add(key);
|
|
6500
|
-
merged.push(entry);
|
|
6501
|
-
}
|
|
6502
|
-
return merged;
|
|
6503
|
-
}
|
|
6504
|
-
function isValidLinkEntry(value2) {
|
|
6505
|
-
if (typeof value2 !== "object" || value2 === null) {
|
|
6506
|
-
return false;
|
|
6507
|
-
}
|
|
6508
|
-
const record = value2;
|
|
6509
|
-
return typeof record.expression === "string" && typeof record.url === "string" && (typeof record.title === "string" || record.title === null || record.title === void 0);
|
|
6510
|
-
}
|
|
6511
|
-
function readErrorCode2(error) {
|
|
6512
|
-
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
6513
|
-
return null;
|
|
6514
|
-
}
|
|
6515
|
-
const code = error.code;
|
|
6516
|
-
return typeof code === "string" ? code : null;
|
|
6517
|
-
}
|
|
6518
|
-
function formatRelativePath2(cwd2, targetPath) {
|
|
6519
|
-
const relativePath = path9.relative(cwd2, targetPath);
|
|
6520
|
-
return relativePath.length > 0 ? relativePath : targetPath;
|
|
6521
|
-
}
|
|
6522
|
-
function logProgress(event, log) {
|
|
6523
|
-
if (event.phase === "resolving-expression" || event.phase === "selecting-expressions") {
|
|
6524
|
-
return;
|
|
6525
|
-
}
|
|
6526
|
-
log(event.detail);
|
|
6527
|
-
}
|
|
6528
|
-
function parseCustomLinkFlag(raw) {
|
|
6529
|
-
const separatorIndex = raw.indexOf("->");
|
|
6530
|
-
if (separatorIndex < 0) {
|
|
6531
|
-
throw new ReportedError(`Invalid --link value "${raw}". Expected format: "expression->url".`);
|
|
6532
|
-
}
|
|
6533
|
-
const expression = raw.slice(0, separatorIndex).trim();
|
|
6534
|
-
const url = raw.slice(separatorIndex + 2).trim();
|
|
6535
|
-
if (!expression) {
|
|
6536
|
-
throw new ReportedError(`Invalid --link value "${raw}": expression (left side of ->) cannot be empty.`);
|
|
6537
|
-
}
|
|
6538
|
-
if (!url) {
|
|
6539
|
-
throw new ReportedError(`Invalid --link value "${raw}": url (right side of ->) cannot be empty.`);
|
|
6540
|
-
}
|
|
6541
|
-
return { expression, url };
|
|
6542
|
-
}
|
|
6543
|
-
function resolveCustomLinks(existing, addRaw, removeExpressions) {
|
|
6544
|
-
const result = new Map(
|
|
6545
|
-
existing.map((entry) => [entry.expression.trim().toLowerCase(), entry])
|
|
6546
|
-
);
|
|
6547
|
-
for (const raw of addRaw) {
|
|
6548
|
-
const { expression, url } = parseCustomLinkFlag(raw);
|
|
6549
|
-
result.set(expression.toLowerCase(), { expression, url, title: null });
|
|
6550
|
-
}
|
|
6551
|
-
for (const expr of removeExpressions) {
|
|
6552
|
-
result.delete(expr.trim().toLowerCase());
|
|
6553
|
-
}
|
|
6554
|
-
return Array.from(result.values());
|
|
6555
|
-
}
|
|
6556
|
-
|
|
6557
6709
|
// src/cli/commands/settings.tsx
|
|
6558
6710
|
import { render } from "ink";
|
|
6559
6711
|
|
package/package.json
CHANGED
package/README.zh-Hans.md
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
# Ideon
|
|
2
|
-
|
|
3
|
-
Ideon 是一款 AI 内容写作工具,可将一个想法转化为多格式、多风格、可发布的内容。
|
|
4
|
-
|
|
5
|
-
[🇺🇸 English](./README.md) | [🇨🇳 简体中文](./README.zh-Hans.md)
|
|
6
|
-
|
|
7
|
-
[](https://github.com/telepat-io/ideon/actions/workflows/ci.yml)
|
|
8
|
-
[](https://codecov.io/gh/telepat-io/ideon)
|
|
9
|
-
[](https://www.npmjs.com/package/@telepat/ideon)
|
|
10
|
-
[](https://docs.telepat.io/ideon)
|
|
11
|
-
[](https://github.com/telepat-io/ideon/blob/main/LICENSE)
|
|
12
|
-
|
|
13
|
-
## 为什么团队使用 Ideon
|
|
14
|
-
|
|
15
|
-
Ideon 帮助团队更快地从想法走到可发布内容,减少跨渠道重复改写的工作量。
|
|
16
|
-
|
|
17
|
-
一次运行,Ideon 可以:
|
|
18
|
-
|
|
19
|
-
- 基于同一核心想法生成多种输出类型:article、blog、newsletter、Reddit、LinkedIn、X thread、X post 等。
|
|
20
|
-
- 在所有输出中统一应用写作风格(`professional`、`friendly`、`technical`、`academic`、`opinionated`、`storytelling`)。
|
|
21
|
-
- 生成研究导向的内容 brief,补充相关链接,并在文章型任务中生成配图。
|
|
22
|
-
- 通过作业文件、可配置参数与断点恢复能力支持持续迭代。
|
|
23
|
-
|
|
24
|
-
这使 Ideon 适用于内容团队、开发者关系、产品营销、创始人以及需要按节奏进行多渠道写作的个人或团队。
|
|
25
|
-
|
|
26
|
-
## 快速开始
|
|
27
|
-
|
|
28
|
-
安装并完成第一次内容生成:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npm i -g @telepat/ideon
|
|
32
|
-
ideon settings
|
|
33
|
-
ideon write "How small editorial teams can productionize AI writing" --primary article=1 --secondary x-post=1
|
|
34
|
-
ideon preview
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
预期结果:
|
|
38
|
-
|
|
39
|
-
- 在 `output/<timestamp>-<slug>/` 下生成一个运行目录。
|
|
40
|
-
- 产出一个或多个可发布的 Markdown 输出。
|
|
41
|
-
- 保存 analytics 与元数据,便于复现与回溯。
|
|
42
|
-
- 本地预览自动打开,便于检查内容、链接与资源。
|
|
43
|
-
|
|
44
|
-
## 环境要求
|
|
45
|
-
|
|
46
|
-
- Node.js 20+
|
|
47
|
-
- npm 10+
|
|
48
|
-
- OpenRouter API key
|
|
49
|
-
- Replicate API token
|
|
50
|
-
|
|
51
|
-
## 核心能力
|
|
52
|
-
|
|
53
|
-
- 一次想法,多格式写作:article、blog、newsletter、Reddit、LinkedIn、X thread、X post、landing-page copy。
|
|
54
|
-
- 风格控制:按运行设置统一写作语气与表达风格。
|
|
55
|
-
- 研究与链接增强:生成规划 brief,并为内容补充相关链接。
|
|
56
|
-
- 图像生成:为文章型任务生成封面图与文内配图。
|
|
57
|
-
- 迭代能力:可通过不同目标/风格重复生成,支持中断恢复和作业文件复用。
|
|
58
|
-
- 本地审阅:在发布前通过浏览器预览内容与资源。
|
|
59
|
-
|
|
60
|
-
## 工作原理
|
|
61
|
-
|
|
62
|
-
Ideon 采用分阶段写作流水线:内容规划、正文生成、图像提示扩展、图像渲染、渠道内容生成,以及可选的链接增强。
|
|
63
|
-
|
|
64
|
-
运行时会合并 settings、环境变量、job 文件与 CLI 参数,并输出结构化产物,便于追踪与复用。
|
|
65
|
-
|
|
66
|
-
核心命令:
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
ideon settings
|
|
70
|
-
ideon config list --json
|
|
71
|
-
ideon write "An article idea" --primary article=1
|
|
72
|
-
ideon write --no-interactive --idea "An article idea" --primary article=1 --style technical --length medium
|
|
73
|
-
ideon write --job ./job.json
|
|
74
|
-
ideon write resume
|
|
75
|
-
ideon delete my-article-slug
|
|
76
|
-
ideon preview --no-open
|
|
77
|
-
ideon mcp serve
|
|
78
|
-
ideon agent status --json
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Agent 集成范围:
|
|
82
|
-
|
|
83
|
-
- 支持:CLI 与 MCP runtime 工作流。
|
|
84
|
-
- 不支持:Cursor 与 VS Code runtime 集成。
|
|
85
|
-
|
|
86
|
-
## 安全与信任
|
|
87
|
-
|
|
88
|
-
- 默认通过 `ideon settings` 将密钥保存到系统钥匙串。
|
|
89
|
-
- 在 CI 或容器环境中,请使用 `IDEON_OPENROUTER_API_KEY` 和 `IDEON_REPLICATE_API_TOKEN`。
|
|
90
|
-
- 在无法访问钥匙串时设置 `IDEON_DISABLE_KEYTAR=true`。
|
|
91
|
-
- 生成内容来自模型输出,发布前请进行人工审阅。
|
|
92
|
-
|
|
93
|
-
如需报告安全问题,请通过仓库安全报告通道私下提交,或通过仓库 issue 渠道联系维护者并避免包含敏感细节。
|
|
94
|
-
|
|
95
|
-
## 文档与支持
|
|
96
|
-
|
|
97
|
-
- 文档站点:https://docs.telepat.io/ideon
|
|
98
|
-
- 语言支持:English 与 简体中文(`README.md` / `README.zh-Hans.md`,以及双语文档)
|
|
99
|
-
- 快速上手:`docs/getting-started/quickstart.md`
|
|
100
|
-
- CLI 参考:`docs/reference/cli-reference.md`
|
|
101
|
-
- 配置指南:`docs/guides/configuration.md`
|
|
102
|
-
- 故障排查:`docs/guides/troubleshooting.md`
|
|
103
|
-
- 仓库:https://github.com/telepat-io/ideon
|
|
104
|
-
- npm 包:https://www.npmjs.com/package/@telepat/ideon
|
|
105
|
-
|
|
106
|
-
## 贡献
|
|
107
|
-
|
|
108
|
-
欢迎贡献。请先阅读 `docs/contributing/development.md`(开发环境与质量门禁),再参考 `docs/contributing/releasing-and-docs-deploy.md`(发布与文档部署流程)。
|
|
109
|
-
|
|
110
|
-
如修改面向用户的文档内容,请在同一变更中同时更新 English 与 简体中文版本。
|
|
111
|
-
|
|
112
|
-
## 许可证
|
|
113
|
-
|
|
114
|
-
MIT。详见 [LICENSE](./LICENSE).
|