@telepat/ideon 0.1.0 → 0.1.6

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 +268 -22
  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, {
@@ -52,11 +56,13 @@ var appSettingsSchema = z.object({
52
56
  var envSettingsSchema = z.object({
53
57
  openRouterApiKey: z.string().optional(),
54
58
  replicateApiToken: z.string().optional(),
59
+ disableKeytar: z.boolean().optional(),
55
60
  model: z.string().optional(),
56
61
  temperature: z.number().min(0).max(2).optional(),
57
62
  maxTokens: z.number().int().positive().optional(),
58
63
  topP: z.number().min(0).max(1).optional(),
59
64
  modelRequestTimeoutMs: z.number().int().positive().optional(),
65
+ notificationsEnabled: z.boolean().optional(),
60
66
  markdownOutputDir: z.string().optional(),
61
67
  assetOutputDir: z.string().optional(),
62
68
  style: z.enum(writingStyleValues).optional(),
@@ -78,15 +84,30 @@ function parseNumber(value2) {
78
84
  const parsed = Number(value2);
79
85
  return Number.isFinite(parsed) ? parsed : void 0;
80
86
  }
87
+ function parseBoolean(value2) {
88
+ if (!value2) {
89
+ return void 0;
90
+ }
91
+ const normalized = value2.trim().toLowerCase();
92
+ if (normalized === "true") {
93
+ return true;
94
+ }
95
+ if (normalized === "false") {
96
+ return false;
97
+ }
98
+ return void 0;
99
+ }
81
100
  function readEnvSettings(env = process.env) {
82
101
  return envSettingsSchema.parse({
83
102
  openRouterApiKey: env.IDEON_OPENROUTER_API_KEY,
84
103
  replicateApiToken: env.IDEON_REPLICATE_API_TOKEN,
104
+ disableKeytar: parseBoolean(env.IDEON_DISABLE_KEYTAR),
85
105
  model: env.IDEON_MODEL,
86
106
  temperature: parseNumber(env.IDEON_TEMPERATURE),
87
107
  maxTokens: parseNumber(env.IDEON_MAX_TOKENS),
88
108
  topP: parseNumber(env.IDEON_TOP_P),
89
109
  modelRequestTimeoutMs: parseNumber(env.IDEON_MODEL_REQUEST_TIMEOUT_MS),
110
+ notificationsEnabled: parseBoolean(env.IDEON_NOTIFICATIONS_ENABLED),
90
111
  markdownOutputDir: env.IDEON_MARKDOWN_OUTPUT_DIR,
91
112
  assetOutputDir: env.IDEON_ASSET_OUTPUT_DIR,
92
113
  style: env.IDEON_STYLE,
@@ -896,6 +917,10 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
896
917
  label: `LLM model: ${settings.model}`,
897
918
  value: "llm-model"
898
919
  },
920
+ {
921
+ label: `Notifications > OS notifications enabled: ${settings.notifications.enabled ? "true" : "false"}`,
922
+ value: "notifications-enabled"
923
+ },
899
924
  {
900
925
  label: `Temperature: ${settings.modelSettings.temperature}`,
901
926
  value: "temperature"
@@ -1018,6 +1043,9 @@ function handleMenuSelect(action, settings, secrets, setEditing, setShowModelSel
1018
1043
  case "llm-model":
1019
1044
  setEditing({ key: action, label: "LLM model", value: settings.model });
1020
1045
  return;
1046
+ case "notifications-enabled":
1047
+ setEditing({ key: action, label: "Notifications > OS notifications enabled (true|false)", value: String(settings.notifications.enabled) });
1048
+ return;
1021
1049
  case "temperature":
1022
1050
  setEditing({ key: action, label: "Temperature", value: String(settings.modelSettings.temperature) });
1023
1051
  return;
@@ -1069,6 +1097,17 @@ function applyEdit(action, value2, settings, secrets, setSettings, setSecrets) {
1069
1097
  setSettings({ ...settings, model: value2.trim() || settings.model });
1070
1098
  return;
1071
1099
  }
1100
+ if (action === "notifications-enabled") {
1101
+ const parsed = parseBooleanOrFallback(value2, settings.notifications.enabled);
1102
+ setSettings({
1103
+ ...settings,
1104
+ notifications: {
1105
+ ...settings.notifications,
1106
+ enabled: parsed
1107
+ }
1108
+ });
1109
+ return;
1110
+ }
1072
1111
  if (action === "temperature") {
1073
1112
  const nextTemperature = clampNumber2(parseNumberOrFallback(value2, settings.modelSettings.temperature), 0, 2);
1074
1113
  setSettings({
@@ -1147,23 +1186,96 @@ function parseNumberOrFallback(value2, fallback) {
1147
1186
  function clampNumber2(value2, minimum, maximum) {
1148
1187
  return Math.min(maximum, Math.max(minimum, value2));
1149
1188
  }
1189
+ function parseBooleanOrFallback(value2, fallback) {
1190
+ const normalized = value2.trim().toLowerCase();
1191
+ if (normalized === "true") {
1192
+ return true;
1193
+ }
1194
+ if (normalized === "false") {
1195
+ return false;
1196
+ }
1197
+ return fallback;
1198
+ }
1150
1199
 
1151
1200
  // src/config/secretStore.ts
1152
1201
  import keytar from "keytar";
1153
1202
  var SERVICE_NAME = "ideon";
1154
1203
  var OPENROUTER_ACCOUNT = "openrouter-api-key";
1155
1204
  var REPLICATE_ACCOUNT = "replicate-api-token";
1156
- async function loadSecrets() {
1157
- const [openRouterApiKey, replicateApiToken] = await Promise.all([
1158
- keytar.getPassword(SERVICE_NAME, OPENROUTER_ACCOUNT),
1159
- keytar.getPassword(SERVICE_NAME, REPLICATE_ACCOUNT)
1160
- ]);
1205
+ var KEYTAR_UNAVAILABLE_ERROR_NAME = "KeytarUnavailableError";
1206
+ var hasWarnedAboutUnavailableKeytar = false;
1207
+ var KeytarUnavailableError = class extends Error {
1208
+ constructor(message) {
1209
+ super(message);
1210
+ this.name = KEYTAR_UNAVAILABLE_ERROR_NAME;
1211
+ }
1212
+ };
1213
+ function nullSecrets() {
1161
1214
  return {
1162
- openRouterApiKey,
1163
- replicateApiToken
1215
+ openRouterApiKey: null,
1216
+ replicateApiToken: null
1164
1217
  };
1165
1218
  }
1166
- async function saveSecrets(secrets) {
1219
+ function shouldDisableKeytar(options) {
1220
+ return options.disableKeytar === true;
1221
+ }
1222
+ function isKeytarAvailabilityError(error) {
1223
+ if (!(error instanceof Error)) {
1224
+ return false;
1225
+ }
1226
+ const lowered = error.message.toLowerCase();
1227
+ return [
1228
+ "dbus",
1229
+ "d-bus",
1230
+ "org.freedesktop.secrets",
1231
+ "secret service",
1232
+ "secret-service",
1233
+ "keychain",
1234
+ "keyring",
1235
+ "credential store",
1236
+ "credentials were unavailable",
1237
+ "cannot autolaunch",
1238
+ "no such interface",
1239
+ "not supported in this environment"
1240
+ ].some((fragment) => lowered.includes(fragment));
1241
+ }
1242
+ function warnKeytarUnavailable(details) {
1243
+ if (hasWarnedAboutUnavailableKeytar) {
1244
+ return;
1245
+ }
1246
+ hasWarnedAboutUnavailableKeytar = true;
1247
+ console.warn(
1248
+ `System keychain unavailable (${details}). Falling back to environment variables for secrets. Set IDEON_DISABLE_KEYTAR=true to skip keychain access in this environment.`
1249
+ );
1250
+ }
1251
+ async function loadSecrets(options = {}) {
1252
+ if (shouldDisableKeytar(options)) {
1253
+ return nullSecrets();
1254
+ }
1255
+ try {
1256
+ const [openRouterApiKey, replicateApiToken] = await Promise.all([
1257
+ keytar.getPassword(SERVICE_NAME, OPENROUTER_ACCOUNT),
1258
+ keytar.getPassword(SERVICE_NAME, REPLICATE_ACCOUNT)
1259
+ ]);
1260
+ return {
1261
+ openRouterApiKey,
1262
+ replicateApiToken
1263
+ };
1264
+ } catch (error) {
1265
+ if (isKeytarAvailabilityError(error)) {
1266
+ const message = error instanceof Error ? error.message : "unknown error";
1267
+ warnKeytarUnavailable(message);
1268
+ return nullSecrets();
1269
+ }
1270
+ throw error;
1271
+ }
1272
+ }
1273
+ async function saveSecrets(secrets, options = {}) {
1274
+ if (shouldDisableKeytar(options)) {
1275
+ throw new KeytarUnavailableError(
1276
+ "System keychain access is disabled by IDEON_DISABLE_KEYTAR=true. Use IDEON_OPENROUTER_API_KEY and IDEON_REPLICATE_API_TOKEN instead."
1277
+ );
1278
+ }
1167
1279
  const tasks = [];
1168
1280
  if (secrets.openRouterApiKey !== void 0) {
1169
1281
  tasks.push(saveSecretValue(OPENROUTER_ACCOUNT, secrets.openRouterApiKey));
@@ -1174,17 +1286,31 @@ async function saveSecrets(secrets) {
1174
1286
  await Promise.all(tasks);
1175
1287
  }
1176
1288
  async function saveSecretValue(account, value2) {
1177
- if (!value2) {
1178
- await keytar.deletePassword(SERVICE_NAME, account);
1179
- return;
1289
+ try {
1290
+ if (!value2) {
1291
+ await keytar.deletePassword(SERVICE_NAME, account);
1292
+ return;
1293
+ }
1294
+ await keytar.setPassword(SERVICE_NAME, account, value2);
1295
+ } catch (error) {
1296
+ if (isKeytarAvailabilityError(error)) {
1297
+ const message = error instanceof Error ? error.message : "unknown error";
1298
+ throw new KeytarUnavailableError(
1299
+ `System keychain unavailable while saving credentials (${message}). Use IDEON_OPENROUTER_API_KEY and IDEON_REPLICATE_API_TOKEN instead.`
1300
+ );
1301
+ }
1302
+ throw error;
1180
1303
  }
1181
- await keytar.setPassword(SERVICE_NAME, account, value2);
1182
1304
  }
1183
1305
 
1184
1306
  // src/cli/commands/settings.tsx
1185
1307
  import { jsx as jsx2 } from "react/jsx-runtime";
1186
1308
  async function openSettings() {
1187
- const [settings, secrets] = await Promise.all([loadSavedSettings(), loadSecrets()]);
1309
+ const envSettings = readEnvSettings();
1310
+ const [settings, secrets] = await Promise.all([
1311
+ loadSavedSettings(),
1312
+ loadSecrets({ disableKeytar: envSettings.disableKeytar })
1313
+ ]);
1188
1314
  let result = null;
1189
1315
  const app = render(
1190
1316
  /* @__PURE__ */ jsx2(
@@ -1205,7 +1331,17 @@ async function openSettings() {
1205
1331
  return;
1206
1332
  }
1207
1333
  const savedResult = finalResult;
1208
- await Promise.all([saveSettings(savedResult.settings), saveSecrets(savedResult.secrets)]);
1334
+ await saveSettings(savedResult.settings);
1335
+ try {
1336
+ await saveSecrets(savedResult.secrets, { disableKeytar: envSettings.disableKeytar });
1337
+ } catch (error) {
1338
+ if (error instanceof KeytarUnavailableError) {
1339
+ console.log("Settings saved, but secrets were not stored in the system keychain.");
1340
+ console.log("Use IDEON_OPENROUTER_API_KEY and IDEON_REPLICATE_API_TOKEN in this environment.");
1341
+ return;
1342
+ }
1343
+ throw error;
1344
+ }
1209
1345
  console.log(`Settings saved to ${getSettingsFilePath()}.`);
1210
1346
  }
1211
1347
 
@@ -3479,8 +3615,11 @@ import { createInterface } from "readline/promises";
3479
3615
  // src/config/resolver.ts
3480
3616
  import { readFile as readFile4 } from "fs/promises";
3481
3617
  async function resolveRunInput(input) {
3482
- const [savedSettings, secrets] = await Promise.all([loadSavedSettings(), loadSecrets()]);
3483
3618
  const envSettings = readEnvSettings();
3619
+ const [savedSettings, secrets] = await Promise.all([
3620
+ loadSavedSettings(),
3621
+ loadSecrets({ disableKeytar: envSettings.disableKeytar })
3622
+ ]);
3484
3623
  const job = input.jobPath ? await loadJobInput(input.jobPath) : null;
3485
3624
  assertNoLegacyXMode(savedSettings.contentTargets, "saved settings contentTargets");
3486
3625
  assertNoLegacyXMode(job?.settings?.contentTargets, "job settings contentTargets");
@@ -3491,6 +3630,13 @@ async function resolveRunInput(input) {
3491
3630
  ...job?.settings ?? {},
3492
3631
  ...envSettings.model ? { model: envSettings.model } : {},
3493
3632
  ...envSettings.modelRequestTimeoutMs !== void 0 ? { modelRequestTimeoutMs: envSettings.modelRequestTimeoutMs } : {},
3633
+ ...envSettings.notificationsEnabled !== void 0 ? {
3634
+ notifications: {
3635
+ ...savedSettings.notifications,
3636
+ ...job?.settings?.notifications ?? {},
3637
+ enabled: envSettings.notificationsEnabled
3638
+ }
3639
+ } : {},
3494
3640
  ...envSettings.temperature !== void 0 || envSettings.maxTokens !== void 0 || envSettings.topP !== void 0 ? {
3495
3641
  modelSettings: {
3496
3642
  ...savedSettings.modelSettings,
@@ -3909,9 +4055,9 @@ function buildUrlResolutionMessages(options) {
3909
4055
  role: "system",
3910
4056
  content: [
3911
4057
  "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.",
4058
+ "Use web search to find the best single URL to attach as a link to the provided text in context.",
4059
+ "Start with the exact input text as the search phrase before trying broader variants.",
4060
+ "Reject results that do not directly match the topic and paragraph meaning.",
3915
4061
  "Prefer canonical, trustworthy, stable sources that match the paragraph intent.",
3916
4062
  'Return only one line: the selected URL, or "none" when no strong match exists.',
3917
4063
  "Do not return markdown, explanations, bullets, or extra text."
@@ -3922,15 +4068,12 @@ function buildUrlResolutionMessages(options) {
3922
4068
  content: [
3923
4069
  `Article title: ${options.articleTitle}`,
3924
4070
  `Article description: ${options.articleDescription}`,
3925
- `Exact expression token: "${options.expression}"`,
3926
- `Expression to link: ${options.expression}`,
4071
+ `Text to add link to (input text): "${options.expression}"`,
3927
4072
  "",
3928
4073
  "Paragraph context:",
3929
4074
  options.paragraph,
3930
4075
  "",
3931
4076
  "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
4077
  'Output format: URL only, or "none".'
3935
4078
  ].join("\n")
3936
4079
  }
@@ -7688,6 +7831,76 @@ function withWriteResumeHint(message) {
7688
7831
  return `${trimmed} ${WRITE_RESUME_HINT}`;
7689
7832
  }
7690
7833
 
7834
+ // src/cli/notifications/osNotifier.ts
7835
+ import { spawn as spawn2 } from "child_process";
7836
+ var APP_NAME = "Ideon";
7837
+ var MAX_MESSAGE_LENGTH = 180;
7838
+ async function notifyWriteStarted(params) {
7839
+ if (!params.enabled) {
7840
+ return;
7841
+ }
7842
+ const title = params.runMode === "resume" ? `${APP_NAME}: Resumed article write` : `${APP_NAME}: Started article write`;
7843
+ const message = truncateMessage(params.idea);
7844
+ sendOsNotification(title, message);
7845
+ }
7846
+ async function notifyWriteSucceeded(params) {
7847
+ if (!params.enabled) {
7848
+ return;
7849
+ }
7850
+ const title = `${APP_NAME}: Article ready`;
7851
+ const message = truncateMessage(`${params.title} (${params.slug})`);
7852
+ sendOsNotification(title, message);
7853
+ }
7854
+ async function notifyWriteFailed(params) {
7855
+ if (!params.enabled) {
7856
+ return;
7857
+ }
7858
+ const title = `${APP_NAME}: Article write failed`;
7859
+ const message = truncateMessage(params.message);
7860
+ sendOsNotification(title, message);
7861
+ }
7862
+ async function notifyWriteCanceled(params) {
7863
+ if (!params.enabled) {
7864
+ return;
7865
+ }
7866
+ const title = `${APP_NAME}: Article write canceled`;
7867
+ const message = truncateMessage(`Interrupted by ${params.signal}.`);
7868
+ sendOsNotification(title, message);
7869
+ }
7870
+ function sendOsNotification(title, message) {
7871
+ if (process.platform === "darwin") {
7872
+ const escapedTitle = escapeAppleScript(title);
7873
+ const escapedMessage = escapeAppleScript(message);
7874
+ runCommand("osascript", ["-e", `display notification "${escapedMessage}" with title "${escapedTitle}"`]);
7875
+ return;
7876
+ }
7877
+ if (process.platform === "linux") {
7878
+ runCommand("notify-send", [title, message]);
7879
+ }
7880
+ }
7881
+ function runCommand(command, args) {
7882
+ try {
7883
+ const child = spawn2(command, args, {
7884
+ stdio: "ignore",
7885
+ windowsHide: true
7886
+ });
7887
+ child.on("error", () => {
7888
+ });
7889
+ child.unref();
7890
+ } catch {
7891
+ }
7892
+ }
7893
+ function escapeAppleScript(value2) {
7894
+ return value2.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
7895
+ }
7896
+ function truncateMessage(value2) {
7897
+ const normalized = value2.trim();
7898
+ if (normalized.length <= MAX_MESSAGE_LENGTH) {
7899
+ return normalized;
7900
+ }
7901
+ return `${normalized.slice(0, MAX_MESSAGE_LENGTH - 3)}...`;
7902
+ }
7903
+
7691
7904
  // src/cli/logging/plainRenderer.ts
7692
7905
  function formatDuration2(durationMs) {
7693
7906
  if (durationMs >= 1e3) {
@@ -7735,7 +7948,13 @@ function formatCost2(costUsd) {
7735
7948
  async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
7736
7949
  let previousStatuses = /* @__PURE__ */ new Map();
7737
7950
  let previousItemStatuses = /* @__PURE__ */ new Map();
7951
+ const notificationsEnabled = input.config.settings.notifications.enabled;
7738
7952
  try {
7953
+ await notifyWriteStarted({
7954
+ enabled: notificationsEnabled,
7955
+ idea: input.idea,
7956
+ runMode
7957
+ });
7739
7958
  const result = await runPipelineShell(input, {
7740
7959
  dryRun,
7741
7960
  enrichLinks: enrichLinks2,
@@ -7771,8 +7990,17 @@ async function renderPlainPipeline(input, dryRun, enrichLinks2, runMode) {
7771
7990
  console.log(` duration_ms: ${result.analytics.summary.totalDurationMs}`);
7772
7991
  console.log(` retries: ${result.analytics.summary.totalRetries}`);
7773
7992
  console.log(` cost: ${formatCost2(result.analytics.summary.totalCostUsd)}`);
7993
+ await notifyWriteSucceeded({
7994
+ enabled: notificationsEnabled,
7995
+ title: result.artifact.title,
7996
+ slug: result.artifact.slug
7997
+ });
7774
7998
  } catch (error) {
7775
7999
  const message = error instanceof Error ? withWriteResumeHint(error.message) : withWriteResumeHint("Pipeline failed.");
8000
+ await notifyWriteFailed({
8001
+ enabled: notificationsEnabled,
8002
+ message
8003
+ });
7776
8004
  console.error(`Pipeline failed: ${message}`);
7777
8005
  throw new ReportedError(message);
7778
8006
  }
@@ -8115,6 +8343,11 @@ function WriteApp({
8115
8343
  let mounted = true;
8116
8344
  void (async () => {
8117
8345
  try {
8346
+ await notifyWriteStarted({
8347
+ enabled: input.config.settings.notifications.enabled,
8348
+ idea: input.idea,
8349
+ runMode
8350
+ });
8118
8351
  const runResult = await runPipelineShell(input, {
8119
8352
  dryRun,
8120
8353
  enrichLinks: enrichLinks2,
@@ -8129,6 +8362,11 @@ function WriteApp({
8129
8362
  return;
8130
8363
  }
8131
8364
  setResult(runResult);
8365
+ await notifyWriteSucceeded({
8366
+ enabled: input.config.settings.notifications.enabled,
8367
+ title: runResult.artifact.title,
8368
+ slug: runResult.artifact.slug
8369
+ });
8132
8370
  } catch (error) {
8133
8371
  if (!mounted) {
8134
8372
  return;
@@ -8137,6 +8375,10 @@ function WriteApp({
8137
8375
  const messageWithResumeHint = withWriteResumeHint(normalizedError.message);
8138
8376
  setErrorMessage(messageWithResumeHint);
8139
8377
  onError(new Error(messageWithResumeHint));
8378
+ await notifyWriteFailed({
8379
+ enabled: input.config.settings.notifications.enabled,
8380
+ message: messageWithResumeHint
8381
+ });
8140
8382
  }
8141
8383
  })();
8142
8384
  return () => {
@@ -8191,6 +8433,10 @@ async function runWritePipeline(input, dryRun, enrichLinks2, runMode) {
8191
8433
  interruptHandled = true;
8192
8434
  void (async () => {
8193
8435
  try {
8436
+ await notifyWriteCanceled({
8437
+ enabled: input.config.settings.notifications.enabled,
8438
+ signal
8439
+ });
8194
8440
  await recordInterruptedWrite(signal);
8195
8441
  } finally {
8196
8442
  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.6",
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
  },