@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.
- package/LICENSE +21 -0
- package/README.md +25 -12
- package/dist/ideon.js +268 -22
- 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
|
-
#
|
|
11
|
+
# AI Writer Extraordinaire
|
|
12
|
+
|
|
13
|
+
[](https://github.com/telepat-io/ideon/actions/workflows/ci.yml)
|
|
14
|
+
[](https://codecov.io/gh/telepat-io/ideon)
|
|
15
|
+
[](https://www.npmjs.com/package/@telepat/ideon)
|
|
16
|
+
[](https://www.npmjs.com/package/@telepat/ideon)
|
|
17
|
+
[](https://docs.telepat.io/ideon)
|
|
18
|
+
[](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
|
|
40
|
+
Install globally:
|
|
34
41
|
|
|
35
42
|
```bash
|
|
36
|
-
npm
|
|
43
|
+
npm i -g @telepat/ideon
|
|
37
44
|
```
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
Verify installation:
|
|
40
47
|
|
|
41
48
|
```bash
|
|
42
|
-
|
|
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
|
-
|
|
57
|
+
ideon settings
|
|
51
58
|
```
|
|
52
59
|
|
|
53
60
|
2. Generate your first article:
|
|
54
61
|
|
|
55
62
|
```bash
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
|
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
|
|
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
|
|
3913
|
-
"Start with the exact
|
|
3914
|
-
"Reject results that do not directly match the
|
|
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
|
-
`
|
|
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.
|
|
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
|
|
10
|
+
"homepage": "https://docs.telepat.io/ideon",
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/telepat-io/ideon/issues"
|
|
13
13
|
},
|