@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 CHANGED
@@ -1,16 +1,23 @@
1
- # Ideon
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
- [🇺🇸 English](./README.md) | [🇨🇳 简体中文](./README.zh-Hans.md)
6
-
7
- [![CI](https://github.com/telepat-io/ideon/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/telepat-io/ideon/actions/workflows/ci.yml)
8
- [![Coverage](https://codecov.io/gh/telepat-io/ideon/graph/badge.svg)](https://codecov.io/gh/telepat-io/ideon)
9
- [![npm version](https://img.shields.io/npm/v/%40telepat%2Fideon)](https://www.npmjs.com/package/@telepat/ideon)
10
- [![Docs](https://img.shields.io/badge/docs-live-1f6feb)](https://docs.telepat.io/ideon)
11
- [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](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
- Agent integration scope:
77
+ ## Using With AI Agents
78
+
79
+ Ideon is built for agentic workflows:
82
80
 
83
- - Supported: CLI and MCP runtime workflows.
84
- - Not supported: Cursor and VS Code runtime integrations.
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: https://docs.telepat.io/ideon
98
- - Language support: English and Simplified Chinese (`README.md` / `README.zh-Hans.md`, plus docs locales)
99
- - Getting started: `docs/getting-started/quickstart.md`
100
- - CLI reference: `docs/reference/cli-reference.md`
101
- - Configuration guide: `docs/guides/configuration.md`
102
- - Troubleshooting guide: `docs/guides/troubleshooting.md`
103
- - Repository: https://github.com/telepat-io/ideon
104
- - npm package: https://www.npmjs.com/package/@telepat/ideon
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 `docs/contributing/development.md` for setup, workflow, and quality gates, then follow `docs/contributing/releasing-and-docs-deploy.md` for release and docs deployment details.
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
 
@@ -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.14",
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/writeTargetSpecs.ts
6009
- function parseTargetSpec(spec) {
6010
- const trimmed = spec.trim();
6011
- if (!trimmed) {
6012
- throw new ReportedError("Target spec cannot be empty. Use <content-type=count>.");
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 [rawType, rawCount] = trimmed.split("=");
6015
- if (!rawType || !rawCount) {
6016
- throw new ReportedError(`Invalid target "${spec}". Use format content-type=count, e.g. article=1 or x-thread=3.`);
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 contentType = rawType.trim();
6019
- if (!contentTypeValues.includes(contentType)) {
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
- `Unsupported content type "${contentType}". Supported values: ${contentTypeValues.join(", ")}.`
6152
+ `Could not find article "${slug}". Expected a markdown file in ${outputPaths.markdownOutputDir} with frontmatter slug "${slug}".`
6022
6153
  );
6023
6154
  }
6024
- const count = Number.parseInt(rawCount.trim(), 10);
6025
- if (!Number.isFinite(count) || count <= 0) {
6026
- throw new ReportedError(`Invalid count in target "${spec}". Count must be a positive integer.`);
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
- contentType,
6030
- count
6183
+ slug: parseFrontmatterValue(block, "slug"),
6184
+ title: parseFrontmatterValue(block, "title"),
6185
+ description: parseFrontmatterValue(block, "description")
6031
6186
  };
6032
6187
  }
6033
- function parsePrimaryAndSecondarySpecs(options) {
6034
- const { primarySpec, secondarySpecs } = options;
6035
- if (!primarySpec && (!secondarySpecs || secondarySpecs.length === 0)) {
6036
- return void 0;
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
- if (!primarySpec) {
6039
- throw new ReportedError("Missing required --primary <content-type=count>.");
6194
+ const rawValue = match[1].trim();
6195
+ if (!rawValue) {
6196
+ return null;
6040
6197
  }
6041
- const primary = parseTargetSpec(primarySpec);
6042
- if (primary.count !== 1) {
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
- const secondaryDedupedByType = /* @__PURE__ */ new Map();
6046
- for (const spec of secondarySpecs ?? []) {
6047
- const parsed = parseTargetSpec(spec);
6048
- if (parsed.contentType === primary.contentType) {
6049
- throw new ReportedError(
6050
- `Content type "${parsed.contentType}" cannot be both primary and secondary in the same run.`
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 previous = secondaryDedupedByType.get(parsed.contentType);
6054
- if (previous) {
6055
- previous.count += parsed.count;
6056
- continue;
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
- secondaryDedupedByType.set(parsed.contentType, {
6059
- ...parsed,
6060
- role: "secondary"
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
- // src/integrations/mcp/server.ts
6073
- async function startIdeonMcpServer() {
6074
- const server = new McpServer({
6075
- name: "ideon",
6076
- version: package_default.version
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 ?? true
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 ?? true
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telepat/ideon",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "CLI for generating rich articles and images from ideas.",
5
5
  "type": "module",
6
6
  "repository": {
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
- [![CI](https://github.com/telepat-io/ideon/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/telepat-io/ideon/actions/workflows/ci.yml)
8
- [![Coverage](https://codecov.io/gh/telepat-io/ideon/graph/badge.svg)](https://codecov.io/gh/telepat-io/ideon)
9
- [![npm version](https://img.shields.io/npm/v/%40telepat%2Fideon)](https://www.npmjs.com/package/@telepat/ideon)
10
- [![Docs](https://img.shields.io/badge/docs-live-1f6feb)](https://docs.telepat.io/ideon)
11
- [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](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).