@telepat/ideon 0.1.14 → 0.1.16
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 +474 -328
- 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="./assets/avatar/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="./assets/avatar/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
|
@@ -521,7 +521,7 @@ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from
|
|
|
521
521
|
import path4 from "path";
|
|
522
522
|
import envPaths2 from "env-paths";
|
|
523
523
|
import { z as z2 } from "zod";
|
|
524
|
-
var supportedAgentRuntimeValues = ["claude", "chatgpt", "gemini", "generic-mcp"];
|
|
524
|
+
var supportedAgentRuntimeValues = ["claude", "claude-desktop", "chatgpt", "gemini", "codex", "cursor", "vscode", "opencode", "generic-mcp"];
|
|
525
525
|
var integrationEntrySchema = z2.object({
|
|
526
526
|
runtime: z2.enum(supportedAgentRuntimeValues),
|
|
527
527
|
installedAt: z2.string(),
|
|
@@ -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
|
|
|
@@ -1159,7 +1208,6 @@ function compareStringArrays(drifts, id, actual, expected) {
|
|
|
1159
1208
|
}
|
|
1160
1209
|
|
|
1161
1210
|
// src/cli/commands/agent.ts
|
|
1162
|
-
var unsupportedIdeRuntimeAliases = ["cursor", "vscode"];
|
|
1163
1211
|
var defaultDependencies = {
|
|
1164
1212
|
install: installAgentIntegration,
|
|
1165
1213
|
uninstall: uninstallAgentIntegration,
|
|
@@ -1229,11 +1277,6 @@ async function collectAgentStatus(deps) {
|
|
|
1229
1277
|
}
|
|
1230
1278
|
function parseRuntime(rawRuntime) {
|
|
1231
1279
|
const runtime = rawRuntime.trim().toLowerCase();
|
|
1232
|
-
if (unsupportedIdeRuntimeAliases.includes(runtime)) {
|
|
1233
|
-
throw new ReportedError(
|
|
1234
|
-
`Unsupported runtime "${rawRuntime}". Ideon agent integration does not support Cursor or VS Code runtimes.`
|
|
1235
|
-
);
|
|
1236
|
-
}
|
|
1237
1280
|
if (!supportedAgentRuntimeValues.includes(runtime)) {
|
|
1238
1281
|
throw new ReportedError(
|
|
1239
1282
|
`Unsupported runtime "${rawRuntime}". Supported runtimes: ${supportedAgentRuntimeValues.join(", ")}.`
|
|
@@ -1305,7 +1348,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
1305
1348
|
// package.json
|
|
1306
1349
|
var package_default = {
|
|
1307
1350
|
name: "@telepat/ideon",
|
|
1308
|
-
version: "0.1.
|
|
1351
|
+
version: "0.1.16",
|
|
1309
1352
|
description: "CLI for generating rich articles and images from ideas.",
|
|
1310
1353
|
type: "module",
|
|
1311
1354
|
repository: {
|
|
@@ -6005,76 +6048,338 @@ function parsePipelineCustomLinks(rawLinks, unlinks) {
|
|
|
6005
6048
|
return Array.from(result.values());
|
|
6006
6049
|
}
|
|
6007
6050
|
|
|
6008
|
-
// src/cli/commands/
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6051
|
+
// src/cli/commands/links.ts
|
|
6052
|
+
import { readFile as readFile6, stat as stat3 } from "fs/promises";
|
|
6053
|
+
import path9 from "path";
|
|
6054
|
+
async function runLinksCommand(options, dependencies = {}) {
|
|
6055
|
+
const slug = normalizeSlug2(options.slug);
|
|
6056
|
+
const mode = normalizeMode(options.mode);
|
|
6057
|
+
const cwd2 = dependencies.cwd ?? process.cwd();
|
|
6058
|
+
const log = dependencies.log ?? ((message) => console.log(message));
|
|
6059
|
+
const resolved = await resolveRunInput({
|
|
6060
|
+
idea: `Enrich links for ${slug}`
|
|
6061
|
+
});
|
|
6062
|
+
const markdownPath = await resolveMarkdownPathForSlug2(resolved.config.settings, slug, cwd2);
|
|
6063
|
+
const frontmatter = await readFrontmatter(markdownPath);
|
|
6064
|
+
const fileId = path9.parse(markdownPath).name;
|
|
6065
|
+
const articleTitle = frontmatter.title ?? toTitleFromSlug(slug);
|
|
6066
|
+
const articleDescription = frontmatter.description ?? "";
|
|
6067
|
+
const openRouterApiKey = resolved.config.secrets.openRouterApiKey;
|
|
6068
|
+
if (!openRouterApiKey) {
|
|
6069
|
+
throw new ReportedError(
|
|
6070
|
+
"Missing OpenRouter API key. Run `ideon settings` to configure credentials or set IDEON_OPENROUTER_API_KEY."
|
|
6071
|
+
);
|
|
6013
6072
|
}
|
|
6014
|
-
const
|
|
6015
|
-
|
|
6016
|
-
|
|
6073
|
+
const openRouter = new OpenRouterClient(openRouterApiKey);
|
|
6074
|
+
const linksPath = resolveLinksPath(markdownPath);
|
|
6075
|
+
const existing = await readExistingLinks(linksPath);
|
|
6076
|
+
const updatedCustomLinks = resolveCustomLinks(existing?.customLinks ?? [], options.links ?? [], options.unlinks ?? []);
|
|
6077
|
+
const effectiveMaxLinks = options.maxLinks;
|
|
6078
|
+
const linksResult = await enrichLinks({
|
|
6079
|
+
markdownFiles: [{ markdownPath, fileId, contentType: "article" }],
|
|
6080
|
+
articleTitle,
|
|
6081
|
+
articleDescription,
|
|
6082
|
+
openRouter,
|
|
6083
|
+
settings: resolved.config.settings,
|
|
6084
|
+
dryRun: false,
|
|
6085
|
+
customLinks: updatedCustomLinks,
|
|
6086
|
+
maxLinks: effectiveMaxLinks,
|
|
6087
|
+
onItemProgress(event) {
|
|
6088
|
+
logProgress(event, log);
|
|
6089
|
+
}
|
|
6090
|
+
});
|
|
6091
|
+
const generatedLinks = linksResult[0]?.links ?? [];
|
|
6092
|
+
const mergedGeneratedLinks = mode === "append" ? mergeLinks(existing?.links ?? [], generatedLinks) : generatedLinks;
|
|
6093
|
+
const appendedCount = Math.max(0, mergedGeneratedLinks.length - (existing?.links.length ?? 0));
|
|
6094
|
+
await writeLinksFile(markdownPath, {
|
|
6095
|
+
version: 2,
|
|
6096
|
+
customLinks: updatedCustomLinks,
|
|
6097
|
+
links: mergedGeneratedLinks
|
|
6098
|
+
});
|
|
6099
|
+
const relativeMarkdownPath = formatRelativePath2(cwd2, markdownPath);
|
|
6100
|
+
const relativeLinksPath = formatRelativePath2(cwd2, linksPath);
|
|
6101
|
+
if (mode === "fresh") {
|
|
6102
|
+
const replaced = existing ? "Replaced existing links." : "Created links sidecar.";
|
|
6103
|
+
log(`Enriched links for "${slug}".`);
|
|
6104
|
+
log(`${replaced} Saved ${generatedLinks.length} generated + ${updatedCustomLinks.length} custom links to ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6105
|
+
return;
|
|
6017
6106
|
}
|
|
6018
|
-
const
|
|
6019
|
-
|
|
6107
|
+
const baseCount = existing?.links.length ?? 0;
|
|
6108
|
+
const verb = existing ? "Appended and deduplicated links." : "Created links sidecar.";
|
|
6109
|
+
log(`Enriched links for "${slug}".`);
|
|
6110
|
+
log(`${verb} Base ${baseCount}, added ${appendedCount}, total ${mergedGeneratedLinks.length} generated + ${updatedCustomLinks.length} custom in ${relativeLinksPath} (${relativeMarkdownPath}).`);
|
|
6111
|
+
}
|
|
6112
|
+
function normalizeMode(rawMode) {
|
|
6113
|
+
const normalized = rawMode.trim().toLowerCase();
|
|
6114
|
+
if (normalized === "fresh" || normalized === "append") {
|
|
6115
|
+
return normalized;
|
|
6116
|
+
}
|
|
6117
|
+
throw new ReportedError(`Unsupported --mode value "${rawMode}". Use "fresh" or "append".`);
|
|
6118
|
+
}
|
|
6119
|
+
function normalizeSlug2(rawSlug) {
|
|
6120
|
+
const slug = rawSlug.trim();
|
|
6121
|
+
if (!slug) {
|
|
6122
|
+
throw new ReportedError("Slug cannot be empty. Pass the generated article slug, for example `ideon links my-article-slug`.");
|
|
6123
|
+
}
|
|
6124
|
+
if (slug.toLowerCase().endsWith(".md")) {
|
|
6125
|
+
throw new ReportedError(`Expected a slug, not a markdown filename: ${slug}. Pass the slug without .md.`);
|
|
6126
|
+
}
|
|
6127
|
+
if (slug === "." || slug === ".." || /[/\\]/.test(slug)) {
|
|
6128
|
+
throw new ReportedError(`Invalid slug "${slug}". Pass the article slug only, without any path separators.`);
|
|
6129
|
+
}
|
|
6130
|
+
return slug;
|
|
6131
|
+
}
|
|
6132
|
+
async function resolveMarkdownPathForSlug2(settings, slug, cwd2) {
|
|
6133
|
+
const outputPaths = resolveOutputPaths(settings, cwd2);
|
|
6134
|
+
const directPath = path9.join(outputPaths.markdownOutputDir, `${slug}.md`);
|
|
6135
|
+
if (await isReadableFile(directPath)) {
|
|
6136
|
+
return directPath;
|
|
6137
|
+
}
|
|
6138
|
+
const markdownFiles = await listMarkdownFilesRecursively(outputPaths.markdownOutputDir);
|
|
6139
|
+
const matches = [];
|
|
6140
|
+
for (const candidate of markdownFiles) {
|
|
6141
|
+
if (path9.basename(candidate) === `${slug}.md`) {
|
|
6142
|
+
matches.push(candidate);
|
|
6143
|
+
continue;
|
|
6144
|
+
}
|
|
6145
|
+
const frontmatter = await readFrontmatter(candidate);
|
|
6146
|
+
if (frontmatter.slug === slug) {
|
|
6147
|
+
matches.push(candidate);
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
if (matches.length === 0) {
|
|
6020
6151
|
throw new ReportedError(
|
|
6021
|
-
`
|
|
6152
|
+
`Could not find article "${slug}". Expected a markdown file in ${outputPaths.markdownOutputDir} with frontmatter slug "${slug}".`
|
|
6022
6153
|
);
|
|
6023
6154
|
}
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6155
|
+
return newestPath(matches);
|
|
6156
|
+
}
|
|
6157
|
+
async function newestPath(paths) {
|
|
6158
|
+
let latestPath = paths[0];
|
|
6159
|
+
let latestMtime = 0;
|
|
6160
|
+
for (const candidate of paths) {
|
|
6161
|
+
const candidateStat = await stat3(candidate);
|
|
6162
|
+
if (candidateStat.mtimeMs >= latestMtime) {
|
|
6163
|
+
latestMtime = candidateStat.mtimeMs;
|
|
6164
|
+
latestPath = candidate;
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
return latestPath;
|
|
6168
|
+
}
|
|
6169
|
+
async function readFrontmatter(markdownPath) {
|
|
6170
|
+
const markdown = await readFile6(markdownPath, "utf8");
|
|
6171
|
+
return parseFrontmatter(markdown);
|
|
6172
|
+
}
|
|
6173
|
+
function parseFrontmatter(markdown) {
|
|
6174
|
+
if (!markdown.startsWith("---\n")) {
|
|
6175
|
+
return { slug: null, title: null, description: null };
|
|
6176
|
+
}
|
|
6177
|
+
const frontmatterEnd = markdown.indexOf("\n---\n", 4);
|
|
6178
|
+
if (frontmatterEnd < 0) {
|
|
6179
|
+
return { slug: null, title: null, description: null };
|
|
6027
6180
|
}
|
|
6181
|
+
const block = markdown.slice(4, frontmatterEnd);
|
|
6028
6182
|
return {
|
|
6029
|
-
|
|
6030
|
-
|
|
6183
|
+
slug: parseFrontmatterValue(block, "slug"),
|
|
6184
|
+
title: parseFrontmatterValue(block, "title"),
|
|
6185
|
+
description: parseFrontmatterValue(block, "description")
|
|
6031
6186
|
};
|
|
6032
6187
|
}
|
|
6033
|
-
function
|
|
6034
|
-
const
|
|
6035
|
-
|
|
6036
|
-
|
|
6188
|
+
function parseFrontmatterValue(block, key) {
|
|
6189
|
+
const pattern = new RegExp(`^${key}:\\s*(.+)$`, "m");
|
|
6190
|
+
const match = block.match(pattern);
|
|
6191
|
+
if (!match || !match[1]) {
|
|
6192
|
+
return null;
|
|
6037
6193
|
}
|
|
6038
|
-
|
|
6039
|
-
|
|
6194
|
+
const rawValue = match[1].trim();
|
|
6195
|
+
if (!rawValue) {
|
|
6196
|
+
return null;
|
|
6040
6197
|
}
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
throw new ReportedError("Primary target count must be exactly 1. Use --primary <content-type=1>.");
|
|
6198
|
+
if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
|
|
6199
|
+
return rawValue.slice(1, -1);
|
|
6044
6200
|
}
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6201
|
+
return rawValue;
|
|
6202
|
+
}
|
|
6203
|
+
function toTitleFromSlug(slug) {
|
|
6204
|
+
return slug.split("-").filter((part) => part.length > 0).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
6205
|
+
}
|
|
6206
|
+
async function isReadableFile(filePath) {
|
|
6207
|
+
try {
|
|
6208
|
+
const fileStat = await stat3(filePath);
|
|
6209
|
+
return fileStat.isFile();
|
|
6210
|
+
} catch {
|
|
6211
|
+
return false;
|
|
6212
|
+
}
|
|
6213
|
+
}
|
|
6214
|
+
async function readExistingLinks(linksPath) {
|
|
6215
|
+
try {
|
|
6216
|
+
const raw = await readFile6(linksPath, "utf8");
|
|
6217
|
+
const parsed = JSON.parse(raw);
|
|
6218
|
+
const links = Array.isArray(parsed.links) ? parsed.links.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6219
|
+
expression: entry.expression.trim(),
|
|
6220
|
+
url: entry.url.trim(),
|
|
6221
|
+
title: typeof entry.title === "string" ? entry.title : null
|
|
6222
|
+
})) : null;
|
|
6223
|
+
if (!links) {
|
|
6224
|
+
throw new ReportedError(`Invalid links sidecar format at ${linksPath}. Expected { version, links[] }.`);
|
|
6052
6225
|
}
|
|
6053
|
-
const
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6226
|
+
const customLinks = Array.isArray(parsed.customLinks) ? parsed.customLinks.filter((entry) => isValidLinkEntry(entry)).map((entry) => ({
|
|
6227
|
+
expression: entry.expression.trim(),
|
|
6228
|
+
url: entry.url.trim(),
|
|
6229
|
+
title: typeof entry.title === "string" ? entry.title : null
|
|
6230
|
+
})) : [];
|
|
6231
|
+
return {
|
|
6232
|
+
version: typeof parsed.version === "number" ? parsed.version : 1,
|
|
6233
|
+
customLinks,
|
|
6234
|
+
links
|
|
6235
|
+
};
|
|
6236
|
+
} catch (error) {
|
|
6237
|
+
if (readErrorCode2(error) === "ENOENT") {
|
|
6238
|
+
return null;
|
|
6057
6239
|
}
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6240
|
+
if (error instanceof ReportedError) {
|
|
6241
|
+
throw error;
|
|
6242
|
+
}
|
|
6243
|
+
const message = error instanceof Error ? error.message : "Unknown links sidecar read error.";
|
|
6244
|
+
throw new ReportedError(`Failed to read existing links from ${linksPath}: ${message}`);
|
|
6062
6245
|
}
|
|
6063
|
-
return [
|
|
6064
|
-
{
|
|
6065
|
-
...primary,
|
|
6066
|
-
role: "primary"
|
|
6067
|
-
},
|
|
6068
|
-
...secondaryDedupedByType.values()
|
|
6069
|
-
];
|
|
6070
6246
|
}
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
const
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6247
|
+
function mergeLinks(existingLinks, generatedLinks) {
|
|
6248
|
+
const merged = [];
|
|
6249
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6250
|
+
for (const entry of [...existingLinks, ...generatedLinks]) {
|
|
6251
|
+
const key = `${entry.expression.trim().toLowerCase()}::${entry.url.trim().toLowerCase()}`;
|
|
6252
|
+
if (seen.has(key)) {
|
|
6253
|
+
continue;
|
|
6254
|
+
}
|
|
6255
|
+
seen.add(key);
|
|
6256
|
+
merged.push(entry);
|
|
6257
|
+
}
|
|
6258
|
+
return merged;
|
|
6259
|
+
}
|
|
6260
|
+
function isValidLinkEntry(value2) {
|
|
6261
|
+
if (typeof value2 !== "object" || value2 === null) {
|
|
6262
|
+
return false;
|
|
6263
|
+
}
|
|
6264
|
+
const record = value2;
|
|
6265
|
+
return typeof record.expression === "string" && typeof record.url === "string" && (typeof record.title === "string" || record.title === null || record.title === void 0);
|
|
6266
|
+
}
|
|
6267
|
+
function readErrorCode2(error) {
|
|
6268
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
6269
|
+
return null;
|
|
6270
|
+
}
|
|
6271
|
+
const code = error.code;
|
|
6272
|
+
return typeof code === "string" ? code : null;
|
|
6273
|
+
}
|
|
6274
|
+
function formatRelativePath2(cwd2, targetPath) {
|
|
6275
|
+
const relativePath = path9.relative(cwd2, targetPath);
|
|
6276
|
+
return relativePath.length > 0 ? relativePath : targetPath;
|
|
6277
|
+
}
|
|
6278
|
+
function logProgress(event, log) {
|
|
6279
|
+
if (event.phase === "resolving-expression" || event.phase === "selecting-expressions") {
|
|
6280
|
+
return;
|
|
6281
|
+
}
|
|
6282
|
+
log(event.detail);
|
|
6283
|
+
}
|
|
6284
|
+
function parseCustomLinkFlag(raw) {
|
|
6285
|
+
const separatorIndex = raw.indexOf("->");
|
|
6286
|
+
if (separatorIndex < 0) {
|
|
6287
|
+
throw new ReportedError(`Invalid --link value "${raw}". Expected format: "expression->url".`);
|
|
6288
|
+
}
|
|
6289
|
+
const expression = raw.slice(0, separatorIndex).trim();
|
|
6290
|
+
const url = raw.slice(separatorIndex + 2).trim();
|
|
6291
|
+
if (!expression) {
|
|
6292
|
+
throw new ReportedError(`Invalid --link value "${raw}": expression (left side of ->) cannot be empty.`);
|
|
6293
|
+
}
|
|
6294
|
+
if (!url) {
|
|
6295
|
+
throw new ReportedError(`Invalid --link value "${raw}": url (right side of ->) cannot be empty.`);
|
|
6296
|
+
}
|
|
6297
|
+
return { expression, url };
|
|
6298
|
+
}
|
|
6299
|
+
function resolveCustomLinks(existing, addRaw, removeExpressions) {
|
|
6300
|
+
const result = new Map(
|
|
6301
|
+
existing.map((entry) => [entry.expression.trim().toLowerCase(), entry])
|
|
6302
|
+
);
|
|
6303
|
+
for (const raw of addRaw) {
|
|
6304
|
+
const { expression, url } = parseCustomLinkFlag(raw);
|
|
6305
|
+
result.set(expression.toLowerCase(), { expression, url, title: null });
|
|
6306
|
+
}
|
|
6307
|
+
for (const expr of removeExpressions) {
|
|
6308
|
+
result.delete(expr.trim().toLowerCase());
|
|
6309
|
+
}
|
|
6310
|
+
return Array.from(result.values());
|
|
6311
|
+
}
|
|
6312
|
+
|
|
6313
|
+
// src/cli/commands/writeTargetSpecs.ts
|
|
6314
|
+
function parseTargetSpec(spec) {
|
|
6315
|
+
const trimmed = spec.trim();
|
|
6316
|
+
if (!trimmed) {
|
|
6317
|
+
throw new ReportedError("Target spec cannot be empty. Use <content-type=count>.");
|
|
6318
|
+
}
|
|
6319
|
+
const [rawType, rawCount] = trimmed.split("=");
|
|
6320
|
+
if (!rawType || !rawCount) {
|
|
6321
|
+
throw new ReportedError(`Invalid target "${spec}". Use format content-type=count, e.g. article=1 or x-thread=3.`);
|
|
6322
|
+
}
|
|
6323
|
+
const contentType = rawType.trim();
|
|
6324
|
+
if (!contentTypeValues.includes(contentType)) {
|
|
6325
|
+
throw new ReportedError(
|
|
6326
|
+
`Unsupported content type "${contentType}". Supported values: ${contentTypeValues.join(", ")}.`
|
|
6327
|
+
);
|
|
6328
|
+
}
|
|
6329
|
+
const count = Number.parseInt(rawCount.trim(), 10);
|
|
6330
|
+
if (!Number.isFinite(count) || count <= 0) {
|
|
6331
|
+
throw new ReportedError(`Invalid count in target "${spec}". Count must be a positive integer.`);
|
|
6332
|
+
}
|
|
6333
|
+
return {
|
|
6334
|
+
contentType,
|
|
6335
|
+
count
|
|
6336
|
+
};
|
|
6337
|
+
}
|
|
6338
|
+
function parsePrimaryAndSecondarySpecs(options) {
|
|
6339
|
+
const { primarySpec, secondarySpecs } = options;
|
|
6340
|
+
if (!primarySpec && (!secondarySpecs || secondarySpecs.length === 0)) {
|
|
6341
|
+
return void 0;
|
|
6342
|
+
}
|
|
6343
|
+
if (!primarySpec) {
|
|
6344
|
+
throw new ReportedError("Missing required --primary <content-type=count>.");
|
|
6345
|
+
}
|
|
6346
|
+
const primary = parseTargetSpec(primarySpec);
|
|
6347
|
+
if (primary.count !== 1) {
|
|
6348
|
+
throw new ReportedError("Primary target count must be exactly 1. Use --primary <content-type=1>.");
|
|
6349
|
+
}
|
|
6350
|
+
const secondaryDedupedByType = /* @__PURE__ */ new Map();
|
|
6351
|
+
for (const spec of secondarySpecs ?? []) {
|
|
6352
|
+
const parsed = parseTargetSpec(spec);
|
|
6353
|
+
if (parsed.contentType === primary.contentType) {
|
|
6354
|
+
throw new ReportedError(
|
|
6355
|
+
`Content type "${parsed.contentType}" cannot be both primary and secondary in the same run.`
|
|
6356
|
+
);
|
|
6357
|
+
}
|
|
6358
|
+
const previous = secondaryDedupedByType.get(parsed.contentType);
|
|
6359
|
+
if (previous) {
|
|
6360
|
+
previous.count += parsed.count;
|
|
6361
|
+
continue;
|
|
6362
|
+
}
|
|
6363
|
+
secondaryDedupedByType.set(parsed.contentType, {
|
|
6364
|
+
...parsed,
|
|
6365
|
+
role: "secondary"
|
|
6366
|
+
});
|
|
6367
|
+
}
|
|
6368
|
+
return [
|
|
6369
|
+
{
|
|
6370
|
+
...primary,
|
|
6371
|
+
role: "primary"
|
|
6372
|
+
},
|
|
6373
|
+
...secondaryDedupedByType.values()
|
|
6374
|
+
];
|
|
6375
|
+
}
|
|
6376
|
+
|
|
6377
|
+
// src/integrations/mcp/server.ts
|
|
6378
|
+
async function startIdeonMcpServer() {
|
|
6379
|
+
const server = new McpServer({
|
|
6380
|
+
name: "ideon",
|
|
6381
|
+
version: package_default.version
|
|
6382
|
+
});
|
|
6078
6383
|
server.registerTool(
|
|
6079
6384
|
"ideon_write",
|
|
6080
6385
|
{
|
|
@@ -6093,6 +6398,7 @@ async function startIdeonMcpServer() {
|
|
|
6093
6398
|
audience: input.audience,
|
|
6094
6399
|
jobPath: input.jobPath,
|
|
6095
6400
|
style: input.style,
|
|
6401
|
+
intent: input.intent,
|
|
6096
6402
|
targetLength: input.length,
|
|
6097
6403
|
contentTargets: parsedTargets
|
|
6098
6404
|
});
|
|
@@ -6100,7 +6406,10 @@ async function startIdeonMcpServer() {
|
|
|
6100
6406
|
workingDir: cwd(),
|
|
6101
6407
|
runMode: "fresh",
|
|
6102
6408
|
dryRun: input.dryRun ?? false,
|
|
6103
|
-
enrichLinks: input.enrichLinks ??
|
|
6409
|
+
enrichLinks: input.enrichLinks ?? false,
|
|
6410
|
+
customLinks: input.link,
|
|
6411
|
+
unlinks: input.unlink,
|
|
6412
|
+
maxLinks: input.maxLinks
|
|
6104
6413
|
});
|
|
6105
6414
|
return {
|
|
6106
6415
|
content: [
|
|
@@ -6156,7 +6465,10 @@ async function startIdeonMcpServer() {
|
|
|
6156
6465
|
workingDir: cwd(),
|
|
6157
6466
|
runMode: "resume",
|
|
6158
6467
|
dryRun: input.dryRun ?? false,
|
|
6159
|
-
enrichLinks: input.enrichLinks ??
|
|
6468
|
+
enrichLinks: input.enrichLinks ?? false,
|
|
6469
|
+
customLinks: input.link,
|
|
6470
|
+
unlinks: input.unlink,
|
|
6471
|
+
maxLinks: input.maxLinks
|
|
6160
6472
|
});
|
|
6161
6473
|
return {
|
|
6162
6474
|
content: [
|
|
@@ -6215,6 +6527,48 @@ async function startIdeonMcpServer() {
|
|
|
6215
6527
|
}
|
|
6216
6528
|
}
|
|
6217
6529
|
);
|
|
6530
|
+
server.registerTool(
|
|
6531
|
+
"ideon_links",
|
|
6532
|
+
{
|
|
6533
|
+
title: "Ideon Links",
|
|
6534
|
+
description: "Run link enrichment for a previously generated article by slug.",
|
|
6535
|
+
inputSchema: linksToolInputSchema
|
|
6536
|
+
},
|
|
6537
|
+
async (input) => {
|
|
6538
|
+
try {
|
|
6539
|
+
const messages = [];
|
|
6540
|
+
await runLinksCommand(
|
|
6541
|
+
{
|
|
6542
|
+
slug: input.slug,
|
|
6543
|
+
mode: input.mode ?? "fresh",
|
|
6544
|
+
links: input.link,
|
|
6545
|
+
unlinks: input.unlink,
|
|
6546
|
+
maxLinks: input.maxLinks
|
|
6547
|
+
},
|
|
6548
|
+
{
|
|
6549
|
+
cwd: cwd(),
|
|
6550
|
+
log: (message) => {
|
|
6551
|
+
messages.push(message);
|
|
6552
|
+
}
|
|
6553
|
+
}
|
|
6554
|
+
);
|
|
6555
|
+
return {
|
|
6556
|
+
content: [
|
|
6557
|
+
{
|
|
6558
|
+
type: "text",
|
|
6559
|
+
text: messages.length > 0 ? messages.join("\n") : `Enriched links for ${input.slug}.`
|
|
6560
|
+
}
|
|
6561
|
+
],
|
|
6562
|
+
structuredContent: {
|
|
6563
|
+
slug: input.slug,
|
|
6564
|
+
mode: input.mode ?? "fresh"
|
|
6565
|
+
}
|
|
6566
|
+
};
|
|
6567
|
+
} catch (error) {
|
|
6568
|
+
return formatToolError(error);
|
|
6569
|
+
}
|
|
6570
|
+
}
|
|
6571
|
+
);
|
|
6218
6572
|
server.registerTool(
|
|
6219
6573
|
"ideon_config_get",
|
|
6220
6574
|
{
|
|
@@ -6276,6 +6630,60 @@ async function startIdeonMcpServer() {
|
|
|
6276
6630
|
}
|
|
6277
6631
|
}
|
|
6278
6632
|
);
|
|
6633
|
+
server.registerTool(
|
|
6634
|
+
"ideon_config_list",
|
|
6635
|
+
{
|
|
6636
|
+
title: "Ideon Config List",
|
|
6637
|
+
description: "List current settings and secret availability flags.",
|
|
6638
|
+
inputSchema: configListToolInputSchema
|
|
6639
|
+
},
|
|
6640
|
+
async (_input) => {
|
|
6641
|
+
try {
|
|
6642
|
+
const result = await configList();
|
|
6643
|
+
return {
|
|
6644
|
+
content: [
|
|
6645
|
+
{
|
|
6646
|
+
type: "text",
|
|
6647
|
+
text: JSON.stringify(result, null, 2)
|
|
6648
|
+
}
|
|
6649
|
+
],
|
|
6650
|
+
structuredContent: result
|
|
6651
|
+
};
|
|
6652
|
+
} catch (error) {
|
|
6653
|
+
return formatToolError(error);
|
|
6654
|
+
}
|
|
6655
|
+
}
|
|
6656
|
+
);
|
|
6657
|
+
server.registerTool(
|
|
6658
|
+
"ideon_config_unset",
|
|
6659
|
+
{
|
|
6660
|
+
title: "Ideon Config Unset",
|
|
6661
|
+
description: "Reset a setting to its default or delete a stored secret.",
|
|
6662
|
+
inputSchema: configUnsetToolInputSchema
|
|
6663
|
+
},
|
|
6664
|
+
async (input) => {
|
|
6665
|
+
try {
|
|
6666
|
+
if (!isConfigKey(input.key)) {
|
|
6667
|
+
throw new ReportedError(`Unsupported config key: ${input.key}`);
|
|
6668
|
+
}
|
|
6669
|
+
await configUnset(input.key);
|
|
6670
|
+
return {
|
|
6671
|
+
content: [
|
|
6672
|
+
{
|
|
6673
|
+
type: "text",
|
|
6674
|
+
text: `Unset ${input.key}.`
|
|
6675
|
+
}
|
|
6676
|
+
],
|
|
6677
|
+
structuredContent: {
|
|
6678
|
+
key: input.key,
|
|
6679
|
+
updated: true
|
|
6680
|
+
}
|
|
6681
|
+
};
|
|
6682
|
+
} catch (error) {
|
|
6683
|
+
return formatToolError(error);
|
|
6684
|
+
}
|
|
6685
|
+
}
|
|
6686
|
+
);
|
|
6279
6687
|
const transport = new StdioServerTransport();
|
|
6280
6688
|
await server.connect(transport);
|
|
6281
6689
|
}
|
|
@@ -6292,268 +6700,6 @@ async function runMcpServeCommand() {
|
|
|
6292
6700
|
await startIdeonMcpServer();
|
|
6293
6701
|
}
|
|
6294
6702
|
|
|
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
6703
|
// src/cli/commands/settings.tsx
|
|
6558
6704
|
import { render } from "ink";
|
|
6559
6705
|
|
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).
|