tonedial 0.1.6 → 0.1.7

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/README.zh.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **给编程代理一个角色。**
4
4
 
5
- 选择角色,调整十二个语气轴,保存一套能在不同项目中复用的语气。
5
+ ToneDial 是本地 CLI,用于调整编程代理的语气并在不同项目中复用。
6
6
 
7
7
  **7 个角色 · 12 个语气轴 · 5 种 CLI 语言 · 零运行时依赖**
8
8
 
@@ -19,9 +19,36 @@
19
19
 
20
20
  **[查看全部 7 个角色 →](https://github.com/awesometic/tonedial/blob/main/docs/EXAMPLES.zh.md)**
21
21
 
22
- ## 试试这样用?
22
+ ## 同一模型,更精简的计划
23
23
 
24
- 完成[设置](#开始试用)后,选择一组配置。
24
+ 在 `gpt-5.6-sol` / `high` 下,一个全新会话使用 Codex 默认设置,另一个使用下方完整的 `vibe-coder` 配置。
25
+ `verbosity=1` 只是该配置的一部分,并非唯一差异。
26
+
27
+ **指令**
28
+
29
+ > I want to make ToneDial easier for first-time users. The README should get them from installation through `tonedial init` to their first preview with `tonedial test codex --preset <slug>`, and the CLI should make that next step obvious after `init`. Give me a prioritized three-step improvement plan for the README and first-run CLI flow.
30
+
31
+ <img src="https://raw.githubusercontent.com/awesometic/tonedial/main/docs/media/readme/zh/verbosity-comparison.svg" alt="GPT-5.6 Sol high 使用 Codex 默认设置和完整 vibe-coder 配置回答同一条 ToneDial 改进计划指令;配置后的英文回复从 148 个词缩短到 82 个词" width="760">
32
+
33
+ <sub>同一指令 · 每个新会话的首次完整回复 · 英文回复 148 个词 → 82 个词。这是一次示例性对比,并非只隔离单个轴的基准测试。回复用于展示语气差异;产品行为和命令请以下方指南为准。 <a href="https://github.com/awesometic/tonedial/blob/main/docs/media/readme/verbosity-comparison.json">输入与生成记录</a>.</sub>
34
+
35
+ ## 开始试用
36
+
37
+ 需要 **Node 24 或更高版本**和 npm。
38
+
39
+ ```bash
40
+ npm install --global tonedial
41
+ tonedial init --tools none --global --lang zh
42
+ tonedial set
43
+ ```
44
+
45
+ 设置时会先展示文件变更并请求批准。无需连接代理即可先打开面板试用。
46
+
47
+ [连接代理](https://github.com/awesometic/tonedial/blob/main/docs/USAGE.md#connecting-an-agent) · [使用临时设置试用](https://github.com/awesometic/tonedial/blob/main/docs/TESTING.md#isolated-candidate)
48
+
49
+ ## 复制一种语气
50
+
51
+ 复制完整配置,或把它作为自定义设置的起点。
25
52
  每条命令都会保存并应用完整配置;`--auto` 会批准显示的更改。
26
53
 
27
54
  实际 `tonedial test codex` 回复 · `gpt-6.1-sol` / `high` · 每组配置保留问题的首次回答 · 仅对话,不编辑文件。 [输入与生成记录](https://github.com/awesometic/tonedial/blob/main/docs/media/readme/zh/use-cases.json).
@@ -35,7 +62,7 @@
35
62
  话少、直接选定推荐方案,简短汇报做完了什么、还剩什么的编程搭档。
36
63
 
37
64
  <details>
38
- <summary>复制这套配置</summary>
65
+ <summary>配置与实际回复</summary>
39
66
 
40
67
  ```bash
41
68
  tonedial create '彻底放手的氛围编程' --slug vibe-coder --auto \
@@ -51,8 +78,6 @@ tonedial create '彻底放手的氛围编程' --slug vibe-coder --auto \
51
78
 
52
79
  试试这个语气: `tonedial test codex --preset vibe-coder`
53
80
 
54
- </details>
55
-
56
81
  **Codex**
57
82
 
58
83
  > 老板,我选把 `null` 和 `undefined` 转为空字符串,避免 `trim()` 报错:
@@ -63,13 +88,14 @@ tonedial create '彻底放手的氛围编程' --slug vibe-coder --auto \
63
88
  > }
64
89
  > ```
65
90
 
91
+ </details>
66
92
 
67
93
  ### 需要安慰的时候
68
94
 
69
95
  温柔、爱夸人,偶尔说几句安慰的话,像个让人安心的搭档。
70
96
 
71
97
  <details>
72
- <summary>复制这套配置</summary>
98
+ <summary>配置与实际回复</summary>
73
99
 
74
100
  ```bash
75
101
  tonedial create '需要安慰的时候' --slug steady-companion --auto \
@@ -85,8 +111,6 @@ tonedial create '需要安慰的时候' --slug steady-companion --auto \
85
111
 
86
112
  试试这个语气: `tonedial test codex --preset steady-companion`
87
113
 
88
- </details>
89
-
90
114
  **Codex**
91
115
 
92
116
  > 我的选择:搭档,把 `null` 和 `undefined` 当作空字符串,其他输入保留原来的处理方式。
@@ -101,13 +125,14 @@ tonedial create '需要安慰的时候' --slug steady-companion --auto \
101
125
  >
102
126
  > 你贴出的函数很清楚,指出触发条件很细致,聚焦一个方案很明智,要求直接替换的版本也很周到。辛苦了,我陪你拆问题,慢慢来,放心先喘口气——这个空值小坑已经有块补丁了。
103
127
 
128
+ </details>
104
129
 
105
130
  ### 就喜欢嘴狠的
106
131
 
107
132
  脏话照说,嘲讽照给,活也照样干利索的狠角色。
108
133
 
109
134
  <details>
110
- <summary>复制这套配置</summary>
135
+ <summary>配置与实际回复</summary>
111
136
 
112
137
  ```bash
113
138
  tonedial create '就喜欢嘴狠的' --slug rough-but-reliable --auto \
@@ -123,8 +148,6 @@ tonedial create '就喜欢嘴狠的' --slug rough-but-reliable --auto \
123
148
 
124
149
  试试这个语气: `tonedial test codex --preset rough-but-reliable`
125
150
 
126
- </details>
127
-
128
151
  **Codex**
129
152
 
130
153
  > 喂,老子选空值返回空字符串这招,一行就把这他妈的崩溃点收拾利索:
@@ -137,23 +160,11 @@ tonedial create '就喜欢嘴狠的' --slug rough-but-reliable --auto \
137
160
  >
138
161
  > `null` 或 `undefined` 返回 `""`,`" Ada "` 返回 `"Ada"`。
139
162
 
163
+ </details>
164
+
140
165
  Codex 开始对话后,输入上面的问题即可。
141
166
  模型与推理强度由 Codex 设置,与 ToneDial 配置分开。
142
167
 
143
- ## 开始试用
144
-
145
- 需要 **Node 24 或更高版本**和 npm。
146
-
147
- ```bash
148
- npm install --global tonedial
149
- tonedial init --tools none --global --lang zh
150
- tonedial set
151
- ```
152
-
153
- 设置时会先展示文件变更并请求批准。无需连接代理即可先打开面板试用。
154
-
155
- [连接代理](https://github.com/awesometic/tonedial/blob/main/docs/USAGE.md#connecting-an-agent) · [使用临时设置试用](https://github.com/awesometic/tonedial/blob/main/docs/TESTING.md#isolated-candidate)
156
-
157
168
  ## 选择你的角色
158
169
 
159
170
  | 预设 | 你 → 代理 |
package/dist/cli.js CHANGED
@@ -30,13 +30,14 @@ import { runEject } from "./commands/eject.js";
30
30
  import { runInit } from "./commands/init.js";
31
31
  import { runUnwire, runWire } from "./commands/wire.js";
32
32
  import { runTest } from "./commands/test.js";
33
+ import { runOff, runOn } from "./commands/toggle.js";
33
34
  export const JSON_SCHEMA = 3;
34
35
  /**
35
36
  * Stated here rather than read from `package.json`, because reading a package-relative file at
36
37
  * run time is the one thing that breaks all three binary packagers at once. A test asserts this
37
38
  * equals the manifest, which is the cheap half of the trade.
38
39
  */
39
- export const VERSION = "0.1.6";
40
+ export const VERSION = "0.1.7";
40
41
  const FLAG_NAMES = ["project", "check", "explain", "global", "here", "migrateLegacy", "auto", "dryRun", "fix", "rollback", "quarantineOperation", "fromOperation", "fromPreset", "restorePreset", "preset", "slug", "acceptUsage", "lang", "commands", "tools"];
41
42
  /** Command-line spelling for each flag, where it differs from the field name. */
42
43
  const FLAG_SPELLING = {
@@ -49,6 +50,8 @@ const FLAG_SPELLING = {
49
50
  };
50
51
  /** Each command declares its accepted flags and positional limit; null means unlimited. */
51
52
  const COMMANDS = {
53
+ on: { run: runOn, flags: [], max: 0, context: "state" },
54
+ off: { run: runOff, flags: [], max: 0, context: "state" },
52
55
  set: { run: runSet, flags: [], max: null, context: "state" },
53
56
  get: { run: runGet, flags: [], max: 1 },
54
57
  test: { run: runTest, flags: ["project", "dryRun", "preset", "acceptUsage"], max: 1 },
@@ -81,6 +84,8 @@ export const COMMAND_NAMES = Object.keys(COMMANDS);
81
84
  /** Intent groups share one command inventory between compact and full help. */
82
85
  const COMMAND_GROUPS = [
83
86
  ["tone", [
87
+ ["on", "on"],
88
+ ["off", "off"],
84
89
  ["set", "set <key>=<value> …"],
85
90
  ["get", "get [key]"],
86
91
  ["address", "address [self user]"],
@@ -112,6 +117,8 @@ const COMMAND_GROUPS = [
112
117
  ];
113
118
  const COMMAND_SYNTAX = COMMAND_GROUPS.flatMap(([, rows]) => rows);
114
119
  const COMMAND_EXAMPLES = {
120
+ on: ["tonedial on"],
121
+ off: ["tonedial off"],
115
122
  set: ["tonedial set", "tonedial set warmth=5 critique=4", "tonedial set persona.agent=reviewer"],
116
123
  get: ["tonedial get", "tonedial get language", "tonedial get speech_level --json"],
117
124
  test: ["tonedial test codex --dry-run", "tonedial test claude --project .", "tonedial test codex --preset my-tone --accept-usage"],
@@ -274,16 +281,12 @@ function columns(env, stream, isTTY) {
274
281
  return isTTY ? stream.columns : undefined;
275
282
  }
276
283
  /**
277
- * Words a person reaches for when they want to stop, none of which is a command.
278
- *
279
- * There is no off switch to give them, so the answer names the acts that do exist and says so.
284
+ * Words a person reaches for when they want to stop, but which are not command spellings.
280
285
  *
281
286
  * A deliberate ceiling: the list is guessed, not measured — nobody has reported typing any of
282
- * them. Replace it with a measured one when a user reports a word that is not here, or drop it
283
- * if the answer turns out to be the list of seventeen after all. The cost of being wrong is one
284
- * string; the cost of being right is that `tonedial off` stops dead-ending in that list.
287
+ * them. Replace it with a measured one when a user reports a word that is not here.
285
288
  */
286
- const STOPPING = ["off", "disable", "stop", "quit", "uninstall", "remove", "delete"];
289
+ const STOPPING = ["disable", "stop", "quit", "uninstall", "remove", "delete"];
287
290
  export async function run(argv, env = process.env, cwd = process.cwd(), isTTY = process.stdout.isTTY === true, isErrTTY = process.stderr.isTTY === true) {
288
291
  const options = {
289
292
  json: { type: "boolean", default: false },
@@ -387,7 +390,7 @@ export async function run(argv, env = process.env, cwd = process.cwd(), isTTY =
387
390
  // one: `stop` is two edits from `show` and `delete` three from `reset`, so the match would
388
391
  // answer a person trying to leave with the command they were leaving.
389
392
  if (STOPPING.includes(name))
390
- return fail(asJson, say.errors.noOffSwitch, 2, uiErr);
393
+ return fail(asJson, say.errors.stoppingHelp, 2, uiErr);
391
394
  return fail(asJson, say.errors.notACommand(asJson ? name : humanText(name), COMMAND_NAMES, nearest(name, COMMAND_NAMES)), 2, uiErr);
392
395
  }
393
396
  const flags = {
@@ -107,7 +107,7 @@ export async function runCompile(ctx) {
107
107
  ` ${ctx.ui.dim(artifactSize(size, ctx.t))}`,
108
108
  ...nextStep(check && !current ? "tonedial compile" : "tonedial show", ctx.ui),
109
109
  ],
110
- json: { path: ctx.paths.tone, agentsPath: ctx.paths.agents, written: !check, ...size },
110
+ json: { path: ctx.paths.tone, agentsPath: ctx.paths.agents, enabled: config.enabled, written: !check, ...size },
111
111
  warnings: [],
112
112
  };
113
113
  }
@@ -11,6 +11,7 @@ import { join, relative } from "node:path";
11
11
  import { AGENT_TOOLS, CONTEXT_ARTIFACT_FILENAME, CONTEXT_GUIDE_FILENAME, UPDATE_COMMAND, connectionScopeFlag, onPath, readerPath, resolveAgentPaths, shimScript, installShape, canonicalDestination, shellQuote } from "../core/paths.js";
12
12
  import { SKILL_NAME } from "../core/skill.js";
13
13
  import { GUIDE_FORMAT_MARKER } from "../core/reader.js";
14
+ import { DELIVERY_OFF_MARKER } from "../core/anchors.js";
14
15
  import { inspect as inspectSkill, skillPath } from "../wiring/skill.js";
15
16
  import { candidateRcPaths, pathLineState, rcPath, withPathLine } from "../wiring/shell.js";
16
17
  import { SEED_VERSION, inspectContextLayout, inspectSeeds, recordedSeedSelection, } from "../core/seeds.js";
@@ -94,7 +95,9 @@ function readerHealth(ctx, config, configRead, seeds, pairRequired) {
94
95
  temporary = mkdtempSync(join(tmpdir(), "tonedial-reader-check-"));
95
96
  temporary = realpathSync(temporary);
96
97
  const guide = `${GUIDE_FORMAT_MARKER}\nToneDial diagnostic guide.\n`;
97
- const artifact = "ToneDial diagnostic tone — 한글 🌿.\n";
98
+ const artifact = config.enabled
99
+ ? "ToneDial diagnostic tone — 한글 🌿.\n"
100
+ : `<!-- generated by tonedial · preset: unsaved -->\n<!-- tonedial-style:1 verbosity=3 care=3 -->\n${DELIVERY_OFF_MARKER}\nToneDial diagnostic tone — 한글 🌿.\n`;
98
101
  writeFileSync(join(temporary, CONTEXT_GUIDE_FILENAME), guide, { flag: "wx", mode: 0o600 });
99
102
  writeFileSync(join(temporary, CONTEXT_ARTIFACT_FILENAME), artifact, { flag: "wx", mode: 0o600 });
100
103
  const body = guide + "\n\n" + artifact;
@@ -110,7 +113,8 @@ function readerHealth(ctx, config, configRead, seeds, pairRequired) {
110
113
  maxBuffer: 8192,
111
114
  });
112
115
  const expected = provider === "antigravity"
113
- ? JSON.stringify({ injectSteps: [{ ephemeralMessage: body }] }) + "\n" : body;
116
+ ? JSON.stringify({ injectSteps: config.enabled ? [{ ephemeralMessage: body }] : [] }) + "\n"
117
+ : config.enabled ? body : "";
114
118
  if (probe.error || probe.status !== 0 || probe.stderr !== "" || probe.stdout !== expected)
115
119
  return result;
116
120
  }
@@ -33,9 +33,11 @@ export function runGet(ctx, args) {
33
33
  return [` ${ctx.ui.accent(padEnd(key, column))} ${first ?? ""}`,
34
34
  ...rest.map(line => `${" ".repeat(column + 4)}${line}`)];
35
35
  });
36
+ if (requested === undefined && !config.enabled)
37
+ lines.unshift(` ${ctx.ui.bold(ctx.t.delivery.status(false))}`, "");
36
38
  if (requested !== undefined) {
37
39
  const copy = catalogue(isLanguage(config.language) ? config.language : detectLanguage(ctx.env));
38
40
  lines.push("", ...settingHelp(requested, copy).flatMap(hint => wrap(hint, ctx.ui.width - 2).map(line => ` ${ctx.ui.dim(line)}`)));
39
41
  }
40
- return { lines, json: { values } };
42
+ return { lines, json: { enabled: config.enabled, values } };
41
43
  }
@@ -18,7 +18,7 @@ import { wrap } from "../ui/width.js";
18
18
  import { parseToolSelection } from "../wiring/targets.js";
19
19
  import { riskWarnings } from "../ui/format.js";
20
20
  const CONFIG_MAX_BYTES = 1024 * 1024;
21
- const scalarFields = ["preset", "unsaved", "safeword", "speech_level", "language"];
21
+ const scalarFields = ["enabled", "preset", "unsaved", "safeword", "speech_level", "language"];
22
22
  const personaFields = ["age", "gender", "note", "user", "agent"];
23
23
  function capturedText(capture) {
24
24
  if (capture.type !== "regular" || capture.bytes === null)
@@ -69,7 +69,8 @@ export function runShow(ctx, args) {
69
69
  const gauges = [gaugeHeader(ui, ctx.t), ...gaugeBlock(axes, ui)];
70
70
  const gaugeWidth = Math.max(...gauges.map((line) => maxCells(stripVTControlCharacters(line))));
71
71
  const detailWidth = ui.terminal && ui.width >= 119 ? ui.width - gaugeWidth - 4 : ui.width - 2;
72
- const details = [ui.bold(ctx.t.artifact.settingsHeading)];
72
+ const details = [ui.bold(ctx.t.artifact.settingsHeading),
73
+ ...(config.enabled ? [] : [ctx.t.delivery.status(false)])];
73
74
  if (personaSet) {
74
75
  const bits = [
75
76
  age ? humanText(age) : null,
@@ -163,6 +164,7 @@ export function runShow(ctx, args) {
163
164
  json: {
164
165
  preset: preview ? preview.slug : config.preset,
165
166
  preview: preview !== null,
167
+ enabled: config.enabled,
166
168
  activeKnown: preview !== null || !activeConflict,
167
169
  // Layered rather than flattened: the skill has to tell a forbidden contradiction from a
168
170
  // cosmetic one, and a joined string cannot carry that.
@@ -58,7 +58,8 @@ export async function runTest(ctx, args) {
58
58
  }
59
59
  input = toCompileInput(withPreset(config, preview));
60
60
  }
61
- const tone = compile(input);
61
+ // A test is an explicit one-shot preview, independent of the persistent native delivery switch.
62
+ const tone = compile({ ...input, enabled: true });
62
63
  const installed = PROVIDERS.map((provider) => ({ provider, program: onPath(provider, ctx.env) }))
63
64
  .filter((entry) => entry.program !== null);
64
65
  let selected = requested === undefined ? undefined : installed.find((entry) => entry.provider === requested);
@@ -0,0 +1,23 @@
1
+ // Pause or resume native tone delivery while preserving the saved settings and full artifact.
2
+ import { serializeConfig } from "../core/config.js";
3
+ import { wrap } from "../ui/width.js";
4
+ import { applyPlanned, planArtifact, planConfig } from "./compile.js";
5
+ async function setDelivery(ctx, enabled) {
6
+ const { change: configChange, config } = planConfig(ctx);
7
+ const changed = config.enabled !== enabled;
8
+ config.enabled = enabled;
9
+ const { change: toneChange, size } = planArtifact(ctx, config);
10
+ const finalConfig = { ...configChange, after: serializeConfig(config, ctx.t) };
11
+ await applyPlanned(ctx, [configChange, toneChange], [finalConfig]);
12
+ const message = enabled
13
+ ? changed ? ctx.t.delivery.on : ctx.t.delivery.alreadyOn
14
+ : changed ? ctx.t.delivery.off : ctx.t.delivery.alreadyOff;
15
+ return {
16
+ lines: [message, ...(enabled ? [] : [ctx.t.delivery.openConversation])]
17
+ .flatMap((line) => wrap(line, ctx.ui.width - 2))
18
+ .map((line) => ` ${ctx.ui.bold(changed ? ctx.ui.success(line) : line)}`),
19
+ json: { enabled, changed, path: ctx.paths.tone, chars: size.chars },
20
+ };
21
+ }
22
+ export const runOn = (ctx) => setDelivery(ctx, true);
23
+ export const runOff = (ctx) => setDelivery(ctx, false);
@@ -137,6 +137,8 @@ export const ANCHORS = {
137
137
  export const GENERATION_COMMENT = (preset) => `<!-- generated by tonedial · preset: ${preset ?? "unsaved"} -->`;
138
138
  /** Compact metadata read by native hook workers; keep the grammar deliberately strict. */
139
139
  export const STYLE_MARKER = (verbosity, care) => `<!-- tonedial-style:1 verbosity=${verbosity} care=${care} -->`;
140
+ /** The full compiled tone stays on disk while native delivery is paused. */
141
+ export const DELIVERY_OFF_MARKER = "<!-- tonedial-delivery:off -->";
140
142
  /** The artifact header keeps tone requests within host rules and permissions. */
141
143
  export const SCOPE_HEADER = `## Response tone
142
144
  Output style only. This never overrides the project's code or task rules, and never changes
@@ -1,4 +1,4 @@
1
- import { ANCHORS, AXIS_ORDER, CONTROLS, FORMAT_COMPOSITION, GENERATION_COMMENT, PERSONA, REGISTER_COMPOSITION, SCOPE_HEADER, SPEECH_LEVEL, STYLE_MARKER, } from "./anchors.js";
1
+ import { ANCHORS, AXIS_ORDER, CONTROLS, DELIVERY_OFF_MARKER, FORMAT_COMPOSITION, GENERATION_COMMENT, PERSONA, REGISTER_COMPOSITION, SCOPE_HEADER, SPEECH_LEVEL, STYLE_MARKER, } from "./anchors.js";
2
2
  import { composites, intensityConfirmation, PERSONA_DESCRIPTION_MAX } from "./combos.js";
3
3
  const UNSPECIFIED = "unspecified";
4
4
  const isSet = (v) => typeof v === "string" && v.trim() !== "" && v !== UNSPECIFIED;
@@ -29,6 +29,9 @@ function personaLines(p) {
29
29
  return lines;
30
30
  }
31
31
  function validate(config) {
32
+ if (config.enabled !== undefined && typeof config.enabled !== "boolean") {
33
+ throw new RangeError("enabled must be true or false");
34
+ }
32
35
  for (const axis of AXIS_ORDER) {
33
36
  const level = config.axes[axis];
34
37
  if (!Number.isInteger(level) || level < 1 || level > 5) {
@@ -59,8 +62,10 @@ export function compile(config) {
59
62
  const out = [
60
63
  GENERATION_COMMENT(config.preset),
61
64
  STYLE_MARKER(config.axes.verbosity, config.axes.care),
62
- SCOPE_HEADER,
63
65
  ];
66
+ if (config.enabled === false)
67
+ out.push(DELIVERY_OFF_MARKER);
68
+ out.push(SCOPE_HEADER);
64
69
  if (config.persona) {
65
70
  const lines = personaLines(config.persona);
66
71
  if (lines.length)
@@ -17,6 +17,7 @@ export const RESOURCE_TYPES = ["file", "directory", "symlink"];
17
17
  export function defaultConfig() {
18
18
  return {
19
19
  schema: CONFIG_SCHEMA,
20
+ enabled: true,
20
21
  preset: null,
21
22
  unsaved: false,
22
23
  axes: { ...AXIS_DEFAULT },
@@ -512,7 +513,7 @@ export function parseConfig(text, t = catalogue("en").cli) {
512
513
  // An unknown key is refused by name rather than silently dropped on the next write. This
513
514
  // file is user-editable, and a tool that eats what a person typed has no business asking
514
515
  // them to trust it with someone else's files later.
515
- const KNOWN = ["schema", "preset", "unsaved", "axes", "persona", "safeword", "guards", "fixed_width",
516
+ const KNOWN = ["schema", "enabled", "preset", "unsaved", "axes", "persona", "safeword", "guards", "fixed_width",
516
517
  "speech_level", "language", "seedVersion", "managedSeeds", "wiring"];
517
518
  const unknown = Object.keys(raw).filter((k) => !KNOWN.includes(k));
518
519
  if (unknown.length)
@@ -541,6 +542,7 @@ export function parseConfig(text, t = catalogue("en").cli) {
541
542
  }
542
543
  }
543
544
  const common = {
545
+ enabled: raw.enabled === undefined ? true : requiredBoolean(raw.enabled, "enabled", t),
544
546
  preset: asString(raw.preset, "preset", t),
545
547
  unsaved: raw.unsaved === true,
546
548
  axes: readAxes(raw.axes, t),
@@ -569,6 +571,9 @@ export function serializeConfig(config, t = catalogue("en").cli) {
569
571
  const wiring = readWiringV2(current.wiring, t);
570
572
  const text = JSON.stringify({
571
573
  schema: CONFIG_SCHEMA,
574
+ // On is the historical default. Omit it so ordinary writes retain the released shape;
575
+ // only the meaningful paused state needs a persisted byte.
576
+ ...(current.enabled ? {} : { enabled: false }),
572
577
  preset: current.preset,
573
578
  unsaved: current.unsaved,
574
579
  axes,
@@ -599,6 +604,7 @@ export function prepareApprovedMigration(legacy, t = catalogue("en").cli) {
599
604
  return {
600
605
  config: {
601
606
  schema: CONFIG_SCHEMA,
607
+ enabled: legacy.enabled,
602
608
  preset: legacy.preset,
603
609
  unsaved: legacy.unsaved,
604
610
  axes: { ...legacy.axes },
@@ -617,7 +623,7 @@ export function prepareApprovedMigration(legacy, t = catalogue("en").cli) {
617
623
  },
618
624
  };
619
625
  }
620
- export const USER_SETTING_KEYS = ["preset", "unsaved", "axes", "persona", "safeword", "speech_level", "language"];
626
+ export const USER_SETTING_KEYS = ["enabled", "preset", "unsaved", "axes", "persona", "safeword", "speech_level", "language"];
621
627
  /** Recover supported user values only. Opaque delivery metadata never becomes new authority. */
622
628
  export function recoverSettings(text, t = catalogue("en").cli) {
623
629
  let raw;
package/dist/core/i18n.js CHANGED
@@ -37,6 +37,14 @@ const enMessages = {
37
37
  launchFailed: (provider) => `Could not start ${provider}. Check its installation and try again.`,
38
38
  selectionCancelled: "No provider selected.",
39
39
  },
40
+ delivery: {
41
+ on: "ToneDial on · delivery marker cleared",
42
+ off: "ToneDial off · delivery marker saved · settings and TONE.md kept",
43
+ alreadyOn: "ToneDial already on · no changes",
44
+ alreadyOff: "ToneDial already off · no changes",
45
+ openConversation: "Open conversations may retain an earlier tone. Use a new conversation for a clean no-ToneDial comparison.",
46
+ status: (enabled) => `ToneDial ${enabled ? "on" : "off"}`,
47
+ },
40
48
  axis: {
41
49
  range: (axis, given) => `${axis}: expected 1–5 (got ${given})`,
42
50
  unknown: (name, nearest) => `${name}: not an axis${nearest ? `. Did you mean ${nearest}?` : ""}`,
@@ -443,6 +451,8 @@ const enMessages = {
443
451
  firstSteps: "Set up your tone, then inspect it:",
444
452
  examples: "Examples",
445
453
  commands: {
454
+ on: "resume saved tone delivery",
455
+ off: "pause tone delivery · keep settings and TONE.md",
446
456
  init: "set up and refresh bundled files",
447
457
  set: "change axes, role or settings · bare opens dial",
448
458
  get: "read current setting values · key adds its meaning and choices",
@@ -501,10 +511,9 @@ const enMessages = {
501
511
  errors: {
502
512
  notACommand: (name, known, nearest) => `${name} is not a command.${nearest ? ` Did you mean ${nearest}?` : ""} ` +
503
513
  `Available: ${known.join(", ")}`,
504
- noOffSwitch: "There is no off switch. If set, a safeword requests one plain answer; tonedial show reports " +
505
- "whether one exists. tonedial reset restores axis defaults. tonedial eject --global or " +
506
- "--project <path> disconnects that scope. Once all connections are gone, " +
507
- "npm uninstall -g tonedial removes the command.",
514
+ stoppingHelp: "Use tonedial off to pause tone delivery without deleting settings; tonedial on resumes it. " +
515
+ "tonedial reset restores axis defaults. tonedial eject --global or --project <path> " +
516
+ "disconnects that scope. Once all connections are gone, npm uninstall -g tonedial removes the command.",
508
517
  notAnOption: (flag) => `${flag}: not an option. List: tonedial --help`,
509
518
  flagNotAccepted: (command, flag) => `${command}: does not accept --${flag}`,
510
519
  tooManyArguments: (command, max, got) => `${command}: maximum ${max} argument${max === 1 ? "" : "s"}, got ${got}`,
@@ -700,6 +709,14 @@ const koMessages = {
700
709
  launchFailed: (provider) => `${provider}를 시작하지 못했습니다. 설치 상태를 확인하고 다시 시도해 주세요.`,
701
710
  selectionCancelled: "도구를 선택하지 않았습니다.",
702
711
  },
712
+ delivery: {
713
+ on: "ToneDial 켜짐 · 전달 중지 마커 제거",
714
+ off: "ToneDial 꺼짐 · 전달 중지 마커 저장 · 설정과 TONE.md 유지",
715
+ alreadyOn: "ToneDial이 이미 켜져 있음 · 변경 없음",
716
+ alreadyOff: "ToneDial이 이미 꺼져 있음 · 변경 없음",
717
+ openConversation: "열린 대화에는 이전 말투가 남을 수 있습니다. ToneDial이 없는 상태를 제대로 비교하려면 새 대화를 여세요.",
718
+ status: (enabled) => `ToneDial ${enabled ? "켜짐" : "꺼짐"}`,
719
+ },
703
720
  axis: {
704
721
  range: (axis, given) => `${axis}: 1~5만 쓸 수 있습니다(입력: ${given})`,
705
722
  unknown: (name, nearest) => `${name}: 축이 아닙니다${nearest ? `. ${nearest} 말씀이신가요?` : ""}`,
@@ -1109,6 +1126,8 @@ const koMessages = {
1109
1126
  firstSteps: "말투를 설정한 뒤 확인하세요:",
1110
1127
  examples: "사용 예",
1111
1128
  commands: {
1129
+ on: "저장한 말투 전달 재개",
1130
+ off: "말투 전달 일시정지 · 설정·TONE.md 유지",
1112
1131
  init: "초기 설정과 기본 파일 갱신",
1113
1132
  set: "축·역할·설정 변경 · 인자 없으면 다이얼",
1114
1133
  get: "현재 설정값 조회 · 키를 지정하면 의미와 선택지도 표시",
@@ -1167,10 +1186,9 @@ const koMessages = {
1167
1186
  errors: {
1168
1187
  notACommand: (name, known, nearest) => `${name}: 명령이 아닙니다.${nearest ? ` ${nearest} 말씀이신가요?` : ""} ` +
1169
1188
  `사용 가능: ${known.join(", ")}`,
1170
- noOffSwitch: "끄는 스위치는 없습니다. 세이프워드를 정했다면 한 메시지만 담백하게 답해 달라고 요청할 수 " +
1171
- "있습니다. 설정 여부는 tonedial show에서 확인하세요. tonedial reset은 축을 기본값으로 " +
1172
- "돌립니다. tonedial eject --global이나 --project <path>는 그 범위의 연결을 해제합니다. " +
1173
- "연결을 모두 해제한 뒤 npm uninstall -g tonedial로 명령을 삭제할 수 있습니다.",
1189
+ stoppingHelp: "설정을 지우지 않고 말투 전달만 멈추려면 tonedial off를 실행하세요. tonedial on으로 다시 켤 수 있습니다. " +
1190
+ "tonedial reset은 축을 기본값으로 돌립니다. tonedial eject --global이나 --project <path>는 " +
1191
+ "해당 범위의 연결을 해제합니다. 연결을 모두 해제한 뒤 npm uninstall -g tonedial로 명령을 삭제할 수 있습니다.",
1174
1192
  notAnOption: (flag) => `${flag}: 옵션이 아닙니다. 목록: tonedial --help`,
1175
1193
  flagNotAccepted: (command, flag) => `${command}: --${flag} 옵션을 받지 않습니다`,
1176
1194
  tooManyArguments: (command, max, got) => `${command}: 인자는 최대 ${max}개지만 ${got}개를 받았습니다`,
@@ -1,5 +1,5 @@
1
1
  // Decode known current and historical English artifact bodies. Unknown prose is never a data source.
2
- import { ANCHORS, AXIS_DEFAULT, AXIS_ORDER, CONTROLS, FORMAT_COMPOSITION, PERSONA, REGISTER_COMPOSITION, SCOPE_HEADER, SPEECH_LEVEL, } from "./anchors.js";
2
+ import { ANCHORS, AXIS_DEFAULT, AXIS_ORDER, CONTROLS, DELIVERY_OFF_MARKER, FORMAT_COMPOSITION, PERSONA, REGISTER_COMPOSITION, SCOPE_HEADER, SPEECH_LEVEL, } from "./anchors.js";
3
3
  import { AGE_BANDS, GENDERS, defaultConfig, recoverSettings, safewordRejection, } from "./config.js";
4
4
  import { composites, intensityConfirmation } from "./combos.js";
5
5
  // Historical producer text is decoder evidence only, never a current tone instruction.
@@ -355,11 +355,16 @@ export function recoverLegacyTone(text, t) {
355
355
  const styleMarker = /^<!-- tonedial-style:1 verbosity=([1-5]) care=([1-5]) -->\n/.exec(afterGeneration);
356
356
  if (afterGeneration.startsWith("<!-- tonedial-style:") && styleMarker === null)
357
357
  return null;
358
- const prefix = `${marker[0]}${styleMarker?.[0] ?? ""}${SCOPE_HEADER}\n`;
358
+ const afterStyle = afterGeneration.slice(styleMarker?.[0].length ?? 0);
359
+ const deliveryMarker = afterStyle.startsWith(`${DELIVERY_OFF_MARKER}\n`) ? `${DELIVERY_OFF_MARKER}\n` : "";
360
+ if (afterStyle.startsWith("<!-- tonedial-delivery:") && deliveryMarker === "")
361
+ return null;
362
+ const prefix = `${marker[0]}${styleMarker?.[0] ?? ""}${deliveryMarker}${SCOPE_HEADER}\n`;
359
363
  if (!source.startsWith(prefix))
360
364
  return null;
361
365
  let body = source.slice(prefix.length);
362
366
  const config = defaultConfig();
367
+ config.enabled = deliveryMarker !== "" ? false : true;
363
368
  const unknown = [];
364
369
  if (body.startsWith("### Persona\n")) {
365
370
  const end = body.indexOf("\n### Rules\n");
@@ -26,6 +26,14 @@ const messages = {
26
26
  launchFailed: (provider) => `No se pudo iniciar ${provider}. Comprueba su instalación y vuelve a intentarlo.`,
27
27
  selectionCancelled: "No se eligió ningún proveedor.",
28
28
  },
29
+ delivery: {
30
+ on: "ToneDial activado · marcador de pausa eliminado",
31
+ off: "ToneDial desactivado · marcador de pausa guardado · se conservan los ajustes y TONE.md",
32
+ alreadyOn: "La entrega del tono ya está activada · sin cambios",
33
+ alreadyOff: "La entrega del tono ya está desactivada · sin cambios",
34
+ openConversation: "Las conversaciones abiertas pueden conservar el tono anterior. Usa una conversación nueva para comparar el estado sin ToneDial.",
35
+ status: (enabled) => `Entrega del tono ${enabled ? "activada" : "desactivada"}`,
36
+ },
29
37
  axis: {
30
38
  range: (axis, given) => `${axis}: se esperaba 1–5 (se recibió ${given})`,
31
39
  unknown: (name, nearest) => `${name}: no es un eje${nearest ? `. ¿Querías decir ${nearest}?` : ""}`,
@@ -428,6 +436,8 @@ const messages = {
428
436
  firstSteps: "Configura el tono y revísalo:",
429
437
  examples: "Ejemplos",
430
438
  commands: {
439
+ on: "reanudar la entrega del tono guardado",
440
+ off: "pausar la entrega · conservar ajustes y TONE.md",
431
441
  init: "configurar y actualizar archivos incluidos",
432
442
  set: "cambiar ejes, rol o ajustes · sin argumentos abre el dial",
433
443
  get: "consultar valores actuales · una clave muestra su significado y opciones",
@@ -486,10 +496,9 @@ const messages = {
486
496
  errors: {
487
497
  notACommand: (name, known, nearest) => `${name} no es un comando.${nearest ? ` ¿Querías decir ${nearest}?` : ""} ` +
488
498
  `Disponibles: ${known.join(", ")}`,
489
- noOffSwitch: "No hay interruptor de apagado. Si está definida, la palabra clave pide una respuesta sencilla; tonedial show indica " +
490
- "si existe. tonedial reset restablece los ejes. tonedial eject --global o " +
491
- "--project <path> desconecta ese ámbito. Tras quitar todas las conexiones, " +
492
- "npm uninstall -g tonedial elimina el comando.",
499
+ stoppingHelp: "Usa tonedial off para pausar la entrega sin borrar los ajustes y tonedial on para reanudarla. " +
500
+ "tonedial reset restablece los ejes. tonedial eject --global o --project <path> desconecta ese ámbito. " +
501
+ "Tras quitar todas las conexiones, npm uninstall -g tonedial elimina el comando.",
493
502
  notAnOption: (flag) => `${flag}: no es una opción. Lista: tonedial --help`,
494
503
  flagNotAccepted: (command, flag) => `${command}: no admite --${flag}`,
495
504
  tooManyArguments: (command, max, got) => `${command}: máximo ${max} argumento${max === 1 ? "" : "s"}; se recibieron ${got}`,
@@ -26,6 +26,14 @@ const messages = {
26
26
  launchFailed: (provider) => `${provider} を起動できません。インストール状況を確認して再試行してください。`,
27
27
  selectionCancelled: "プロバイダーが選択されていません。",
28
28
  },
29
+ delivery: {
30
+ on: "ToneDialをオンにしました · 配信停止マーカーを削除",
31
+ off: "ToneDialをオフにしました · 配信停止マーカーを保存 · 設定とTONE.mdは保持",
32
+ alreadyOn: "口調の配信はすでにオンです · 変更なし",
33
+ alreadyOff: "口調の配信はすでにオフです · 変更なし",
34
+ openConversation: "開いている会話には以前の口調が残る場合があります。ToneDialなしの状態と正確に比較するには新しい会話を使ってください。",
35
+ status: (enabled) => `口調の配信:${enabled ? "オン" : "オフ"}`,
36
+ },
29
37
  axis: {
30
38
  range: (axis, given) => `${axis}: 1–5を指定してください(入力:${given})`,
31
39
  unknown: (name, nearest) => `${name}: 軸ではありません${nearest ? `。${nearest} のことですか?` : ""}`,
@@ -428,6 +436,8 @@ const messages = {
428
436
  firstSteps: "口調を設定し、確認します:",
429
437
  examples: "使用例",
430
438
  commands: {
439
+ on: "保存した口調の配信を再開",
440
+ off: "口調の配信を一時停止 · 設定とTONE.mdは保持",
431
441
  init: "初期設定と同梱ファイルの更新",
432
442
  set: "軸・役割・設定を変更 · 引数なしでダイヤル",
433
443
  get: "現在の設定値を確認 · キー指定で意味と選択肢も表示",
@@ -486,10 +496,9 @@ const messages = {
486
496
  errors: {
487
497
  notACommand: (name, known, nearest) => `${name} はコマンドではありません。${nearest ? `${nearest} のことですか?` : ""} ` +
488
498
  `選択肢:${known.join(", ")}`,
489
- noOffSwitch: "オフスイッチはありません。合言葉は一度だけ平易な回答を求めます。設定の有無は tonedial show で確認できます。" +
490
- "tonedial reset は軸の既定値を復元します。tonedial eject --global または " +
491
- "--project <path> でその範囲を解除します。全接続を解除した後、" +
492
- "npm uninstall -g tonedial でコマンドを削除できます。",
499
+ stoppingHelp: "設定を削除せずに口調の配信を止めるには tonedial off を実行し、再開するには tonedial on を実行してください。" +
500
+ "tonedial reset は軸の既定値を復元します。tonedial eject --global または --project <path> でその範囲を解除します。" +
501
+ "全接続を解除した後、npm uninstall -g tonedial でコマンドを削除できます。",
493
502
  notAnOption: (flag) => `${flag}: オプションではありません。一覧:tonedial --help`,
494
503
  flagNotAccepted: (command, flag) => `${command}: --${flag} は使えません`,
495
504
  tooManyArguments: (command, max, got) => `${command}: 引数は最大 ${max}個、入力は ${got}個です`,