tonedial 0.1.0 → 0.1.2
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/dist/cli.js +5 -2
- package/dist/commands/compile.js +3 -12
- package/dist/commands/create.js +7 -7
- package/dist/commands/doctor.js +23 -16
- package/dist/commands/edit.js +10 -11
- package/dist/commands/init.js +8 -14
- package/dist/commands/save.js +5 -13
- package/dist/commands/set.js +85 -62
- package/dist/commands/show.js +20 -30
- package/dist/commands/transfer.js +0 -1
- package/dist/commands/update.js +89 -42
- package/dist/commands/use.js +6 -8
- package/dist/commands/wire.js +1 -1
- package/dist/core/anchors.js +0 -3
- package/dist/core/combos.js +6 -38
- package/dist/core/compile.js +1 -5
- package/dist/core/config.js +0 -1
- package/dist/core/i18n.js +70 -72
- package/dist/core/locales/es.js +35 -36
- package/dist/core/locales/ja.js +35 -36
- package/dist/core/locales/zh.js +35 -36
- package/dist/core/seeds.js +3 -3
- package/dist/core/selfupdate.js +47 -2
- package/dist/core/skill.js +9 -6
- package/dist/ui/format.js +5 -8
- package/package.json +1 -1
package/dist/commands/set.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
// `set`, `reset`, and `
|
|
1
|
+
// `set`, `reset`, `address` and `persona`. Change state and recompile in one action, so they cannot
|
|
2
2
|
// drift apart.
|
|
3
3
|
//
|
|
4
4
|
// Warnings never block. A user is entitled to a combination that reads badly, and the tool's
|
|
5
5
|
// job is to say so. Every selected level remains unchanged, including contradictions.
|
|
6
6
|
import { AXIS_DEFAULT, AXIS_ORDER, riskyAxes } from "../core/anchors.js";
|
|
7
7
|
import { check as checkCombos, PERSONA_DESCRIPTION_MAX } from "../core/combos.js";
|
|
8
|
-
import { AGE_BANDS, GENDERS,
|
|
8
|
+
import { AGE_BANDS, GENDERS, SPEECH_LEVELS, ConfigError, SETTING_KEYS, isAxis, nearest, nearestAxis, serializeConfig, } from "../core/config.js";
|
|
9
9
|
import { toCompileInput } from "../core/resolve.js";
|
|
10
10
|
import { LANGUAGES, catalogue, isLanguage } from "../core/i18n.js";
|
|
11
11
|
import { presetSettings, resolvePreset } from "../core/presets.js";
|
|
12
12
|
import { dial } from "../ui/dial.js";
|
|
13
|
-
import {
|
|
14
|
-
import { interactive } from "../ui/prompt.js";
|
|
13
|
+
import { artifactSize, combination, humanText, riskWarnings } from "../ui/format.js";
|
|
14
|
+
import { guardTerminal, interactive } from "../ui/prompt.js";
|
|
15
15
|
import { createInterface } from "node:readline/promises";
|
|
16
16
|
import { cells, padEnd, truncate, wrap } from "../ui/width.js";
|
|
17
|
-
import { applyPlanned, planArtifact, planConfig
|
|
17
|
+
import { applyPlanned, planArtifact, planConfig } from "./compile.js";
|
|
18
18
|
/** A raw number is bucketed on write; the stored and compiled value is always the band. */
|
|
19
19
|
function bucketAge(value, t, asJson) {
|
|
20
20
|
if (AGE_BANDS.includes(value))
|
|
@@ -31,36 +31,15 @@ function bucketAge(value, t, asJson) {
|
|
|
31
31
|
}
|
|
32
32
|
/** The settable persona keys, in the order `set --help` and the error message list them. */
|
|
33
33
|
const PERSONA_FIELDS = ["age", "gender", "self_ref", "user_ref", "note", "user", "agent"];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
harsh: "critique", critic: "critique", blunt: "critique",
|
|
40
|
-
praise: "flattery", compliment: "flattery", flatter: "flattery",
|
|
41
|
-
warm: "warmth", kind: "warmth", empath: "warmth",
|
|
42
|
-
opinion: "conviction", decisive: "conviction",
|
|
43
|
-
thorough: "care", proactive: "care",
|
|
44
|
-
obedient: "compliance", grumble: "compliance",
|
|
45
|
-
refuse: "refusal", boast: "bravado", brag: "bravado",
|
|
46
|
-
};
|
|
47
|
-
/** The note is a content channel, so `set` says what an axis would own better. It never edits. */
|
|
48
|
-
function noteWarnings(note, t) {
|
|
49
|
-
const out = [];
|
|
50
|
-
if ([...note].length > NOTE_MAX) {
|
|
51
|
-
out.push(t.persona.noteOverCap([...note].length, NOTE_MAX));
|
|
52
|
-
}
|
|
53
|
-
const lower = note.toLowerCase();
|
|
54
|
-
const named = [...new Set(Object.entries(OBSERVABLE_WORDS)
|
|
55
|
-
.filter(([word]) => lower.includes(word))
|
|
56
|
-
.map(([, axis]) => axis))];
|
|
57
|
-
if (named.length) {
|
|
58
|
-
out.push(t.persona.noteNamesAxis(named));
|
|
34
|
+
/** Report limitations in the final settings, never superseded interview answers. */
|
|
35
|
+
export function settingsWarnings(config, t, asJson) {
|
|
36
|
+
const warnings = [];
|
|
37
|
+
if (config.safeword && [...config.safeword].length < 4) {
|
|
38
|
+
warnings.push(t.settings.safewordShort(asJson ? config.safeword : humanText(config.safeword)));
|
|
59
39
|
}
|
|
60
|
-
return
|
|
40
|
+
return warnings;
|
|
61
41
|
}
|
|
62
42
|
function assign(config, key, value, t, asJson) {
|
|
63
|
-
const warnings = [];
|
|
64
43
|
if (key === "guards") {
|
|
65
44
|
throw new ConfigError(t.settings.guardsRetired);
|
|
66
45
|
}
|
|
@@ -70,23 +49,20 @@ function assign(config, key, value, t, asJson) {
|
|
|
70
49
|
if (key === "safeword") {
|
|
71
50
|
const phrase = value.trim();
|
|
72
51
|
config.safeword = phrase === "" ? null : phrase;
|
|
73
|
-
|
|
74
|
-
warnings.push(t.settings.safewordShort(asJson ? config.safeword : humanText(config.safeword)));
|
|
75
|
-
}
|
|
76
|
-
return warnings;
|
|
52
|
+
return;
|
|
77
53
|
}
|
|
78
54
|
if (key === "speech_level") {
|
|
79
55
|
if (!SPEECH_LEVELS.includes(value)) {
|
|
80
56
|
throw new ConfigError(t.settings.speechLevel(SPEECH_LEVELS));
|
|
81
57
|
}
|
|
82
58
|
config.speech_level = value;
|
|
83
|
-
return
|
|
59
|
+
return;
|
|
84
60
|
}
|
|
85
61
|
if (key === "language") {
|
|
86
62
|
if (!isLanguage(value))
|
|
87
63
|
throw new ConfigError(t.errors.languageUnsupported(asJson ? value : humanText(value), LANGUAGES));
|
|
88
64
|
config.language = value;
|
|
89
|
-
return
|
|
65
|
+
return;
|
|
90
66
|
}
|
|
91
67
|
if (key.startsWith("persona.")) {
|
|
92
68
|
const field = key.slice("persona.".length);
|
|
@@ -109,8 +85,6 @@ function assign(config, key, value, t, asJson) {
|
|
|
109
85
|
break;
|
|
110
86
|
case "note":
|
|
111
87
|
config.persona.note = empty ? null : value;
|
|
112
|
-
if (!empty)
|
|
113
|
-
warnings.push(...noteWarnings(value, t));
|
|
114
88
|
break;
|
|
115
89
|
case "user":
|
|
116
90
|
case "agent": {
|
|
@@ -132,7 +106,7 @@ function assign(config, key, value, t, asJson) {
|
|
|
132
106
|
// first. An axis error already states its range; this one owed the same.
|
|
133
107
|
throw new ConfigError(t.persona.unknownField(asJson ? field : humanText(field), PERSONA_FIELDS));
|
|
134
108
|
}
|
|
135
|
-
return
|
|
109
|
+
return;
|
|
136
110
|
}
|
|
137
111
|
if (key.startsWith("subagent.")) {
|
|
138
112
|
throw new ConfigError(t.axis.subagentUnsupported);
|
|
@@ -154,10 +128,9 @@ function assign(config, key, value, t, asJson) {
|
|
|
154
128
|
config.axes[key] = level;
|
|
155
129
|
config.unsaved = true;
|
|
156
130
|
}
|
|
157
|
-
return warnings;
|
|
158
131
|
}
|
|
159
132
|
function comboWarnings(config, t) {
|
|
160
|
-
return checkCombos(config.axes
|
|
133
|
+
return checkCombos(config.axes).map((w) => combination(w, t));
|
|
161
134
|
}
|
|
162
135
|
const UNSET = "—";
|
|
163
136
|
/**
|
|
@@ -221,16 +194,12 @@ function readKey(config, key, t) {
|
|
|
221
194
|
*/
|
|
222
195
|
function applyPairs(config, pairs, t, asJson) {
|
|
223
196
|
const settingsBefore = JSON.stringify(presetSettings(config));
|
|
224
|
-
const warnings = [];
|
|
225
197
|
const changed = [];
|
|
226
198
|
const moves = [];
|
|
227
|
-
let personaTouched = false;
|
|
228
199
|
for (const [key, value] of pairs) {
|
|
229
200
|
const before = readKey(config, key, t);
|
|
230
|
-
|
|
201
|
+
assign(config, key, value, t, asJson);
|
|
231
202
|
changed.push(key);
|
|
232
|
-
if (key.startsWith("persona.") && value.trim() !== "")
|
|
233
|
-
personaTouched = true;
|
|
234
203
|
const after = readKey(config, key, t);
|
|
235
204
|
// Only a real move. `set formality=3` on an axis already at 3 was reporting `3 → 3`, which
|
|
236
205
|
// is the misleading `from` this line exists to prevent.
|
|
@@ -240,9 +209,7 @@ function applyPairs(config, pairs, t, asJson) {
|
|
|
240
209
|
}
|
|
241
210
|
if (settingsBefore !== JSON.stringify(presetSettings(config)))
|
|
242
211
|
config.unsaved = true;
|
|
243
|
-
|
|
244
|
-
warnings.push(t.notice.personaUnanchored);
|
|
245
|
-
return { warnings, changed, moves };
|
|
212
|
+
return { warnings: settingsWarnings(config, t, asJson), changed, moves };
|
|
246
213
|
}
|
|
247
214
|
/** The same assignments and validation for direct tuning and preset creation. */
|
|
248
215
|
export function applyAssignments(config, args, t, asJson) {
|
|
@@ -264,7 +231,7 @@ async function commit(ctx, configChange, config, applied) {
|
|
|
264
231
|
` ${ctx.ui.bold(finalConfig.before === finalConfig.after ? ctx.t.axis.settingsUnchanged : ctx.ui.success(ctx.t.axis.settingsSaved))}`,
|
|
265
232
|
...movementLines(applied.moves, ctx, config.axes),
|
|
266
233
|
` ${ctx.ui.success(toneChange.before === toneChange.after ? ctx.t.artifact.current : ctx.t.artifact.generated)}`,
|
|
267
|
-
...(ctx.flags.verbose ? [` ${ctx.ui.dim(
|
|
234
|
+
...(ctx.flags.verbose ? [` ${ctx.ui.dim(artifactSize(size, ctx.t))}`] : []),
|
|
268
235
|
],
|
|
269
236
|
json: {
|
|
270
237
|
changed: applied.changed,
|
|
@@ -272,22 +239,18 @@ async function commit(ctx, configChange, config, applied) {
|
|
|
272
239
|
config: toCompileInput(config),
|
|
273
240
|
chars: size.chars,
|
|
274
241
|
},
|
|
275
|
-
warnings: [...applied.warnings, ...riskWarnings(config.axes, ctx.t), ...comboWarnings(config, ctx.t)
|
|
242
|
+
warnings: [...applied.warnings, ...riskWarnings(config.axes, ctx.t), ...comboWarnings(config, ctx.t)],
|
|
276
243
|
};
|
|
277
244
|
}
|
|
278
245
|
export async function runSet(ctx, args) {
|
|
279
|
-
let result;
|
|
280
246
|
if (!args.length) {
|
|
281
247
|
if (!ctx.canPrompt || !interactive()) {
|
|
282
248
|
throw new ConfigError(ctx.t.axis.needsArgument);
|
|
283
249
|
}
|
|
284
|
-
|
|
250
|
+
return runDial(ctx);
|
|
285
251
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
result = await commit(ctx, configChange, config, applyAssignments(config, args, ctx.t, ctx.asJson));
|
|
289
|
-
}
|
|
290
|
-
return { ...result, warnings: [...(result.warnings ?? []), ctx.t.notice.modelExpectation] };
|
|
252
|
+
const { change: configChange, config } = planConfig(ctx);
|
|
253
|
+
return commit(ctx, configChange, config, applyAssignments(config, args, ctx.t, ctx.asJson));
|
|
291
254
|
}
|
|
292
255
|
/**
|
|
293
256
|
* `tonedial address` — the two terms, and nothing else.
|
|
@@ -313,6 +276,66 @@ export async function runAddress(ctx, args) {
|
|
|
313
276
|
];
|
|
314
277
|
return commit(ctx, configChange, config, applyPairs(config, pairs, ctx.t, ctx.asJson));
|
|
315
278
|
}
|
|
279
|
+
/** Agent first, user second, like address's self/user pair. Apply only after both validate. */
|
|
280
|
+
export async function runPersona(ctx, args) {
|
|
281
|
+
const { change: configChange, config } = planConfig(ctx);
|
|
282
|
+
try {
|
|
283
|
+
const roles = args.length === 0 ? await askRoles(ctx, config) : args;
|
|
284
|
+
if (roles.length !== 2)
|
|
285
|
+
throw new ConfigError(ctx.t.persona.rolesNeedsPair);
|
|
286
|
+
return commit(ctx, configChange, config, applyPairs(config, [
|
|
287
|
+
["persona.agent", roles[0]], ["persona.user", roles[1]],
|
|
288
|
+
], ctx.t, ctx.asJson));
|
|
289
|
+
}
|
|
290
|
+
catch (cause) {
|
|
291
|
+
if (!(cause instanceof Error) || cause.code !== "ABORT_ERR")
|
|
292
|
+
throw cause;
|
|
293
|
+
return { lines: [` ${ctx.t.axis.nothingChanged}`], json: { changed: [], moves: [], cancelled: true }, after: async () => 130 };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function askRoles(ctx, config) {
|
|
297
|
+
if (!ctx.canPrompt || !interactive())
|
|
298
|
+
throw new ConfigError(ctx.t.persona.rolesNeedsPair);
|
|
299
|
+
const release = guardTerminal();
|
|
300
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
301
|
+
try {
|
|
302
|
+
for (const line of wrap(ctx.t.persona.rolesIntroduction, ctx.ui.width - 2))
|
|
303
|
+
process.stdout.write(` ${line}\n`);
|
|
304
|
+
const roles = [];
|
|
305
|
+
for (const field of ["agent", "user"]) {
|
|
306
|
+
const current = config.persona[field] ?? "";
|
|
307
|
+
for (;;) {
|
|
308
|
+
process.stdout.write("\n");
|
|
309
|
+
for (const line of wrap(`${ctx.t.create[field]}${current ? `: ${humanText(current)}` : ""}`, ctx.ui.width - 2)) {
|
|
310
|
+
process.stdout.write(` ${line}\n`);
|
|
311
|
+
}
|
|
312
|
+
const answer = (await rl.question(" > ")).trim();
|
|
313
|
+
const chosen = answer === '""' ? "" : answer || current;
|
|
314
|
+
try {
|
|
315
|
+
assign(structuredClone(config), `persona.${field}`, chosen, ctx.t, false);
|
|
316
|
+
roles.push(chosen);
|
|
317
|
+
process.stdout.write(` ${ctx.t.create.descriptionCount([...chosen].length, PERSONA_DESCRIPTION_MAX)}\n`);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
catch (cause) {
|
|
321
|
+
if (!(cause instanceof ConfigError))
|
|
322
|
+
throw cause;
|
|
323
|
+
for (const line of wrap(humanText(cause.message), ctx.ui.width - 2))
|
|
324
|
+
process.stdout.write(` ${ctx.ui.warn(line)}\n`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return roles;
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
try {
|
|
332
|
+
rl.close();
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
release();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
316
339
|
/**
|
|
317
340
|
* The two questions, on a terminal.
|
|
318
341
|
*
|
|
@@ -398,9 +421,9 @@ export async function runReset(ctx, args) {
|
|
|
398
421
|
: ` ${ctx.ui.bold(axis)} ${ctx.ui.dim(String(before))} ${ctx.ui.dim("→")} ${ctx.ui.bold(ctx.ui.accent(String(AXIS_DEFAULT[axis])))}`]
|
|
399
422
|
: wrap(ctx.t.axis.resetAll, ctx.ui.width - 2).map(line => ` ${ctx.ui.bold(ctx.ui.success(line))}`)),
|
|
400
423
|
` ${ctx.ui.success(toneChange.before === toneChange.after ? ctx.t.artifact.current : ctx.t.artifact.generated)}`,
|
|
401
|
-
...(ctx.flags.verbose ? [` ${ctx.ui.dim(
|
|
424
|
+
...(ctx.flags.verbose ? [` ${ctx.ui.dim(artifactSize(size, ctx.t))}`] : []),
|
|
402
425
|
],
|
|
403
426
|
json: { reset: axis ?? "all", chars: size.chars },
|
|
404
|
-
warnings: [...riskWarnings(config.axes, ctx.t), ...comboWarnings(config, ctx.t)
|
|
427
|
+
warnings: [...riskWarnings(config.axes, ctx.t), ...comboWarnings(config, ctx.t)],
|
|
405
428
|
};
|
|
406
429
|
}
|
package/dist/commands/show.js
CHANGED
|
@@ -12,8 +12,8 @@ import { toCompileInput } from "../core/resolve.js";
|
|
|
12
12
|
import { inspectContextLayout } from "../core/seeds.js";
|
|
13
13
|
import { catalogue, isLanguage } from "../core/i18n.js";
|
|
14
14
|
import { explainBlock, gaugeBlock, gaugeHeader } from "../ui/gauge.js";
|
|
15
|
-
import { planConfig
|
|
16
|
-
import {
|
|
15
|
+
import { planConfig } from "./compile.js";
|
|
16
|
+
import { artifactSize, combination, humanText, nextStep, riskWarnings } from "../ui/format.js";
|
|
17
17
|
import { maxCells, truncate, wrap } from "../ui/width.js";
|
|
18
18
|
import { inspectContextConsumers } from "../wiring/targets.js";
|
|
19
19
|
export function runShow(ctx, args) {
|
|
@@ -48,20 +48,20 @@ export function runShow(ctx, args) {
|
|
|
48
48
|
? "tonedial save"
|
|
49
49
|
: 'tonedial save "My Tone"';
|
|
50
50
|
const lines = [];
|
|
51
|
-
const
|
|
51
|
+
const sizeLabel = artifactSize(size, ctx.t);
|
|
52
52
|
// A display name has no length cap, so the title is the one row that can be pushed past the
|
|
53
53
|
// budget by state alone. Truncating keeps the size at the right margin where it is read.
|
|
54
54
|
const prefix = "ToneDial · show · ";
|
|
55
|
-
const nameBudget = ctx.ui.width - maxCells(
|
|
55
|
+
const nameBudget = ctx.ui.width - maxCells(sizeLabel) - maxCells(status) - maxCells(prefix) - 4;
|
|
56
56
|
if (nameBudget >= 2) {
|
|
57
|
-
lines.push(titleRow(prefix + truncate(humanText(activeName), nameBudget) + status,
|
|
57
|
+
lines.push(titleRow(prefix + truncate(humanText(activeName), nameBudget) + status, sizeLabel, ctx));
|
|
58
58
|
}
|
|
59
59
|
else {
|
|
60
60
|
const title = prefix + truncate(humanText(activeName), Math.max(2, ui.width - maxCells(status) - maxCells(prefix) - 2)) + status;
|
|
61
61
|
for (const folded of wrap(title, ui.width - 2))
|
|
62
62
|
lines.push(` ${ui.bold(folded)}`);
|
|
63
|
-
for (const folded of wrap(
|
|
64
|
-
lines.push(` ${
|
|
63
|
+
for (const folded of wrap(sizeLabel, ui.width - 2))
|
|
64
|
+
lines.push(` ${ui.dim(folded)}`);
|
|
65
65
|
}
|
|
66
66
|
const p = shown.persona;
|
|
67
67
|
const personaSet = Boolean(p.age || p.gender || p.note || p.address || p.user || p.agent);
|
|
@@ -76,20 +76,18 @@ export function runShow(ctx, args) {
|
|
|
76
76
|
p.address?.self_ref ? ctx.t.persona.callsItself(humanText(p.address.self_ref)) : null,
|
|
77
77
|
p.address?.user_ref ? ctx.t.persona.callsYou(humanText(p.address.user_ref)) : null,
|
|
78
78
|
p.note ? ctx.t.persona.noteChars([...p.note].length) : null,
|
|
79
|
-
p.user && p.agent
|
|
80
|
-
p.user && !p.agent
|
|
81
|
-
p.agent && !p.user
|
|
79
|
+
p.user && p.agent ? ctx.t.persona.descriptionCounts([...p.user].length, [...p.agent].length, PERSONA_DESCRIPTION_MAX) : null,
|
|
80
|
+
p.user && !p.agent ? ctx.t.persona.descriptionChars("user", [...p.user].length) : null,
|
|
81
|
+
p.agent && !p.user ? ctx.t.persona.descriptionChars("agent", [...p.agent].length) : null,
|
|
82
82
|
].filter(Boolean);
|
|
83
83
|
if (bits.length)
|
|
84
84
|
for (const folded of wrap(bits.join(" · "), detailWidth))
|
|
85
85
|
details.push(ui.dim(folded));
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
details.push(ui.dim(folded));
|
|
92
|
-
}
|
|
86
|
+
for (const [field, value] of [["agent", p.agent], ["user", p.user]]) {
|
|
87
|
+
if (value === null)
|
|
88
|
+
continue;
|
|
89
|
+
for (const folded of wrap(ctx.t.persona.descriptionFull(field, humanText(value)), detailWidth)) {
|
|
90
|
+
details.push(ui.dim(folded));
|
|
93
91
|
}
|
|
94
92
|
}
|
|
95
93
|
}
|
|
@@ -145,22 +143,16 @@ export function runShow(ctx, args) {
|
|
|
145
143
|
}
|
|
146
144
|
}
|
|
147
145
|
else {
|
|
148
|
-
// Keep the short legend and action together
|
|
146
|
+
// Keep the short legend and save action together after the settings.
|
|
149
147
|
const legend = ctx.t.artifact.movedFromDefault + (needsSave ? ` · ${ctx.t.preset.changesNotSaved}` : "");
|
|
150
148
|
const [first, ...rest] = wrap(legend, ui.width - 4);
|
|
151
149
|
lines.push(` ${ui.accent("*")} ${ui.dim(first)}`, ...rest.map((line) => ` ${ui.dim(line)}`), ...nextStep(preview ? `tonedial use ${preview.slug}` : needsSave ? saveCommand : "tonedial show --explain", ui).slice(1));
|
|
152
150
|
}
|
|
153
|
-
const combinations = checkCombos(axes
|
|
151
|
+
const combinations = checkCombos(axes);
|
|
154
152
|
const warnings = combinations.map((w) => combination(w, ctx.t));
|
|
155
|
-
if (personaSet) {
|
|
156
|
-
warnings.push(ctx.t.notice.personaUnanchored);
|
|
157
|
-
}
|
|
158
153
|
warnings.push(...riskWarnings(axes, ctx.t));
|
|
159
|
-
warnings.push(ctx.t.notice.modelExpectation);
|
|
160
154
|
if (activeConflict && preview === null)
|
|
161
155
|
warnings.push(ctx.t.preset.activeConflict(config.preset));
|
|
162
|
-
// Through the same owner every other command uses, rather than a sentence composed here.
|
|
163
|
-
warnings.push(...sizeWarning(size, ctx.t));
|
|
164
156
|
return {
|
|
165
157
|
lines,
|
|
166
158
|
json: {
|
|
@@ -183,7 +175,6 @@ export function runShow(ctx, args) {
|
|
|
183
175
|
// Interface language never changes the compiled tone.
|
|
184
176
|
language: config.language,
|
|
185
177
|
chars: size.chars,
|
|
186
|
-
cap: size.cap,
|
|
187
178
|
artifact: { exists: artifactOnDisk },
|
|
188
179
|
},
|
|
189
180
|
warnings,
|
|
@@ -194,14 +185,13 @@ export function runShow(ctx, args) {
|
|
|
194
185
|
* while both fit — on anything narrower they fall back to a single space rather than pushing
|
|
195
186
|
* the line past its budget. Right alignment is worth having and never worth an overflow.
|
|
196
187
|
*/
|
|
197
|
-
function titleRow(name,
|
|
198
|
-
const paint = overCap ? ctx.ui.error : ctx.ui.dim;
|
|
188
|
+
function titleRow(name, sizeLabel, ctx) {
|
|
199
189
|
// Measured pessimistically. A display name is user text and may hold ambiguous-width
|
|
200
190
|
// characters, so a gap sized by the narrow measure is a gap that overflows on the terminals
|
|
201
191
|
// this contract exists for.
|
|
202
|
-
const used = maxCells(name) + maxCells(
|
|
192
|
+
const used = maxCells(name) + maxCells(sizeLabel) + 2;
|
|
203
193
|
const gap = used <= ctx.ui.width ? " ".repeat(ctx.ui.width - used) : " ";
|
|
204
|
-
return ` ${ctx.ui.bold(name)}${gap}${
|
|
194
|
+
return ` ${ctx.ui.bold(name)}${gap}${ctx.ui.dim(sizeLabel)}`;
|
|
205
195
|
}
|
|
206
196
|
/**
|
|
207
197
|
* Safeword and register on one row. Plain text, because it is folded before it is
|
package/dist/commands/update.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// Update
|
|
1
|
+
// Update through the installation owner and report application-state work separately.
|
|
2
2
|
import { VERSION } from "../cli.js";
|
|
3
3
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
4
4
|
import { TextDecoder } from "node:util";
|
|
5
5
|
import { ConfigError, parseConfig } from "../core/config.js";
|
|
6
|
-
import { AGENT_TOOLS, UPDATE_COMMAND, installShape } from "../core/paths.js";
|
|
7
|
-
import { REGISTRY, downloadRelease, haveTar, installRelease, installedVersion, latestRelease } from "../core/selfupdate.js";
|
|
6
|
+
import { AGENT_TOOLS, UPDATE_COMMAND, installShape, shellQuote } from "../core/paths.js";
|
|
7
|
+
import { REGISTRY, downloadRelease, haveTar, inspectNpmInstall, installRelease, installedVersion, latestRelease, updateNpmInstall } from "../core/selfupdate.js";
|
|
8
8
|
import { confirm } from "../ui/prompt.js";
|
|
9
9
|
import { SEED_VERSION, inspectSeeds, recordedSeedSelection } from "../core/seeds.js";
|
|
10
|
-
import { humanText } from "../ui/format.js";
|
|
10
|
+
import { humanText, receiptLines } from "../ui/format.js";
|
|
11
11
|
import { urlIdentity } from "../core/net.js";
|
|
12
12
|
/** A damaged state file must not disable package guidance or reveal its parser excerpt. */
|
|
13
13
|
function updateConfig(ctx) {
|
|
@@ -84,16 +84,40 @@ export async function runUpdate(ctx) {
|
|
|
84
84
|
stateIssue = "unreadable";
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
const lines = [
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
const lines = [` ${ctx.ui.bold(ctx.t.update.running(VERSION))}`, ""];
|
|
88
|
+
const preview = () => {
|
|
89
|
+
if (!ctx.asJson) {
|
|
90
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
91
|
+
lines.length = 0;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
94
|
let replaced = null;
|
|
95
|
+
let installed = null;
|
|
96
|
+
let outcome;
|
|
95
97
|
if (shape !== "curl") {
|
|
96
|
-
|
|
98
|
+
if (ctx.flags.dryRun) {
|
|
99
|
+
outcome = "dry-run";
|
|
100
|
+
lines.push(` ${ctx.t.update.toUpdate}`, ` ${ctx.ui.accent(command)}`, ` ${ctx.t.wiring.dryRun}`);
|
|
101
|
+
}
|
|
102
|
+
else if (!ctx.flags.auto && !ctx.canPrompt) {
|
|
103
|
+
outcome = "approval-required";
|
|
104
|
+
lines.push(` ${ctx.t.update.toUpdate}`, ` ${ctx.ui.accent(command)}`, ...receiptLines(ctx.t.update.needsApproval, ctx.ui.width - 2).map(line => ` ${line}`));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const target = inspectNpmInstall(ctx.env, ctx.t);
|
|
108
|
+
lines.push(` ${ctx.t.update.toUpdate}`, ` ${ctx.ui.accent(`${command} --prefix "${shellQuote(humanText(target.prefix))}"`)}`);
|
|
109
|
+
preview();
|
|
110
|
+
if (ctx.flags.auto || (await confirm(ctx.t.update.npmQuestion)) === "yes") {
|
|
111
|
+
installed = updateNpmInstall(target, ctx.env, ctx.t);
|
|
112
|
+
replaced = installed === target.version ? null : installed;
|
|
113
|
+
outcome = replaced === null ? "unchanged" : "updated";
|
|
114
|
+
lines.push(` ${ctx.ui.success(ctx.t.update.npmComplete(installed))}`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
outcome = "declined";
|
|
118
|
+
lines.push(` ${ctx.t.update.declined}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
97
121
|
}
|
|
98
122
|
else {
|
|
99
123
|
// Before the network, like `install.sh` checks its tools before it fetches anything.
|
|
@@ -104,29 +128,38 @@ export async function runUpdate(ctx) {
|
|
|
104
128
|
if (release.version === VERSION) {
|
|
105
129
|
// Nothing downloaded. The common case is an install that is already current, and paying
|
|
106
130
|
// for the whole package to be told so is a cost with nothing on the other side of it.
|
|
131
|
+
installed = VERSION;
|
|
132
|
+
outcome = "unchanged";
|
|
107
133
|
lines.push(` ${ctx.ui.success(ctx.t.update.already(release.version))}`);
|
|
108
134
|
}
|
|
109
135
|
else if (ctx.flags.dryRun) {
|
|
110
|
-
|
|
136
|
+
outcome = "dry-run";
|
|
137
|
+
lines.push(` ${ctx.t.update.wouldReplace(humanText(ctx.paths.lib), humanText(release.version))}`);
|
|
111
138
|
}
|
|
112
139
|
else if (!ctx.flags.auto && !ctx.canPrompt) {
|
|
113
140
|
// JSON never asks, including on a terminal. Explicit --auto supplies approval through
|
|
114
141
|
// the same branch as human output; absent approval, report the existing no-install result.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
else if (ctx.flags.auto || (await confirm(ctx.t.update.question(release.version))) === "yes") {
|
|
118
|
-
const tarball = await downloadRelease(release, ctx.t);
|
|
119
|
-
lines.push(` ${ctx.ui.accent(ctx.t.update.found(release.version, tarball.byteLength))}`);
|
|
120
|
-
installRelease(ctx.paths, tarball, ctx.t);
|
|
121
|
-
// **What landed, not what was claimed.** The registry's `version` is metadata; a stub or
|
|
122
|
-
// a mistake serving one number and a different tarball made `update` report a version it
|
|
123
|
-
// had not installed — and, because the no-op check compares that metadata against this
|
|
124
|
-
// process's own constant, swap again on every run forever.
|
|
125
|
-
replaced = installedVersion(ctx.paths) ?? release.version;
|
|
126
|
-
lines.push(` ${ctx.ui.success(ctx.ui.bold(ctx.t.update.replaced(replaced)))}`);
|
|
142
|
+
outcome = "approval-required";
|
|
143
|
+
lines.push(...receiptLines(ctx.t.update.needsApproval, ctx.ui.width - 2).map(line => ` ${line}`));
|
|
127
144
|
}
|
|
128
145
|
else {
|
|
129
|
-
lines.push(` ${ctx.t.update.
|
|
146
|
+
lines.push(` ${ctx.t.update.wouldReplace(humanText(ctx.paths.lib), humanText(release.version))}`);
|
|
147
|
+
preview();
|
|
148
|
+
if (ctx.flags.auto || (await confirm(ctx.t.update.question(release.version))) === "yes") {
|
|
149
|
+
const tarball = await downloadRelease(release, ctx.t);
|
|
150
|
+
lines.push(` ${ctx.ui.accent(ctx.t.update.found(release.version, tarball.byteLength))}`);
|
|
151
|
+
installRelease(ctx.paths, tarball, ctx.t);
|
|
152
|
+
installed = installedVersion(ctx.paths);
|
|
153
|
+
if (installed === null)
|
|
154
|
+
throw new ConfigError(ctx.t.update.versionUnknown);
|
|
155
|
+
replaced = installed;
|
|
156
|
+
outcome = "updated";
|
|
157
|
+
lines.push(` ${ctx.ui.success(ctx.ui.bold(ctx.t.update.replaced(installed)))}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
outcome = "declined";
|
|
161
|
+
lines.push(` ${ctx.t.update.declined}`);
|
|
162
|
+
}
|
|
130
163
|
}
|
|
131
164
|
}
|
|
132
165
|
const warnings = [];
|
|
@@ -134,20 +167,33 @@ export async function runUpdate(ctx) {
|
|
|
134
167
|
// `seedFiles` are constants in the copy that has just been replaced, so a release that moved
|
|
135
168
|
// the anchors would be reported against the old ones — which is the exact gap this command
|
|
136
169
|
// exists to close. Say the one thing that is still true and stop measuring.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
170
|
+
const inspectable = replaced === null ? stale ?? [] : [];
|
|
171
|
+
const routine = inspectable.filter(({ state }) => state === "missing" || state === "stale");
|
|
172
|
+
const problems = inspectable.filter(({ state }) => state !== "missing" && state !== "stale");
|
|
173
|
+
const fresh = stateIssue === "missing" && ![ctx.paths.agents, ctx.paths.tone, ctx.paths.presets, ctx.paths.skill].some(path => {
|
|
174
|
+
try {
|
|
175
|
+
return lstatSync(path, { throwIfNoEntry: false }) !== undefined;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
if (stateIssue !== null && !fresh)
|
|
182
|
+
warnings.push(ctx.t.update.stateIssue[stateIssue]);
|
|
183
|
+
if (routine.length)
|
|
184
|
+
lines.push("", ` ${ctx.t.update.refreshFiles(routine.length)}`);
|
|
185
|
+
if (problems.length) {
|
|
186
|
+
lines.push("", ` ${ctx.t.wiring.stateRoot}`, ` ${humanText(ctx.paths.root)}`);
|
|
187
|
+
for (const reason of ["modified", "foreign", "unreadable"]) {
|
|
188
|
+
const group = problems.filter(({ state }) => state === reason);
|
|
189
|
+
if (group.length)
|
|
190
|
+
warnings.push([ctx.t.doctor.seedReason[reason], ...group.map(({ seed }) => seed.label)].join("\n"));
|
|
149
191
|
}
|
|
150
192
|
}
|
|
193
|
+
const stateRecoveryRequired = !fresh && (stateIssue !== null || config?.seedVersion === 0 || inspectable.length > 0 || replaced !== null);
|
|
194
|
+
const recoveryCommand = fresh ? "tonedial init" : problems.length ? "tonedial doctor" : "tonedial doctor --fix";
|
|
195
|
+
if (fresh || stateRecoveryRequired)
|
|
196
|
+
lines.push("", ` ${fresh ? ctx.t.update.setupNeeded : problems.length ? ctx.t.update.reviewState : ctx.t.update.thenRefresh}`, ` ${ctx.ui.accent(recoveryCommand)}`);
|
|
151
197
|
return {
|
|
152
198
|
lines,
|
|
153
199
|
json: {
|
|
@@ -155,17 +201,18 @@ export async function runUpdate(ctx) {
|
|
|
155
201
|
shape,
|
|
156
202
|
command,
|
|
157
203
|
replaced,
|
|
204
|
+
installedVersion: installed,
|
|
205
|
+
outcome,
|
|
158
206
|
seedVersion: config?.seedVersion ?? null,
|
|
159
207
|
stateIssue,
|
|
160
|
-
stateRecoveryRequired
|
|
161
|
-
recoveryCommand
|
|
208
|
+
stateRecoveryRequired,
|
|
209
|
+
recoveryCommand,
|
|
162
210
|
// Null after a swap: this process is the old package, and reporting its constants as the
|
|
163
211
|
// installed ones would be a claim it cannot make.
|
|
164
212
|
packageSeedVersion: replaced === null ? SEED_VERSION : null,
|
|
165
213
|
staleSeeds: replaced === null ? (stale?.map(({ seed }) => seed.label) ?? null) : null,
|
|
166
214
|
seedIssues: replaced === null ? (stale?.map(({ seed, state }) => ({ path: seed.path, reason: state })) ?? null) : null,
|
|
167
|
-
//
|
|
168
|
-
// must not read silence here as "you are up to date".
|
|
215
|
+
// npm owns registry/cache resolution; only the curl branch observes remote metadata.
|
|
169
216
|
checkedRemote: shape === "curl",
|
|
170
217
|
},
|
|
171
218
|
warnings,
|
package/dist/commands/use.js
CHANGED
|
@@ -6,9 +6,9 @@ import { ConfigError, serializeConfig } from "../core/config.js";
|
|
|
6
6
|
import { exportAliasesState, projectTonePath } from "../core/paths.js";
|
|
7
7
|
import { check as checkCombos } from "../core/combos.js";
|
|
8
8
|
import { withPreset, assertPresetSelection, inspectPresetLibrary, resolvePreset } from "../core/presets.js";
|
|
9
|
-
import { applyPlanned, planArtifact, planConfig
|
|
9
|
+
import { applyPlanned, planArtifact, planConfig } from "./compile.js";
|
|
10
10
|
import { planChange, rawReadCheck } from "../wiring/apply.js";
|
|
11
|
-
import {
|
|
11
|
+
import { artifactSize, combination, humanText, nextStep } from "../ui/format.js";
|
|
12
12
|
import { truncate, wrap } from "../ui/width.js";
|
|
13
13
|
/**
|
|
14
14
|
* How much of a preset's name this screen shows. `list` and `show` already truncate; this line
|
|
@@ -65,16 +65,16 @@ export async function runUse(ctx, args) {
|
|
|
65
65
|
risk: ctx.t.notice.risk,
|
|
66
66
|
roleplay: config.axes.playfulness >= 4 ? ctx.t.notice.roleplay : null,
|
|
67
67
|
};
|
|
68
|
-
const notices = [notice.safeword, ...(notice.riskAxes ? [notice.riskAxes] : [])
|
|
68
|
+
const notices = [notice.safeword, ...(notice.riskAxes ? [notice.riskAxes] : [])];
|
|
69
69
|
const lines = [
|
|
70
70
|
...wrap(ctx.t.preset.active(truncate(humanText(preset.displayName), ui.width - 12)), ui.width - 2)
|
|
71
71
|
.map((line) => ` ${ui.success(ui.bold(line))}`),
|
|
72
72
|
` ${ctx.t.artifact.generated}`,
|
|
73
|
-
...(ctx.flags.verbose ? [` ${ui.dim(
|
|
73
|
+
...(ctx.flags.verbose ? [` ${ui.dim(artifactSize(size, ctx.t))}`] : []),
|
|
74
74
|
// Folded here rather than at the printer: `lines` are laid out by the command that built
|
|
75
75
|
// them, and a printer that reflowed them would break every column in the gauge block.
|
|
76
76
|
"",
|
|
77
|
-
...notices.flatMap((n) => wrap(humanText(n), ui.width - 2).map((folded) => ` ${n === notice.riskAxes ? ui.risk(folded) :
|
|
77
|
+
...notices.flatMap((n) => wrap(humanText(n), ui.width - 2).map((folded) => ` ${n === notice.riskAxes ? ui.risk(folded) : folded}`)),
|
|
78
78
|
...project,
|
|
79
79
|
...nextStep("tonedial show", ui),
|
|
80
80
|
];
|
|
@@ -93,9 +93,7 @@ export async function runUse(ctx, args) {
|
|
|
93
93
|
project: projectPath,
|
|
94
94
|
},
|
|
95
95
|
warnings: [
|
|
96
|
-
...(
|
|
97
|
-
...checkCombos(config.axes, config.persona, config.speech_level).map((w) => combination(w, ctx.t)),
|
|
98
|
-
...sizeWarning(size, ctx.t),
|
|
96
|
+
...checkCombos(config.axes).map((w) => combination(w, ctx.t)),
|
|
99
97
|
],
|
|
100
98
|
};
|
|
101
99
|
}
|
package/dist/commands/wire.js
CHANGED
|
@@ -453,7 +453,7 @@ function summary(ctx, reason, tools, action = "connect") {
|
|
|
453
453
|
return receiptLines(action === "disconnect" ? ctx.t.wiring.alreadyDisconnected(named) : ctx.t.wiring.alreadyMatch(named), ctx.ui.width - 2)
|
|
454
454
|
.map((line) => ` ${ctx.ui.bold(line)}`);
|
|
455
455
|
if (reason === "dry-run")
|
|
456
|
-
return [` ${ctx.ui.
|
|
456
|
+
return [` ${ctx.ui.dim(ctx.t.wiring.dryRun)}`];
|
|
457
457
|
if (reason === "declined")
|
|
458
458
|
return [` ${ctx.t.wiring.declined}`];
|
|
459
459
|
return [
|
package/dist/core/anchors.js
CHANGED
|
@@ -182,6 +182,3 @@ export const CONTROLS = {
|
|
|
182
182
|
that message still goes through its normal checks.`,
|
|
183
183
|
fallback: "If the user asks to change the tone, use the `tonedial` CLI (`tonedial --help`).",
|
|
184
184
|
};
|
|
185
|
-
/** The compiled artifact's budget, in Unicode code points across the whole file. */
|
|
186
|
-
export const TARGET_CHARS = 1800;
|
|
187
|
-
export const CAP_CHARS = 2700;
|