tonedial 0.1.0 → 0.1.1

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 CHANGED
@@ -20,7 +20,7 @@ import { runEdit } from "./commands/edit.js";
20
20
  import { runUpdate } from "./commands/update.js";
21
21
  import { runSave } from "./commands/save.js";
22
22
  import { runCreate } from "./commands/create.js";
23
- import { runAddress, runSet, runReset } from "./commands/set.js";
23
+ import { runAddress, runPersona, runSet, runReset } from "./commands/set.js";
24
24
  import { runShow } from "./commands/show.js";
25
25
  import { runUse } from "./commands/use.js";
26
26
  import { runDoctor } from "./commands/doctor.js";
@@ -34,7 +34,7 @@ export const JSON_SCHEMA = 3;
34
34
  * run time is the one thing that breaks all three binary packagers at once. A test asserts this
35
35
  * equals the manifest, which is the cheap half of the trade.
36
36
  */
37
- export const VERSION = "0.1.0";
37
+ export const VERSION = "0.1.1";
38
38
  const FLAG_NAMES = ["project", "check", "explain", "global", "here", "migrateLegacy", "auto", "dryRun", "fix", "rollback", "fromOperation", "fromPreset", "restorePreset", "preset", "slug", "acceptUsage", "lang", "commands", "tools"];
39
39
  /** Command-line spelling for each flag, where it differs from the field name. */
40
40
  const FLAG_SPELLING = {
@@ -49,6 +49,7 @@ const COMMANDS = {
49
49
  set: { run: runSet, flags: [], max: null, context: "state" },
50
50
  test: { run: runTest, flags: ["project", "dryRun", "preset", "acceptUsage"], max: 1 },
51
51
  address: { run: runAddress, flags: [], max: 2, context: "state" },
52
+ persona: { run: runPersona, flags: [], max: 2, context: "state" },
52
53
  reset: { run: runReset, flags: [], max: 1, context: "state" },
53
54
  show: { run: runShow, flags: ["explain"], max: 1 },
54
55
  create: { run: runCreate, flags: ["slug", "auto", "dryRun"], max: null, context: "state" },
@@ -78,6 +79,7 @@ const COMMAND_GROUPS = [
78
79
  ["tone", [
79
80
  ["set", "set <axis>=<level> …"],
80
81
  ["address", "address [self user]"],
82
+ ["persona", "persona [agent user]"],
81
83
  ["reset", "reset [axis]"],
82
84
  ["show", "show [slug]"],
83
85
  ["test", "test [claude|codex]"],
@@ -108,6 +110,7 @@ const COMMAND_EXAMPLES = {
108
110
  set: ["tonedial set", "tonedial set warmth=5 critique=4", "tonedial set persona.agent=reviewer"],
109
111
  test: ["tonedial test codex --dry-run", "tonedial test claude --project .", "tonedial test codex --preset my-tone --accept-usage"],
110
112
  address: ['tonedial address "assistant" "friend"'],
113
+ persona: ["tonedial persona", 'tonedial persona "Fictional reviewer" "Developer"'],
111
114
  reset: ["tonedial reset", "tonedial reset warmth"],
112
115
  show: ["tonedial show", "tonedial show --explain"],
113
116
  create: ["tonedial create", 'tonedial create "Jarvis" --slug jarvis care=3 --auto'],
@@ -4,7 +4,7 @@ import { compile, measure } from "../core/compile.js";
4
4
  import { ConfigError, defaultConfig, parseConfig } from "../core/config.js";
5
5
  import { toCompileInput } from "../core/resolve.js";
6
6
  import { contextArtifactTargets } from "../core/seeds.js";
7
- import { budget, humanText, nextStep, num } from "../ui/format.js";
7
+ import { artifactSize, humanText, nextStep } from "../ui/format.js";
8
8
  import { wrap } from "../ui/width.js";
9
9
  import { confirm } from "../ui/prompt.js";
10
10
  import { captureRawEntry, gate, planChange, rawReadChange } from "../wiring/apply.js";
@@ -60,15 +60,6 @@ export async function applyPlanned(ctx, changes, finalChanges = [], review = fal
60
60
  }
61
61
  return result;
62
62
  }
63
- export function sizeWarning(size, t) {
64
- // It warns and never fails: a user is entitled to switch every axis on, and a budget that
65
- // refuses to compile is a different product.
66
- if (size.overCap)
67
- return [t.artifact.overCap(num(size.chars), num(size.cap))];
68
- if (size.overTarget)
69
- return [t.artifact.overTarget(num(size.chars), num(size.target))];
70
- return [];
71
- }
72
63
  export async function runCompile(ctx) {
73
64
  const check = ctx.flags.check;
74
65
  const { change: configChange, config } = planConfig(ctx);
@@ -113,10 +104,10 @@ export async function runCompile(ctx) {
113
104
  lines: [
114
105
  ...wrap(status, ctx.ui.width - 2).map(line => ` ${ctx.ui.bold((!check || current ? ctx.ui.success : ctx.ui.warn)(line))}`),
115
106
  ` ${humanText(ctx.paths.tone)}`,
116
- ` ${size.overCap ? ctx.ui.error(budget(size, ctx.t)) : ctx.ui.dim(budget(size, ctx.t))}`,
107
+ ` ${ctx.ui.dim(artifactSize(size, ctx.t))}`,
117
108
  ...nextStep(check && !current ? "tonedial compile" : "tonedial show", ctx.ui),
118
109
  ],
119
110
  json: { path: ctx.paths.tone, agentsPath: ctx.paths.agents, written: !check, ...size },
120
- warnings: sizeWarning(size, ctx.t),
111
+ warnings: [],
121
112
  };
122
113
  }
@@ -15,7 +15,7 @@ import { inspect as inspectSkill, skillPath } from "../wiring/skill.js";
15
15
  import { candidateRcPaths, pathLineState, rcPath, withPathLine } from "../wiring/shell.js";
16
16
  import { SEED_VERSION, inspectContextLayout, inspectSeeds, recordedSeedSelection, } from "../core/seeds.js";
17
17
  import { toCompileInput } from "../core/resolve.js";
18
- import { budget, humanText, nextStep, receiptLines, recoveryRollbackLines, section } from "../ui/format.js";
18
+ import { artifactSize, humanText, nextStep, receiptLines, recoveryRollbackLines, section } from "../ui/format.js";
19
19
  import { wrap } from "../ui/width.js";
20
20
  import { applyRollback, assertFresh, planChange, previewRollback } from "../wiring/apply.js";
21
21
  import { confirm } from "../ui/prompt.js";
@@ -585,7 +585,7 @@ async function doctorReport(ctx, showRollbackCommands = true, showCodexAction =
585
585
  checks.push(onDisk === null
586
586
  ? { name: "TONE.md", state: "problem", detail: ctx.t.doctor.toneNotWritten, repair: "tonedial compile" }
587
587
  : onDisk === expected
588
- ? { name: "TONE.md", state: "ok", detail: budget(measure(expected), ctx.t) }
588
+ ? { name: "TONE.md", state: "ok", detail: artifactSize(measure(expected), ctx.t) }
589
589
  : {
590
590
  name: "TONE.md",
591
591
  state: "problem",
@@ -6,9 +6,9 @@ import { tmpdir } from "node:os";
6
6
  import { isDeepStrictEqual } from "node:util";
7
7
  import { CONFIG_SCHEMA, ConfigError, parseConfig, serializeConfig } from "../core/config.js";
8
8
  import { BUNDLED, SLUG_PATTERN, matchesPreset, withPreset, assertPresetSelection, customPresetDir, inspectPresetLibrary, parsePreset, preparePresetWrite, resolvePreset, } from "../core/presets.js";
9
- import { applyPlanned, planArtifact, planConfig, sizeWarning } from "./compile.js";
9
+ import { applyPlanned, planArtifact, planConfig } from "./compile.js";
10
10
  import { check as checkCombos } from "../core/combos.js";
11
- import { combination, humanText, riskWarnings } from "../ui/format.js";
11
+ import { artifactSize, combination, humanText, riskWarnings } from "../ui/format.js";
12
12
  import { captureRawEntry, planChange, rawReadChange, rawReadCheck } from "../wiring/apply.js";
13
13
  import { emptyWiring } from "../wiring/ownership.js";
14
14
  /**
@@ -116,11 +116,11 @@ export async function runEdit(ctx, args) {
116
116
  }
117
117
  const { change: toneChange, size } = planArtifact(ctx, config);
118
118
  await commit([toneChange], [{ ...baseline, after }]);
119
- lines.push(` ${(size.overCap ? ctx.ui.error : ctx.ui.success)(ctx.t.artifact.recompiled(size.chars, size.cap))}`);
119
+ lines.push(` ${ctx.ui.dim(artifactSize(size, ctx.t))}`);
120
120
  return {
121
121
  lines,
122
122
  json: { path: target.path, editor: editor[0], opened: true, changed: true, chars: size.chars },
123
- warnings: stateWarnings(ctx, config, size),
123
+ warnings: stateWarnings(ctx, config),
124
124
  };
125
125
  }
126
126
  // **Refused, not dropped.** `parsePreset` returns the slug it was handed and ignores the one
@@ -189,8 +189,8 @@ export async function runEdit(ctx, args) {
189
189
  const { change: toneChange, size } = planArtifact(ctx, config);
190
190
  const finalConfig = { ...configRead.change, after: serializeConfig(config, ctx.t) };
191
191
  await commit([configRead.change, presetChange, toneChange], [finalConfig]);
192
- lines.push(` ${(size.overCap ? ctx.ui.error : ctx.ui.success)(ctx.t.artifact.recompiled(size.chars, size.cap))}`);
193
- warnings.push(...stateWarnings(ctx, config, size));
192
+ lines.push(` ${ctx.ui.dim(artifactSize(size, ctx.t))}`);
193
+ warnings.push(...stateWarnings(ctx, config));
194
194
  }
195
195
  else {
196
196
  await commit([configRead.change, presetChange]);
@@ -226,13 +226,12 @@ export async function runEdit(ctx, args) {
226
226
  }
227
227
  }
228
228
  /** The notices every other state-changing command emits, through the same owners they use. */
229
- function stateWarnings(ctx, config, size) {
229
+ function stateWarnings(ctx, config) {
230
230
  const p = config.persona;
231
231
  return [
232
232
  ...checkCombos(config.axes, p, config.speech_level).map((w) => combination(w, ctx.t)),
233
233
  ...(p.age || p.gender || p.note || p.address || p.user || p.agent ? [ctx.t.notice.personaUnanchored] : []),
234
234
  ...riskWarnings(config.axes, ctx.t),
235
- ...sizeWarning(size, ctx.t),
236
235
  ];
237
236
  }
238
237
  /**
@@ -13,7 +13,7 @@ import { BUNDLED, localizedPreset, matchesPreset, withPreset } from "../core/pre
13
13
  import { SEED_VERSION, inspectContextLayout, planSeedChanges } from "../core/seeds.js";
14
14
  import { inspectOperations } from "../core/operations.js";
15
15
  import { SKILL_NAME, TONE_COMMAND_MD, TONE_COMMAND_VERSION } from "../core/skill.js";
16
- import { budget, humanText, nativeSetup, receiptLines, recoveryRollbackLines, riskWarnings, section } from "../ui/format.js";
16
+ import { artifactSize, humanText, nativeSetup, receiptLines, recoveryRollbackLines, riskWarnings, section } from "../ui/format.js";
17
17
  import { riskyAxes } from "../core/anchors.js";
18
18
  import { padEnd, wrap } from "../ui/width.js";
19
19
  import { gate, planChange } from "../wiring/apply.js";
@@ -204,7 +204,7 @@ export async function runInit(entry, _args) {
204
204
  addReceipt(ctx.t.artifact.generated, ctx.ui.success);
205
205
  }
206
206
  if (ctx.flags.verbose) {
207
- lines.push(` ${ctx.ui.dim(budget(size, ctx.t))}`);
207
+ lines.push(` ${ctx.ui.dim(artifactSize(size, ctx.t))}`);
208
208
  // The seed list grows with every bundled preset, so it folds like any other sentence.
209
209
  addReceipt(ctx.t.wiring.seeded(seeds.labels));
210
210
  if (skills.length) {
@@ -2,10 +2,10 @@
2
2
  import { AXIS_DEFAULT, AXIS_ORDER } from "../core/anchors.js";
3
3
  import { ConfigError, serializeConfig } from "../core/config.js";
4
4
  import { BUNDLED, PRESET_SCHEMA, SLUG_PATTERN, preparePresetWrite, presetSettings, resolvePreset, slugify } from "../core/presets.js";
5
- import { budget, humanText, nextStep } from "../ui/format.js";
5
+ import { artifactSize, humanText, nextStep } from "../ui/format.js";
6
6
  import { cells, truncate, wrap } from "../ui/width.js";
7
7
  import { PERSONA_DESCRIPTION_MAX } from "../core/combos.js";
8
- import { applyPlanned, planArtifact, planConfig, sizeWarning } from "./compile.js";
8
+ import { applyPlanned, planArtifact, planConfig } from "./compile.js";
9
9
  import { planChange, rawReadChange, rawReadCheck } from "../wiring/apply.js";
10
10
  /** Only the axes that moved. Every axis is still emitted; sparsity is storage, not compilation. */
11
11
  function movedValues(axes) {
@@ -73,7 +73,7 @@ export async function savePreset(ctx, displayName, state, options = {}) {
73
73
  };
74
74
  const count = Object.keys(values).length;
75
75
  const countLabel = ctx.t.preset.nonDefault(count);
76
- const warnings = [...sizeWarning(size, ctx.t)];
76
+ const warnings = [];
77
77
  if (replaced)
78
78
  warnings.push(ctx.t.preset.replaced(slug));
79
79
  // Sparsity survives on one reason after the omit-defaults rule fell: fewer extremes mean
@@ -89,7 +89,7 @@ export async function savePreset(ctx, displayName, state, options = {}) {
89
89
  ` ${ctx.ui.success(ctx.ui.bold(ctx.t.preset.savedTarget(slug)))}`,
90
90
  ` ${truncate(humanText(displayName), Math.max(1, ctx.ui.width - cells(` ${countLabel}`)))} ${ctx.ui.dim(countLabel)}`,
91
91
  ` ${ctx.t.artifact.generated}`,
92
- ...(ctx.flags.verbose ? [` ${ctx.ui.dim(budget(size, ctx.t))}`] : []),
92
+ ...(ctx.flags.verbose ? [` ${ctx.ui.dim(artifactSize(size, ctx.t))}`] : []),
93
93
  ...nextStep("tonedial list", ctx.ui),
94
94
  ],
95
95
  json: { slug, displayName, values, settings: preset.settings, nonDefaultAxes: count, replaced, chars: size.chars,
@@ -1,4 +1,4 @@
1
- // `set`, `reset`, and `address`. One action: change state, then recompile, so the two cannot
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
@@ -10,11 +10,11 @@ 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 { budget, combination, humanText, riskWarnings } from "../ui/format.js";
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, sizeWarning } from "./compile.js";
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))
@@ -264,7 +264,7 @@ async function commit(ctx, configChange, config, applied) {
264
264
  ` ${ctx.ui.bold(finalConfig.before === finalConfig.after ? ctx.t.axis.settingsUnchanged : ctx.ui.success(ctx.t.axis.settingsSaved))}`,
265
265
  ...movementLines(applied.moves, ctx, config.axes),
266
266
  ` ${ctx.ui.success(toneChange.before === toneChange.after ? ctx.t.artifact.current : ctx.t.artifact.generated)}`,
267
- ...(ctx.flags.verbose ? [` ${ctx.ui.dim(budget(size, ctx.t))}`] : []),
267
+ ...(ctx.flags.verbose ? [` ${ctx.ui.dim(artifactSize(size, ctx.t))}`] : []),
268
268
  ],
269
269
  json: {
270
270
  changed: applied.changed,
@@ -272,7 +272,7 @@ async function commit(ctx, configChange, config, applied) {
272
272
  config: toCompileInput(config),
273
273
  chars: size.chars,
274
274
  },
275
- warnings: [...applied.warnings, ...riskWarnings(config.axes, ctx.t), ...comboWarnings(config, ctx.t), ...sizeWarning(size, ctx.t)],
275
+ warnings: [...applied.warnings, ...riskWarnings(config.axes, ctx.t), ...comboWarnings(config, ctx.t)],
276
276
  };
277
277
  }
278
278
  export async function runSet(ctx, args) {
@@ -313,6 +313,66 @@ export async function runAddress(ctx, args) {
313
313
  ];
314
314
  return commit(ctx, configChange, config, applyPairs(config, pairs, ctx.t, ctx.asJson));
315
315
  }
316
+ /** Agent first, user second, like address's self/user pair. Apply only after both validate. */
317
+ export async function runPersona(ctx, args) {
318
+ const { change: configChange, config } = planConfig(ctx);
319
+ try {
320
+ const roles = args.length === 0 ? await askRoles(ctx, config) : args;
321
+ if (roles.length !== 2)
322
+ throw new ConfigError(ctx.t.persona.rolesNeedsPair);
323
+ return commit(ctx, configChange, config, applyPairs(config, [
324
+ ["persona.agent", roles[0]], ["persona.user", roles[1]],
325
+ ], ctx.t, ctx.asJson));
326
+ }
327
+ catch (cause) {
328
+ if (!(cause instanceof Error) || cause.code !== "ABORT_ERR")
329
+ throw cause;
330
+ return { lines: [` ${ctx.t.axis.nothingChanged}`], json: { changed: [], moves: [], cancelled: true }, after: async () => 130 };
331
+ }
332
+ }
333
+ async function askRoles(ctx, config) {
334
+ if (!ctx.canPrompt || !interactive())
335
+ throw new ConfigError(ctx.t.persona.rolesNeedsPair);
336
+ const release = guardTerminal();
337
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
338
+ try {
339
+ for (const line of wrap(ctx.t.persona.rolesIntroduction, ctx.ui.width - 2))
340
+ process.stdout.write(` ${line}\n`);
341
+ const roles = [];
342
+ for (const field of ["agent", "user"]) {
343
+ const current = config.persona[field] ?? "";
344
+ for (;;) {
345
+ process.stdout.write("\n");
346
+ for (const line of wrap(`${ctx.t.create[field]}${current ? `: ${humanText(current)}` : ""}`, ctx.ui.width - 2)) {
347
+ process.stdout.write(` ${line}\n`);
348
+ }
349
+ const answer = (await rl.question(" > ")).trim();
350
+ const chosen = answer === '""' ? "" : answer || current;
351
+ try {
352
+ assign(structuredClone(config), `persona.${field}`, chosen, ctx.t, false);
353
+ roles.push(chosen);
354
+ process.stdout.write(` ${ctx.t.create.descriptionCount([...chosen].length, PERSONA_DESCRIPTION_MAX)}\n`);
355
+ break;
356
+ }
357
+ catch (cause) {
358
+ if (!(cause instanceof ConfigError))
359
+ throw cause;
360
+ for (const line of wrap(humanText(cause.message), ctx.ui.width - 2))
361
+ process.stdout.write(` ${ctx.ui.warn(line)}\n`);
362
+ }
363
+ }
364
+ }
365
+ return roles;
366
+ }
367
+ finally {
368
+ try {
369
+ rl.close();
370
+ }
371
+ finally {
372
+ release();
373
+ }
374
+ }
375
+ }
316
376
  /**
317
377
  * The two questions, on a terminal.
318
378
  *
@@ -398,9 +458,9 @@ export async function runReset(ctx, args) {
398
458
  : ` ${ctx.ui.bold(axis)} ${ctx.ui.dim(String(before))} ${ctx.ui.dim("→")} ${ctx.ui.bold(ctx.ui.accent(String(AXIS_DEFAULT[axis])))}`]
399
459
  : wrap(ctx.t.axis.resetAll, ctx.ui.width - 2).map(line => ` ${ctx.ui.bold(ctx.ui.success(line))}`)),
400
460
  ` ${ctx.ui.success(toneChange.before === toneChange.after ? ctx.t.artifact.current : ctx.t.artifact.generated)}`,
401
- ...(ctx.flags.verbose ? [` ${ctx.ui.dim(budget(size, ctx.t))}`] : []),
461
+ ...(ctx.flags.verbose ? [` ${ctx.ui.dim(artifactSize(size, ctx.t))}`] : []),
402
462
  ],
403
463
  json: { reset: axis ?? "all", chars: size.chars },
404
- warnings: [...riskWarnings(config.axes, ctx.t), ...comboWarnings(config, ctx.t), ...sizeWarning(size, ctx.t)],
464
+ warnings: [...riskWarnings(config.axes, ctx.t), ...comboWarnings(config, ctx.t)],
405
465
  };
406
466
  }
@@ -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, sizeWarning } from "./compile.js";
16
- import { budget as budgetLine, combination, humanText, nextStep, riskWarnings } from "../ui/format.js";
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 budget = budgetLine(size, ctx.t);
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(budget) - maxCells(status) - maxCells(prefix) - 4;
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, budget, size.overCap, ctx));
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(budget, ui.width - 2))
64
- lines.push(` ${(size.overCap ? ui.error : ui.dim)(folded)}`);
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 && !ctx.flags.verbose ? ctx.t.persona.descriptionCounts([...p.user].length, [...p.agent].length, PERSONA_DESCRIPTION_MAX) : null,
80
- p.user && !p.agent && !ctx.flags.verbose ? ctx.t.persona.descriptionChars("user", [...p.user].length) : null,
81
- p.agent && !p.user && !ctx.flags.verbose ? ctx.t.persona.descriptionChars("agent", [...p.agent].length) : null,
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
- if (ctx.flags.verbose) {
87
- for (const [field, value] of [["user", p.user], ["agent", p.agent]]) {
88
- if (value === null)
89
- continue;
90
- for (const folded of wrap(ctx.t.persona.descriptionFull(field, humanText(value)), detailWidth)) {
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,7 +143,7 @@ export function runShow(ctx, args) {
145
143
  }
146
144
  }
147
145
  else {
148
- // Keep the short legend and action together so a one-row persona still fits 80×24.
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));
@@ -159,8 +157,6 @@ export function runShow(ctx, args) {
159
157
  warnings.push(ctx.t.notice.modelExpectation);
160
158
  if (activeConflict && preview === null)
161
159
  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
160
  return {
165
161
  lines,
166
162
  json: {
@@ -183,7 +179,6 @@ export function runShow(ctx, args) {
183
179
  // Interface language never changes the compiled tone.
184
180
  language: config.language,
185
181
  chars: size.chars,
186
- cap: size.cap,
187
182
  artifact: { exists: artifactOnDisk },
188
183
  },
189
184
  warnings,
@@ -194,14 +189,13 @@ export function runShow(ctx, args) {
194
189
  * while both fit — on anything narrower they fall back to a single space rather than pushing
195
190
  * the line past its budget. Right alignment is worth having and never worth an overflow.
196
191
  */
197
- function titleRow(name, budget, overCap, ctx) {
198
- const paint = overCap ? ctx.ui.error : ctx.ui.dim;
192
+ function titleRow(name, sizeLabel, ctx) {
199
193
  // Measured pessimistically. A display name is user text and may hold ambiguous-width
200
194
  // characters, so a gap sized by the narrow measure is a gap that overflows on the terminals
201
195
  // this contract exists for.
202
- const used = maxCells(name) + maxCells(budget) + 2;
196
+ const used = maxCells(name) + maxCells(sizeLabel) + 2;
203
197
  const gap = used <= ctx.ui.width ? " ".repeat(ctx.ui.width - used) : " ";
204
- return ` ${ctx.ui.bold(name)}${gap}${paint(budget)}`;
198
+ return ` ${ctx.ui.bold(name)}${gap}${ctx.ui.dim(sizeLabel)}`;
205
199
  }
206
200
  /**
207
201
  * Safeword and register on one row. Plain text, because it is folded before it is
@@ -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, sizeWarning } from "./compile.js";
9
+ import { applyPlanned, planArtifact, planConfig } from "./compile.js";
10
10
  import { planChange, rawReadCheck } from "../wiring/apply.js";
11
- import { budget, combination, humanText, nextStep } from "../ui/format.js";
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
@@ -70,7 +70,7 @@ export async function runUse(ctx, args) {
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(budget(size, ctx.t))}`] : []),
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
  "",
@@ -95,7 +95,6 @@ export async function runUse(ctx, args) {
95
95
  warnings: [
96
96
  ...(notice.roleplay ? [notice.roleplay] : []),
97
97
  ...checkCombos(config.axes, config.persona, config.speech_level).map((w) => combination(w, ctx.t)),
98
- ...sizeWarning(size, ctx.t),
99
98
  ],
100
99
  };
101
100
  }
@@ -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;
@@ -1,4 +1,4 @@
1
- import { ANCHORS, AXIS_ORDER, CAP_CHARS, CONTROLS, FORMAT_COMPOSITION, GENERATION_COMMENT, PERSONA, REGISTER_COMPOSITION, SCOPE_HEADER, SPEECH_LEVEL, STYLE_MARKER, TARGET_CHARS, } from "./anchors.js";
1
+ import { ANCHORS, AXIS_ORDER, CONTROLS, FORMAT_COMPOSITION, GENERATION_COMMENT, PERSONA, REGISTER_COMPOSITION, SCOPE_HEADER, SPEECH_LEVEL, STYLE_MARKER, } from "./anchors.js";
2
2
  import { composites, PERSONA_DESCRIPTION_MAX } from "./combos.js";
3
3
  const UNSPECIFIED = "unspecified";
4
4
  const isSet = (v) => typeof v === "string" && v.trim() !== "" && v !== UNSPECIFIED;
@@ -81,9 +81,5 @@ export function measure(artifact) {
81
81
  const chars = [...artifact].length;
82
82
  return {
83
83
  chars,
84
- target: TARGET_CHARS,
85
- cap: CAP_CHARS,
86
- overTarget: chars > TARGET_CHARS,
87
- overCap: chars > CAP_CHARS,
88
84
  };
89
85
  }
package/dist/core/i18n.js CHANGED
@@ -97,7 +97,7 @@ const enMessages = {
97
97
  introduction: "Choose the profile name, slug, address and role, then adjust all twelve axes. Enter keeps the shown value; \"\" clears an optional field. Final approval saves and applies the whole profile.",
98
98
  name: "Display name",
99
99
  slug: "Slug (lowercase letters, digits, dashes)",
100
- user: "Your background or preferences (up to 200 characters)",
100
+ user: "Your role or background (up to 200 characters)",
101
101
  agent: "Agent role (up to 200 characters)",
102
102
  register: "Register (auto, polite, plain)",
103
103
  safeword: "Safeword",
@@ -144,6 +144,8 @@ const enMessages = {
144
144
  addressNeedsPair: "address needs both terms: tonedial address <self name> <user name>. " +
145
145
  'Use "" in each position to clear them. In a terminal, run it without arguments to be prompted.',
146
146
  askSelf: "What should the agent call itself?",
147
+ rolesNeedsPair: 'persona needs both descriptions: tonedial persona <agent role> <user role>. Use "" to clear a description, or run without arguments in a terminal.',
148
+ rolesIntroduction: 'Set the agent\'s role and your role or background. Enter keeps the current value; "" clears a role. Changes apply after both answers.',
147
149
  askUser: "What should it call you?",
148
150
  addressKept: (current) => `current ${current} · Enter keeps it`,
149
151
  addressNone: "currently unset · Enter keeps it unset",
@@ -169,15 +171,12 @@ const enMessages = {
169
171
  register: (level) => `register ${level}`,
170
172
  },
171
173
  artifact: {
172
- budget: (chars, cap) => `TONE.md ${chars} / ${cap}`,
174
+ size: (chars) => `TONE.md ${chars} characters`,
173
175
  wouldWrite: "would write",
174
176
  wrote: "written",
175
- overCap: (chars, cap) => `TONE.md: ${chars} characters · over ${cap} maximum`,
176
- overTarget: (chars, target) => `TONE.md: ${chars} characters · over ${target} target`,
177
177
  notWritten: "TONE.md is missing. Run tonedial compile",
178
178
  notSetUp: "not initialized. Run tonedial init",
179
179
  movedFromDefault: "differs from ToneDial's default",
180
- recompiled: (chars, cap) => `TONE.md ${chars.toLocaleString("en")} / ${cap.toLocaleString("en")}`,
181
180
  defaultMarker: "marks an axis that differs from ToneDial's default, not the agent's native tone.",
182
181
  riskMarker: "! risky tone",
183
182
  riskHeading: "Advisory risky levels:",
@@ -428,8 +427,9 @@ const enMessages = {
428
427
  init: "set up and refresh bundled files",
429
428
  set: "change axes, role or settings · bare opens dial",
430
429
  address: "set names for each party · bare prompts",
430
+ persona: "set agent and user roles · bare prompts",
431
431
  reset: "reset an axis · bare resets all axes",
432
- show: "current settings · slug previews saved axes",
432
+ show: "current settings · slug previews a saved profile",
433
433
  test: "try saved settings or a preset in a new conversation",
434
434
  save: "save the current tone as a profile",
435
435
  create: "create and apply a preset · bare guides setup",
@@ -745,7 +745,7 @@ const koMessages = {
745
745
  introduction: "프로필 이름·슬러그·호칭·역할을 입력한 뒤 열두 축을 조절합니다. Enter를 누르면 표시된 값을 유지하며, '\"\"'를 입력하면 선택 항목을 지웁니다. 마지막 확인에서 전체 프로필을 저장하고 적용합니다.",
746
746
  name: "표시 이름",
747
747
  slug: "슬러그(영문 소문자·숫자·붙임표)",
748
- user: "사용자 배경이나 선호(최대 200자)",
748
+ user: "사용자 역할·배경(최대 200자)",
749
749
  agent: "에이전트 역할(최대 200자)",
750
750
  register: "말씨(auto, polite, plain)",
751
751
  safeword: "세이프워드",
@@ -792,6 +792,8 @@ const koMessages = {
792
792
  addressNeedsPair: "address에는 두 호칭이 모두 필요합니다: tonedial address <자칭> <사용자 호칭>. " +
793
793
  '지우려면 각 자리에 ""을 넣으세요. 터미널에서는 인자 없이 실행해 입력할 수도 있습니다.',
794
794
  askSelf: "에이전트가 자기를 뭐라고 부를까요?",
795
+ rolesNeedsPair: 'persona에는 두 역할 설명이 필요합니다: tonedial persona <에이전트 역할> <사용자 역할>. 지울 항목에는 ""를 넣으세요. 터미널에서는 인자 없이 실행해 입력할 수도 있습니다.',
796
+ rolesIntroduction: '에이전트와 사용자의 역할·배경을 설정합니다. Enter를 누르면 현재 값을 유지하고, ""를 입력하면 해당 역할을 지웁니다. 두 항목을 입력한 뒤 한 번에 적용합니다.',
795
797
  askUser: "에이전트가 사용자를 뭐라고 부를까요?",
796
798
  addressKept: (current) => `현재 ${current} · 엔터를 누르면 유지`,
797
799
  addressNone: "현재 설정 없음 · 엔터를 누르면 유지",
@@ -820,15 +822,12 @@ const koMessages = {
820
822
  register: (level) => `말씨 ${level}`,
821
823
  },
822
824
  artifact: {
823
- budget: (chars, cap) => `TONE.md ${chars} / ${cap}`,
825
+ size: (chars) => `TONE.md ${chars}자`,
824
826
  wouldWrite: "기록 예정",
825
827
  wrote: "기록됨",
826
- overCap: (chars, cap) => `TONE.md: ${chars}자 · 최대 ${cap}자 초과`,
827
- overTarget: (chars, target) => `TONE.md: ${chars}자 · 목표 ${target}자 초과`,
828
828
  notWritten: "TONE.md가 없습니다. tonedial compile을 실행해 주세요",
829
829
  notSetUp: "초기 설정이 없습니다. tonedial init을 실행해 주세요",
830
830
  movedFromDefault: "ToneDial 기본값과 다름",
831
- recompiled: (chars, cap) => `TONE.md ${chars.toLocaleString("ko")} / ${cap.toLocaleString("ko")}`,
832
831
  defaultMarker: "ToneDial 기본값과 다른 축을 표시합니다. 에이전트 원래 말투와 비교하지 않습니다.",
833
832
  riskMarker: "! 위험 말투",
834
833
  riskHeading: "주의할 레벨:",
@@ -1079,8 +1078,9 @@ const koMessages = {
1079
1078
  init: "초기 설정과 기본 파일 갱신",
1080
1079
  set: "축·역할·설정 변경 · 인자 없으면 다이얼",
1081
1080
  address: "서로 부르는 말 · 인자 없으면 질문",
1081
+ persona: "에이전트·사용자 역할 · 인자 없으면 질문",
1082
1082
  reset: "축을 기본값으로 · 축을 안 주면 전부",
1083
- show: "현재 설정 · 슬러그를 주면 저장한 축",
1083
+ show: "현재 설정 · 슬러그를 주면 저장한 프로필",
1084
1084
  test: "저장된 설정이나 프리셋으로 새 대화 시작",
1085
1085
  save: "현재 말투를 프로필로 저장",
1086
1086
  create: "프리셋 생성·적용 · 인자 없으면 안내",
@@ -86,7 +86,7 @@ const messages = {
86
86
  introduction: "Elige nombre, identificador, formas de trato y rol; después ajusta los doce ejes. Enter conserva el valor; \"\" borra un campo opcional. La aprobación final guarda y aplica todo el perfil.",
87
87
  name: "Nombre visible",
88
88
  slug: "Identificador (minúsculas, dígitos y guiones)",
89
- user: "Tu contexto o preferencias (hasta 200 caracteres)",
89
+ user: "Tu rol o contexto (hasta 200 caracteres)",
90
90
  agent: "Rol del agente (hasta 200 caracteres)",
91
91
  register: "Registro (auto, polite, plain)",
92
92
  safeword: "Palabra clave",
@@ -133,6 +133,8 @@ const messages = {
133
133
  addressNeedsPair: "address necesita ambos nombres: tonedial address <self name> <user name>. " +
134
134
  'Usa una cadena vacía en cada posición para borrarla. Sin argumentos en un terminal, se te preguntará.',
135
135
  askSelf: "¿Cómo debe llamarse el agente a sí mismo?",
136
+ rolesNeedsPair: 'persona necesita ambas descripciones: tonedial persona <rol del agente> <rol del usuario>. Usa "" para borrar una descripción o ejecútalo sin argumentos en una terminal.',
137
+ rolesIntroduction: 'Define el rol del agente y tu rol o contexto. Enter conserva el valor actual; "" borra el rol. Los cambios se aplican al terminar ambas respuestas.',
136
138
  askUser: "¿Cómo debe llamarte?",
137
139
  addressKept: (current) => `actual ${current} · Enter lo conserva`,
138
140
  addressNone: "Sin definir · Enter lo mantiene así",
@@ -158,15 +160,12 @@ const messages = {
158
160
  register: (level) => `registro ${level}`,
159
161
  },
160
162
  artifact: {
161
- budget: (chars, cap) => `TONE.md ${chars} / ${cap}`,
163
+ size: (chars) => `TONE.md ${chars} caracteres`,
162
164
  wouldWrite: "se escribiría",
163
165
  wrote: "escrito",
164
- overCap: (chars, cap) => `TONE.md: ${chars} caracteres · supera el máximo de ${cap}`,
165
- overTarget: (chars, target) => `TONE.md: ${chars} caracteres · supera el objetivo de ${target}`,
166
166
  notWritten: "Falta TONE.md. Ejecuta tonedial compile",
167
167
  notSetUp: "Sin inicializar. Ejecuta tonedial init",
168
168
  movedFromDefault: "difiere del valor predeterminado de ToneDial",
169
- recompiled: (chars, cap) => `TONE.md ${chars.toLocaleString("es")} / ${cap.toLocaleString("es")}`,
170
169
  defaultMarker: "marca un eje distinto del valor predeterminado de ToneDial, no del tono nativo del agente.",
171
170
  riskMarker: "! riesgo",
172
171
  riskHeading: "Niveles de riesgo orientativos:",
@@ -413,8 +412,9 @@ const messages = {
413
412
  init: "configurar y actualizar archivos incluidos",
414
413
  set: "cambiar ejes, rol o ajustes · sin argumentos abre el dial",
415
414
  address: "definir los nombres de ambas partes · sin argumentos pregunta",
415
+ persona: "definir roles del agente y usuario · sin argumentos pregunta",
416
416
  reset: "restablecer un eje · sin argumentos restablece todos",
417
- show: "ajustes actuales · con identificador previsualiza lo guardado",
417
+ show: "ajustes actuales · con identificador previsualiza un perfil guardado",
418
418
  test: "probar los ajustes o un preajuste en una conversación nueva",
419
419
  save: "guardar el tono actual como perfil",
420
420
  create: "crear y aplicar un preajuste · sin argumentos guía la configuración",
@@ -86,7 +86,7 @@ const messages = {
86
86
  introduction: "プロファイル名、スラッグ、呼称、役割を選び、12個の軸を調整します。Enterで表示値を保持し、\"\"で任意項目を消去します。最後の承認で全設定を保存・適用します。",
87
87
  name: "表示名",
88
88
  slug: "スラッグ(英小文字・数字・ハイフン)",
89
- user: "あなたの背景や希望(200文字まで)",
89
+ user: "あなたの役割や背景(200文字まで)",
90
90
  agent: "エージェントの役割(200文字まで)",
91
91
  register: "敬語区分(auto, polite, plain)",
92
92
  safeword: "合言葉",
@@ -133,6 +133,8 @@ const messages = {
133
133
  addressNeedsPair: "address には両方の呼称が必要です:tonedial address <self name> <user name>。" +
134
134
  '各位置に空文字列を指定すると消去できます。ターミナルでは引数なしで実行すると入力できます。',
135
135
  askSelf: "エージェントは自分をどう呼びますか?",
136
+ rolesNeedsPair: 'persona には両方の説明が必要です: tonedial persona <エージェントの役割> <ユーザーの役割>。消す項目には "" を指定してください。端末では引数なしで対話入力できます。',
137
+ rolesIntroduction: 'エージェントの役割と、あなたの役割や背景を設定します。Enter で現在の値を維持し、"" で役割を消去します。両方の入力が終わってからまとめて適用します。',
136
138
  askUser: "あなたをどう呼びますか?",
137
139
  addressKept: (current) => `現在 ${current} · Enterで保持`,
138
140
  addressNone: "未設定 · Enterで未設定のまま保持",
@@ -158,15 +160,12 @@ const messages = {
158
160
  register: (level) => `敬語区分 ${level}`,
159
161
  },
160
162
  artifact: {
161
- budget: (chars, cap) => `TONE.md ${chars} / ${cap}`,
163
+ size: (chars) => `TONE.md ${chars}文字`,
162
164
  wouldWrite: "書き込み予定",
163
165
  wrote: "書き込み済み",
164
- overCap: (chars, cap) => `TONE.md: ${chars}文字 · 上限 ${cap} を超えています`,
165
- overTarget: (chars, target) => `TONE.md: ${chars}文字 · 目標 ${target} を超えています`,
166
166
  notWritten: "TONE.md がありません。tonedial compile を実行してください",
167
167
  notSetUp: "未初期化です。tonedial init を実行してください",
168
168
  movedFromDefault: "ToneDialの既定値と異なります",
169
- recompiled: (chars, cap) => `TONE.md ${chars.toLocaleString("ja")} / ${cap.toLocaleString("ja")}`,
170
169
  defaultMarker: "ToneDialの既定値と異なる軸を示します。エージェント本来の口調との比較ではありません。",
171
170
  riskMarker: "! 要注意",
172
171
  riskHeading: "注意が必要なレベル(参考):",
@@ -413,8 +412,9 @@ const messages = {
413
412
  init: "初期設定と同梱ファイルの更新",
414
413
  set: "軸・役割・設定を変更 · 引数なしでダイヤル",
415
414
  address: "双方の呼称を設定 · 引数なしで質問",
415
+ persona: "エージェントとユーザーの役割を設定 · 引数なしで質問",
416
416
  reset: "軸を初期化 · 引数なしで全軸",
417
- show: "現在の設定 · スラッグ指定で保存値をプレビュー",
417
+ show: "現在の設定 · スラッグ指定で保存済みプロフィールをプレビュー",
418
418
  test: "新しい会話で現在の設定かプリセットを試す",
419
419
  save: "現在の口調をプロファイルに保存",
420
420
  create: "プリセットを作成・適用 · 引数なしで案内",
@@ -86,7 +86,7 @@ const messages = {
86
86
  introduction: "选择配置名称、标识名、称呼和角色,再调整全部十二个语气轴。Enter 保留显示值;\"\" 清空可选项。最终确认后保存并应用完整配置。",
87
87
  name: "显示名称",
88
88
  slug: "标识名(小写字母、数字、连字符)",
89
- user: "你的背景或偏好(最多 200 字符)",
89
+ user: "你的角色或背景(最多 200 字符)",
90
90
  agent: "代理角色(最多 200 字符)",
91
91
  register: "敬语形式(auto, polite, plain)",
92
92
  safeword: "安全词",
@@ -133,6 +133,8 @@ const messages = {
133
133
  addressNeedsPair: "address 需要两个称呼:tonedial address <self name> <user name>。" +
134
134
  '每个位置使用空字符串即可清空。在终端不带参数运行可交互输入。',
135
135
  askSelf: "代理如何自称?",
136
+ rolesNeedsPair: 'persona 需要两项描述:tonedial persona <代理角色> <用户角色>。用 "" 清除对应描述,也可在终端中不带参数交互输入。',
137
+ rolesIntroduction: '设置代理的角色,以及你的角色或背景。按 Enter 保留当前值,输入 "" 清除该角色。两项输入完成后统一应用。',
136
138
  askUser: "代理该如何称呼你?",
137
139
  addressKept: (current) => `当前 ${current} · Enter 保留`,
138
140
  addressNone: "当前未设置 · Enter 保持未设置",
@@ -158,15 +160,12 @@ const messages = {
158
160
  register: (level) => `敬语形式 ${level}`,
159
161
  },
160
162
  artifact: {
161
- budget: (chars, cap) => `TONE.md ${chars} / ${cap}`,
163
+ size: (chars) => `TONE.md ${chars} 字符`,
162
164
  wouldWrite: "将写入",
163
165
  wrote: "已写入",
164
- overCap: (chars, cap) => `TONE.md:${chars} 字符 · 超过上限 ${cap}`,
165
- overTarget: (chars, target) => `TONE.md:${chars} 字符 · 超过目标 ${target}`,
166
166
  notWritten: "缺少 TONE.md,请运行 tonedial compile",
167
167
  notSetUp: "尚未初始化,请运行 tonedial init",
168
168
  movedFromDefault: "与 ToneDial 默认值不同",
169
- recompiled: (chars, cap) => `TONE.md ${chars.toLocaleString("zh")} / ${cap.toLocaleString("zh")}`,
170
169
  defaultMarker: "标记与 ToneDial 默认值不同的语气轴,不代表与代理原生语气的差异。",
171
170
  riskMarker: "! 风险语气",
172
171
  riskHeading: "风险等级提示:",
@@ -413,8 +412,9 @@ const messages = {
413
412
  init: "初始化并刷新内置文件",
414
413
  set: "修改语气轴、角色或设置 · 无参数打开面板",
415
414
  address: "设置双方称呼 · 无参数则交互输入",
415
+ persona: "设置代理和用户角色 · 无参数则交互输入",
416
416
  reset: "重置语气轴 · 无参数重置全部",
417
- show: "当前设置 · 指定标识名预览保存值",
417
+ show: "当前设置 · 指定标识名预览已保存的配置档案",
418
418
  test: "在新对话中试用当前设置或预设",
419
419
  save: "将当前语气保存为配置",
420
420
  create: "创建并应用预设 · 无参数则引导设置",
@@ -11,7 +11,7 @@ import { captureRawEntry, caseRenameMatchesCurrent, isNoop, planChange, planEntr
11
11
  import { BUNDLED, builtinPresetDir, LEGACY_BUILTIN_SLUGS, RETIRED_PRESETS } from "./presets.js";
12
12
  import { SKILL_MD, SKILL_NAME, SKILL_TRAITS_MD } from "./skill.js";
13
13
  // Bump for changed generated content; freshness also checks each managed seed's bytes/provenance.
14
- export const SEED_VERSION = 15;
14
+ export const SEED_VERSION = 16;
15
15
  /** The state-root operating boundaries. The compiler owns the current tone separately. */
16
16
  export const GUIDE_MD = `${GUIDE_FORMAT_MARKER}
17
17
  ToneDial changes manner only; it never expands task scope or permissions.
@@ -73,7 +73,7 @@ export function seedFiles(paths, selection) {
73
73
  };
74
74
  const native = [
75
75
  reader,
76
- { id: "skill/body", version: 15, path: join(paths.skill, SKILL_NAME, "SKILL.md"), label: `skill/${SKILL_NAME}/SKILL.md`, contents: SKILL_MD },
76
+ { id: "skill/body", version: 16, path: join(paths.skill, SKILL_NAME, "SKILL.md"), label: `skill/${SKILL_NAME}/SKILL.md`, contents: SKILL_MD },
77
77
  {
78
78
  id: "skill/traits",
79
79
  version: 8,
@@ -5,6 +5,7 @@ export const SKILL_NAME = "tonedial";
5
5
  /** The current command envelope, shared by both installed control entry points. */
6
6
  const JSON_MD = `Require zero exit status and a JSON result with \`ok: true\` and \`schema: 3\`
7
7
  for every successful CLI step before continuing. Parse JSON rather than scraping human output.
8
+ \`chars\` reports the current TONE.md Unicode character count; no size target or maximum applies.
8
9
  Schema 3 removes guard and ceiling fields and reports advisory \`riskAxes\`; persisted config
9
10
  remains schema 2. Custom profiles use preset schema 2; legacy schema 1 and journal formats
10
11
  remain supported. Never send \`guards\` or
@@ -98,6 +99,7 @@ interpolating it into shell code.
98
99
  | What to call the user only | \`tonedial set "persona.user_ref=friend" --json\` |
99
100
  | What to call yourself only | \`tonedial set "persona.self_ref=guide" --json\` |
100
101
  | Both address terms, self first and user second | \`tonedial address "assistant" "reader" --json\` |
102
+ | Both role descriptions, agent first and user second | \`tonedial persona "Fictional database reviewer" "Developer" --json\` |
101
103
  | Persona age band | \`tonedial set persona.age=30s --json\` |
102
104
  | Persona gender | \`tonedial set persona.gender=neutral --json\` |
103
105
  | User background and preferences | \`tonedial set "persona.user=Developer who likes concise technical answers" --json\` |
@@ -113,6 +115,8 @@ An empty value clears only the named persona field or safeword, for example
113
115
  \`persona.user\` describes the user; \`persona.agent\` describes the agent's fictional role.
114
116
  Each accepts at most 200 Unicode code points; never silently truncate a longer request.
115
117
  Preserve the other fields and ask the user to shorten text rejected by the CLI.
118
+ In a terminal, bare \`tonedial persona\` asks for both descriptions; Enter keeps a value and \`""\` clears it.
119
+ \`show\` displays both descriptions and their character counts; a slug selects the saved profile.
116
120
  Agent experience and specialty are roleplay, not verified credentials or tool capabilities.
117
121
  Both descriptions lose to axes and task/permission rules, and may influence content.
118
122
  A persona note expresses manner or role, loses to axes, and may also influence
@@ -242,7 +246,7 @@ ${SETUP_MD}
242
246
 
243
247
  ## Apply it in this session
244
248
 
245
- Successful \`set\`, \`address\`, \`use\`, \`reset\`, \`save\`, and approved \`create\` commands recompile \`TONE.md\`; that is a saved state
249
+ Successful \`set\`, \`address\`, \`persona\`, \`use\`, \`reset\`, \`save\`, and approved \`create\` commands recompile \`TONE.md\`; that is a saved state
246
250
  change, not evidence of a session reload.
247
251
 
248
252
  ${RELOAD_MD}
@@ -399,7 +403,7 @@ Profanity grades actual swearing; bravado grades effort claims. The CLI carries
399
403
  * command name stays `/tone` even though the tool is `tonedial`: it is what a person types
400
404
  * mid-sentence, and the two extra characters are paid every time.
401
405
  */
402
- export const TONE_COMMAND_VERSION = 7;
406
+ export const TONE_COMMAND_VERSION = 8;
403
407
  export const TONE_COMMAND_MD = `---
404
408
  description: Check, change, or reload this agent's tone, preset, or ToneDial settings
405
409
  ---
package/dist/ui/format.js CHANGED
@@ -61,8 +61,8 @@ export function num(n) {
61
61
  return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
62
62
  }
63
63
  /** The one shape the artifact's size is reported in, by every command that recompiles. */
64
- export function budget(size, t) {
65
- return t.artifact.budget(num(size.chars), num(size.cap));
64
+ export function artifactSize(size, t) {
65
+ return t.artifact.size(num(size.chars));
66
66
  }
67
67
  /** Render combination warnings with the same layer distinction in every command. */
68
68
  export function combination(w, t) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tonedial",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Set a coding agent's manner once and keep it across every project.",
5
5
  "license": "MIT",
6
6
  "type": "module",