@telepat/ideon 0.1.0 → 0.1.5

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -12
  3. package/dist/ideon.js +161 -7
  4. package/package.json +2 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Telepat contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -8,7 +8,14 @@ ooooo oooooooooo. oooooooooooo .oooooo. ooooo ooo
8
8
  o888o o888bood8P' o888ooooood8 `Y8bood8P' o8o `8
9
9
  ```
10
10
 
11
- # Ideon
11
+ # AI Writer Extraordinaire
12
+
13
+ [![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)
14
+ [![Coverage](https://codecov.io/gh/telepat-io/ideon/graph/badge.svg)](https://codecov.io/gh/telepat-io/ideon)
15
+ [![npm version](https://img.shields.io/npm/v/%40telepat%2Fideon)](https://www.npmjs.com/package/@telepat/ideon)
16
+ [![npm downloads](https://img.shields.io/npm/dm/%40telepat%2Fideon)](https://www.npmjs.com/package/@telepat/ideon)
17
+ [![Docs](https://img.shields.io/badge/docs-live-1f6feb)](https://docs.telepat.io/ideon)
18
+ [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/telepat-io/ideon/blob/main/LICENSE)
12
19
 
13
20
  Ideon is a TypeScript CLI that turns an idea into one or more Markdown outputs, with optional generated images for article runs.
14
21
 
@@ -30,16 +37,16 @@ Prerequisites:
30
37
  - Node.js 20+
31
38
  - npm 10+
32
39
 
33
- Install dependencies:
40
+ Install globally:
34
41
 
35
42
  ```bash
36
- npm install
43
+ npm i -g @telepat/ideon
37
44
  ```
38
45
 
39
- Run the CLI in development mode:
46
+ Verify installation:
40
47
 
41
48
  ```bash
42
- npm run dev -- --help
49
+ ideon --help
43
50
  ```
44
51
 
45
52
  ## Getting Started
@@ -47,25 +54,25 @@ npm run dev -- --help
47
54
  1. Configure credentials interactively:
48
55
 
49
56
  ```bash
50
- npm run dev -- settings
57
+ ideon settings
51
58
  ```
52
59
 
53
60
  2. Generate your first article:
54
61
 
55
62
  ```bash
56
- npm run dev -- write "How small editorial teams can productionize AI writing"
63
+ ideon write "How small editorial teams can productionize AI writing"
57
64
  ```
58
65
 
59
66
  3. Generate multi-output runs:
60
67
 
61
68
  ```bash
62
- npm run dev -- write "How small editorial teams can productionize AI writing" --target article=1 --target x-post=2 --style professional
69
+ ideon write "How small editorial teams can productionize AI writing" --target article=1 --target x-post=2 --style professional
63
70
  ```
64
71
 
65
72
  4. Run a safe pipeline dry run (no provider calls):
66
73
 
67
74
  ```bash
68
- npm run dev -- write --dry-run "How AI changes technical publishing"
75
+ ideon write --dry-run "How AI changes technical publishing"
69
76
  ```
70
77
 
71
78
  ## Core Commands
@@ -85,7 +92,7 @@ ideon preview
85
92
  Serve the latest generated article locally with assets and open it in your browser:
86
93
 
87
94
  ```bash
88
- npm run preview
95
+ ideon preview
89
96
  ```
90
97
 
91
98
  This launches the new React preview app (served from `dist/preview`) and the preview API server.
@@ -93,7 +100,7 @@ This launches the new React preview app (served from `dist/preview`) and the pre
93
100
  You can also preview a specific article and choose a port:
94
101
 
95
102
  ```bash
96
- npm run dev -- preview ./output/my-article.md --port 4173
103
+ ideon preview ./output/my-article.md --port 4173
97
104
  ```
98
105
 
99
106
  If you are iterating on preview UI code in `src/preview-app`, rebuild client assets after UI changes:
@@ -136,6 +143,12 @@ npm run pricing:refresh
136
143
  - Start docs locally: `npm run docs:start`
137
144
  - Build docs: `npm run docs:build`
138
145
 
146
+ Links:
147
+
148
+ - GitHub repository: [telepat-io/ideon](https://github.com/telepat-io/ideon)
149
+ - npm package: [@telepat/ideon](https://www.npmjs.com/package/@telepat/ideon)
150
+ - Documentation site: [docs.telepat.io/ideon](https://docs.telepat.io/ideon)
151
+
139
152
  Key docs:
140
153
 
141
154
  - CLI commands: `docs-site/docs/reference/cli-reference.md`
@@ -146,4 +159,4 @@ Key docs:
146
159
 
147
160
  GitHub Pages URL:
148
161
 
149
- - `https://telepat-io.github.io/ideon/`
162
+ - [https://docs.telepat.io/ideon](https://docs.telepat.io/ideon)
package/dist/ideon.js CHANGED
@@ -36,11 +36,15 @@ var baseT2ISettingsSchema = z.object({
36
36
  modelId: z.string().default("black-forest-labs/flux-schnell"),
37
37
  inputOverrides: z.record(z.string(), z.unknown()).default({})
38
38
  });
39
+ var notificationsSettingsSchema = z.object({
40
+ enabled: z.boolean().default(false)
41
+ });
39
42
  var appSettingsSchema = z.object({
40
43
  model: z.string().default("moonshotai/kimi-k2.5"),
41
44
  modelSettings: modelSettingsSchema.default(modelSettingsSchema.parse({})),
42
45
  modelRequestTimeoutMs: z.number().int().positive().default(9e4),
43
46
  t2i: baseT2ISettingsSchema.default(baseT2ISettingsSchema.parse({})),
47
+ notifications: notificationsSettingsSchema.default(notificationsSettingsSchema.parse({})),
44
48
  markdownOutputDir: z.string().default("/output"),
45
49
  assetOutputDir: z.string().default("/output/assets"),
46
50
  contentTargets: z.array(contentTargetSchema).min(1).refine((targets) => targets.filter((target) => target.role === "primary").length === 1, {
@@ -57,6 +61,7 @@ var envSettingsSchema = z.object({
57
61
  maxTokens: z.number().int().positive().optional(),
58
62
  topP: z.number().min(0).max(1).optional(),
59
63
  modelRequestTimeoutMs: z.number().int().positive().optional(),
64
+ notificationsEnabled: z.boolean().optional(),
60
65
  markdownOutputDir: z.string().optional(),
61
66
  assetOutputDir: z.string().optional(),
62
67
  style: z.enum(writingStyleValues).optional(),
@@ -78,6 +83,19 @@ function parseNumber(value2) {
78
83
  const parsed = Number(value2);
79
84
  return Number.isFinite(parsed) ? parsed : void 0;
80
85
  }
86
+ function parseBoolean(value2) {
87
+ if (!value2) {
88
+ return void 0;
89
+ }
90
+ const normalized = value2.trim().toLowerCase();
91
+ if (normalized === "true") {
92
+ return true;
93
+ }
94
+ if (normalized === "false") {
95
+ return false;
96
+ }
97
+ return void 0;
98
+ }
81
99
  function readEnvSettings(env = process.env) {
82
100
  return envSettingsSchema.parse({
83
101
  openRouterApiKey: env.IDEON_OPENROUTER_API_KEY,
@@ -87,6 +105,7 @@ function readEnvSettings(env = process.env) {
87
105
  maxTokens: parseNumber(env.IDEON_MAX_TOKENS),
88
106
  topP: parseNumber(env.IDEON_TOP_P),
89
107
  modelRequestTimeoutMs: parseNumber(env.IDEON_MODEL_REQUEST_TIMEOUT_MS),
108
+ notificationsEnabled: parseBoolean(env.IDEON_NOTIFICATIONS_ENABLED),
90
109
  markdownOutputDir: env.IDEON_MARKDOWN_OUTPUT_DIR,
91
110
  assetOutputDir: env.IDEON_ASSET_OUTPUT_DIR,
92
111
  style: env.IDEON_STYLE,
@@ -896,6 +915,10 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
896
915
  label: `LLM model: ${settings.model}`,
897
916
  value: "llm-model"
898
917
  },
918
+ {
919
+ label: `Notifications > OS notifications enabled: ${settings.notifications.enabled ? "true" : "false"}`,
920
+ value: "notifications-enabled"
921
+ },
899
922
  {
900
923
  label: `Temperature: ${settings.modelSettings.temperature}`,
901
924
  value: "temperature"
@@ -1018,6 +1041,9 @@ function handleMenuSelect(action, settings, secrets, setEditing, setShowModelSel
1018
1041
  case "llm-model":
1019
1042
  setEditing({ key: action, label: "LLM model", value: settings.model });
1020
1043
  return;
1044
+ case "notifications-enabled":
1045
+ setEditing({ key: action, label: "Notifications > OS notifications enabled (true|false)", value: String(settings.notifications.enabled) });
1046
+ return;
1021
1047
  case "temperature":
1022
1048
  setEditing({ key: action, label: "Temperature", value: String(settings.modelSettings.temperature) });
1023
1049
  return;
@@ -1069,6 +1095,17 @@ function applyEdit(action, value2, settings, secrets, setSettings, setSecrets) {
1069
1095
  setSettings({ ...settings, model: value2.trim() || settings.model });
1070
1096
  return;
1071
1097
  }
1098
+ if (action === "notifications-enabled") {
1099
+ const parsed = parseBooleanOrFallback(value2, settings.notifications.enabled);
1100
+ setSettings({
1101
+ ...settings,
1102
+ notifications: {
1103
+ ...settings.notifications,
1104
+ enabled: parsed
1105
+ }
1106
+ });
1107
+ return;
1108
+ }
1072
1109
  if (action === "temperature") {
1073
1110
  const nextTemperature = clampNumber2(parseNumberOrFallback(value2, settings.modelSettings.temperature), 0, 2);
1074
1111
  setSettings({
@@ -1147,6 +1184,16 @@ function parseNumberOrFallback(value2, fallback) {
1147
1184
  function clampNumber2(value2, minimum, maximum) {
1148
1185
  return Math.min(maximum, Math.max(minimum, value2));
1149
1186
  }
1187
+ function parseBooleanOrFallback(value2, fallback) {
1188
+ const normalized = value2.trim().toLowerCase();
1189
+ if (normalized === "true") {
1190
+ return true;
1191
+ }
1192
+ if (normalized === "false") {
1193
+ return false;
1194
+ }
1195
+ return fallback;
1196
+ }
1150
1197
 
1151
1198
  // src/config/secretStore.ts
1152
1199
  import keytar from "keytar";
@@ -3491,6 +3538,13 @@ async function resolveRunInput(input) {
3491
3538
  ...job?.settings ?? {},
3492
3539
  ...envSettings.model ? { model: envSettings.model } : {},
3493
3540
  ...envSettings.modelRequestTimeoutMs !== void 0 ? { modelRequestTimeoutMs: envSettings.modelRequestTimeoutMs } : {},
3541
+ ...envSettings.notificationsEnabled !== void 0 ? {
3542
+ notifications: {
3543
+ ...savedSettings.notifications,
3544
+ ...job?.settings?.notifications ?? {},
3545
+ enabled: envSettings.notificationsEnabled
3546
+ }
3547
+ } : {},
3494
3548
  ...envSettings.temperature !== void 0 || envSettings.maxTokens !== void 0 || envSettings.topP !== void 0 ? {
3495
3549
  modelSettings: {
3496
3550
  ...savedSettings.modelSettings,
@@ -3909,9 +3963,9 @@ function buildUrlResolutionMessages(options) {
3909
3963
  role: "system",
3910
3964
  content: [
3911
3965
  "You are a web research assistant for editorial linking.",
3912
- "Use web search to find the best single URL for the requested expression in context.",
3913
- "Start with the exact expression as the search phrase before trying broader variants.",
3914
- "Reject results that do not directly match the expression and paragraph meaning.",
3966
+ "Use web search to find the best single URL to attach as a link to the provided text in context.",
3967
+ "Start with the exact input text as the search phrase before trying broader variants.",
3968
+ "Reject results that do not directly match the topic and paragraph meaning.",
3915
3969
  "Prefer canonical, trustworthy, stable sources that match the paragraph intent.",
3916
3970
  'Return only one line: the selected URL, or "none" when no strong match exists.',
3917
3971
  "Do not return markdown, explanations, bullets, or extra text."
@@ -3922,15 +3976,12 @@ function buildUrlResolutionMessages(options) {
3922
3976
  content: [
3923
3977
  `Article title: ${options.articleTitle}`,
3924
3978
  `Article description: ${options.articleDescription}`,
3925
- `Exact expression token: "${options.expression}"`,
3926
- `Expression to link: ${options.expression}`,
3979
+ `Text to add link to (input text): "${options.expression}"`,
3927
3980
  "",
3928
3981
  "Paragraph context:",
3929
3982
  options.paragraph,
3930
3983
  "",
3931
3984
  "Search the web and choose the best URL for this inline link in this context.",
3932
- "Use the exact expression first, then only accept close canonical variants when meaning is unchanged.",
3933
- 'If search evidence does not clearly support this expression in this paragraph context, return "none".',
3934
3985
  'Output format: URL only, or "none".'
3935
3986
  ].join("\n")
3936
3987
  }
@@ -7688,6 +7739,76 @@ function withWriteResumeHint(message) {
7688
7739
  return `${trimmed} ${WRITE_RESUME_HINT}`;
7689
7740
  }
7690
7741
 
7742
+ // src/cli/notifications/osNotifier.ts
7743
+ import { spawn as spawn2 } from "child_process";
7744
+ var APP_NAME = "Ideon";
7745
+ var MAX_MESSAGE_LENGTH = 180;
7746
+ async function notifyWriteStarted(params) {
7747
+ if (!params.enabled) {
7748
+ return;
7749
+ }
7750
+ const title = params.runMode === "resume" ? `${APP_NAME}: Resumed article write` : `${APP_NAME}: Started article write`;
7751
+ const message = truncateMessage(params.idea);
7752
+ sendOsNotification(title, message);
7753
+ }
7754
+ async function notifyWriteSucceeded(params) {
7755
+ if (!params.enabled) {
7756
+ return;
7757
+ }
7758
+ const title = `${APP_NAME}: Article ready`;
7759
+ const message = truncateMessage(`${params.title} (${params.slug})`);
7760
+ sendOsNotification(title, message);
7761
+ }
7762
+ async function notifyWriteFailed(params) {
7763
+ if (!params.enabled) {
7764
+ return;
7765
+ }
7766
+ const title = `${APP_NAME}: Article write failed`;
7767
+ const message = truncateMessage(params.message);
7768
+ sendOsNotification(title, message);
7769
+ }
7770
+ async function notifyWriteCanceled(params) {
7771
+ if (!params.enabled) {
7772
+ return;
7773
+ }
7774
+ const title = `${APP_NAME}: Article write canceled`;
7775
+ const message = truncateMessage(`Interrupted by ${params.signal}.`);
7776
+ sendOsNotification(title, message);
7777
+ }
7778
+ function sendOsNotification(title, message) {
7779
+ if (process.platform === "darwin") {
7780
+ const escapedTitle = escapeAppleScript(title);
7781
+ const escapedMessage = escapeAppleScript(message);
7782
+ runCommand("osascript", ["-e", `display notification "${escapedMessage}" with title "${escapedTitle}"`]);
7783
+ return;
7784
+ }
7785
+ if (process.platform === "linux") {
7786
+ runCommand("notify-send", [title, message]);
7787
+ }
7788
+ }
7789
+ function runCommand(command, args) {
7790
+ try {
7791
+ const child = spawn2(command, args, {
7792
+ stdio: "ignore",
7793
+ windowsHide: true
7794
+ });
7795
+ child.on("error", () => {
7796
+ });
7797
+ child.unref();
7798
+ } catch {
7799
+ }
7800
+ }
7801
+ function escapeAppleScript(value2) {
7802
+ return value2.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
7803
+ }
7804
+ function truncateMessage(value2) {
7805
+ const normalized = value2.trim();
7806
+ if (normalized.length <= MAX_MESSAGE_LENGTH) {
7807
+ return normalized;
7808
+ }
7809
+ return `${normalized.slice(0, MAX_MESSAGE_LENGTH - 3)}...`;
7810
+ }
7811
+
7691
7812
  // src/cli/logging/plainRenderer.ts
7692
7813
  function formatDuration2(durationMs) {
7693
7814
  if (durationMs >= 1e3) {
@@ -7735,7 +7856,13 @@ function formatCost2(costUsd) {
7735
7856
  async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
7736
7857
  let previousStatuses = /* @__PURE__ */ new Map();
7737
7858
  let previousItemStatuses = /* @__PURE__ */ new Map();
7859
+ const notificationsEnabled = input.config.settings.notifications.enabled;
7738
7860
  try {
7861
+ await notifyWriteStarted({
7862
+ enabled: notificationsEnabled,
7863
+ idea: input.idea,
7864
+ runMode
7865
+ });
7739
7866
  const result = await runPipelineShell(input, {
7740
7867
  dryRun,
7741
7868
  enrichLinks: enrichLinks2,
@@ -7771,8 +7898,17 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
7771
7898
  console.log(` duration_ms: ${result.analytics.summary.totalDurationMs}`);
7772
7899
  console.log(` retries: ${result.analytics.summary.totalRetries}`);
7773
7900
  console.log(` cost: ${formatCost2(result.analytics.summary.totalCostUsd)}`);
7901
+ await notifyWriteSucceeded({
7902
+ enabled: notificationsEnabled,
7903
+ title: result.artifact.title,
7904
+ slug: result.artifact.slug
7905
+ });
7774
7906
  } catch (error) {
7775
7907
  const message = error instanceof Error ? withWriteResumeHint(error.message) : withWriteResumeHint("Pipeline failed.");
7908
+ await notifyWriteFailed({
7909
+ enabled: notificationsEnabled,
7910
+ message
7911
+ });
7776
7912
  console.error(`Pipeline failed: ${message}`);
7777
7913
  throw new ReportedError(message);
7778
7914
  }
@@ -8115,6 +8251,11 @@ function WriteApp({
8115
8251
  let mounted = true;
8116
8252
  void (async () => {
8117
8253
  try {
8254
+ await notifyWriteStarted({
8255
+ enabled: input.config.settings.notifications.enabled,
8256
+ idea: input.idea,
8257
+ runMode
8258
+ });
8118
8259
  const runResult = await runPipelineShell(input, {
8119
8260
  dryRun,
8120
8261
  enrichLinks: enrichLinks2,
@@ -8129,6 +8270,11 @@ function WriteApp({
8129
8270
  return;
8130
8271
  }
8131
8272
  setResult(runResult);
8273
+ await notifyWriteSucceeded({
8274
+ enabled: input.config.settings.notifications.enabled,
8275
+ title: runResult.artifact.title,
8276
+ slug: runResult.artifact.slug
8277
+ });
8132
8278
  } catch (error) {
8133
8279
  if (!mounted) {
8134
8280
  return;
@@ -8137,6 +8283,10 @@ function WriteApp({
8137
8283
  const messageWithResumeHint = withWriteResumeHint(normalizedError.message);
8138
8284
  setErrorMessage(messageWithResumeHint);
8139
8285
  onError(new Error(messageWithResumeHint));
8286
+ await notifyWriteFailed({
8287
+ enabled: input.config.settings.notifications.enabled,
8288
+ message: messageWithResumeHint
8289
+ });
8140
8290
  }
8141
8291
  })();
8142
8292
  return () => {
@@ -8191,6 +8341,10 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode) {
8191
8341
  interruptHandled = true;
8192
8342
  void (async () => {
8193
8343
  try {
8344
+ await notifyWriteCanceled({
8345
+ enabled: input.config.settings.notifications.enabled,
8346
+ signal
8347
+ });
8194
8348
  await recordInterruptedWrite(signal);
8195
8349
  } finally {
8196
8350
  cleanupSignalHandlers();
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@telepat/ideon",
3
- "version": "0.1.0",
3
+ "version": "0.1.5",
4
4
  "description": "CLI for generating rich articles and images from ideas.",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/telepat-io/ideon.git"
9
9
  },
10
- "homepage": "https://telepat-io.github.io/ideon/",
10
+ "homepage": "https://docs.telepat.io/ideon",
11
11
  "bugs": {
12
12
  "url": "https://github.com/telepat-io/ideon/issues"
13
13
  },