@krtclcdy/tersio 2.24.0 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +28 -24
  2. package/cli/banner.js +7 -5
  3. package/cli/common.js +14 -13
  4. package/cli/currency.js +2 -7
  5. package/cli/dashboard.js +220 -13
  6. package/cli/doctor-fix.js +99 -29
  7. package/cli/doctor.js +32 -35
  8. package/cli/hosts.js +23 -26
  9. package/cli/install.js +228 -115
  10. package/cli/interactive.js +65 -21
  11. package/cli/manifest.js +8 -4
  12. package/cli/profile.js +49 -16
  13. package/cli/reset.js +3 -7
  14. package/cli/rtk-wiring.js +29 -32
  15. package/cli/settings.js +40 -16
  16. package/cli/uninstall.js +79 -29
  17. package/cli/update.js +27 -20
  18. package/cli/usage.js +54 -19
  19. package/dashboard/dist/index.html +18 -18
  20. package/extensions/ai-addons-updater/index.js +75 -20
  21. package/extensions/ai-addons-updater/index.ts +67 -23
  22. package/extensions/caveman-session/index.js +33 -75
  23. package/extensions/caveman-session/index.ts +35 -77
  24. package/extensions/combo-toggle/index.js +40 -62
  25. package/extensions/combo-toggle/index.ts +38 -72
  26. package/extensions/lib/utils.js +64 -15
  27. package/extensions/lib/utils.ts +62 -15
  28. package/extensions/opencode/index.js +104 -0
  29. package/extensions/opencode/server.js +190 -0
  30. package/extensions/opencode/server.ts +200 -0
  31. package/extensions/rtk-session/index.js +38 -57
  32. package/extensions/rtk-session/index.ts +36 -55
  33. package/extensions/shared/carbon.js +4 -10
  34. package/extensions/shared/carbon.ts +5 -10
  35. package/extensions/shared/host.js +95 -56
  36. package/extensions/shared/host.ts +106 -60
  37. package/extensions/shared/plugin-settings.js +6 -18
  38. package/extensions/shared/plugin-settings.ts +6 -18
  39. package/extensions/shared/pricing.js +79 -27
  40. package/extensions/shared/pricing.ts +80 -28
  41. package/extensions/shared/rtk-gain.js +4 -11
  42. package/extensions/shared/rtk-gain.ts +4 -11
  43. package/extensions/shared/session-state.js +28 -39
  44. package/extensions/shared/session-state.ts +29 -38
  45. package/extensions/shared/status.js +38 -0
  46. package/extensions/shared/status.ts +45 -0
  47. package/extensions/shared/types.js +1 -2
  48. package/extensions/shared/types.ts +2 -2
  49. package/extensions/shared/usage-ledger.js +312 -57
  50. package/extensions/shared/usage-ledger.ts +321 -61
  51. package/extensions/shared/usage-store.js +354 -38
  52. package/extensions/shared/usage-store.ts +360 -43
  53. package/extensions/tersio-commands/index.js +5 -14
  54. package/extensions/tersio-commands/index.ts +5 -14
  55. package/package.json +12 -7
  56. package/tersio.js +4 -6
  57. package/cli/agent-hosts.js +0 -42
  58. package/cli/agents.js +0 -266
  59. package/cli/layer.js +0 -24
  60. package/cli/omp-layer.js +0 -51
  61. package/cli/pi-layer.js +0 -37
  62. package/cli/pi-wiring.js +0 -92
  63. package/extensions/shared/omp-prompt.js +0 -12
@@ -1,6 +1,6 @@
1
1
  // cli/interactive.ts — TTY layer: readline, Clack spinners, selects, task phases.
2
2
  import readline from 'node:readline';
3
- import { cancel as clackCancel, confirm as clackConfirm, select as clackSelect, spinner as clackSpinner, tasks as clackTasks } from '@clack/prompts';
3
+ import { cancel as clackCancel, confirm as clackConfirm, log as clackLog, select as clackSelect, spinner as clackSpinner, tasks as clackTasks } from '@clack/prompts';
4
4
  import { execP } from './common.js';
5
5
  const RL = readline.createInterface({ input: process.stdin, output: process.stdout });
6
6
  let rlOpen = true;
@@ -26,13 +26,11 @@ function tty() {
26
26
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
27
27
  }
28
28
  let spinnerDepth = 0;
29
- // Run async work under a TTY-only timer spinner. The spinner is cleared before
30
- // the caller prints its normal result line, preserving locked output shapes.
29
+ // Run async work under a TTY-only timer spinner. The spinner is cleared before the caller prints its normal result line, preserving locked output shapes.
31
30
  async function withInteractiveSpinner(message, work) {
32
31
  if (!tty() || spinnerDepth > 0)
33
32
  return work(() => { });
34
- // Clack manages stdin itself. Close the legacy question interface before its
35
- // first use; non-interactive callers never reach this branch.
33
+ // Clack manages stdin itself. Close the legacy question interface before its first use; non-interactive callers never reach this branch.
36
34
  closeRL();
37
35
  const active = clackSpinner({ indicator: 'timer' });
38
36
  active.start(message);
@@ -49,26 +47,33 @@ async function askInteractiveChoice(message, options, initialValue) {
49
47
  if (!tty())
50
48
  return { status: 'unavailable' };
51
49
  closeRL();
52
- const choice = await clackSelect({ message, options, initialValue });
53
- if (typeof choice !== 'string') {
54
- clackCancel('Aborted.');
55
- return { status: 'cancelled' };
50
+ try {
51
+ const choice = await clackSelect({ message, options, initialValue });
52
+ if (typeof choice !== 'string') {
53
+ clackCancel('Aborted.');
54
+ return { status: 'cancelled' };
55
+ }
56
+ return { status: 'selected', value: choice };
57
+ }
58
+ finally {
56
59
  }
57
- return { status: 'selected', value: choice };
58
60
  }
59
61
  async function askInteractiveConfirm(message, initialValue = true) {
60
62
  if (!tty())
61
63
  return { status: 'unavailable' };
62
64
  closeRL();
63
- const answer = await clackConfirm({ message, initialValue });
64
- if (typeof answer !== 'boolean') {
65
- clackCancel('Aborted.');
66
- return { status: 'cancelled' };
65
+ try {
66
+ const answer = await clackConfirm({ message, initialValue });
67
+ if (typeof answer !== 'boolean') {
68
+ clackCancel('Aborted.');
69
+ return { status: 'cancelled' };
70
+ }
71
+ return { status: 'confirmed', value: answer };
72
+ }
73
+ finally {
67
74
  }
68
- return { status: 'confirmed', value: answer };
69
75
  }
70
- // Confirm a destructive run: a Clack dialog at a terminal, a plain prompt in a
71
- // pipe or script. False means the user declined or aborted.
76
+ // Confirm a destructive run: a Clack dialog at a terminal, a plain prompt in a pipe or script. False means the user declined or aborted.
72
77
  async function confirmDestructive(message) {
73
78
  if (tty()) {
74
79
  closeRL();
@@ -93,17 +98,56 @@ async function confirmDestructive(message) {
93
98
  }
94
99
  return true;
95
100
  }
96
- // Run collecting work under one TTY-only Clack task. Callers print after the
97
- // task completes, keeping normal output out of the spinner animation.
101
+ // Run collecting work under one TTY-only Clack task. Callers print after the task completes, keeping normal output out of the spinner animation.
98
102
  async function runInteractivePhase(title, collect) {
99
103
  if (!tty())
100
104
  return collect();
101
105
  closeRL();
102
106
  let result;
103
- await clackTasks([{ title, task: async () => { result = await collect(); } }]);
107
+ try {
108
+ await clackTasks([{ title, task: async () => { result = await collect(); } }]);
109
+ }
110
+ finally {
111
+ }
112
+ return result;
113
+ }
114
+ const TAG_KIND = {
115
+ fail: 'error', warn: 'warn', hint: 'info', ok: 'success', note: 'message',
116
+ write: 'step', rm: 'step', migrate: 'step', skip: 'info', 'dry-run': 'info',
117
+ };
118
+ function sayTagged(line) {
119
+ const m = /^\s*\[([\w-]+)\]\s?([\s\S]*)$/.exec(line);
120
+ const kind = (m && TAG_KIND[m[1]]) || 'info';
121
+ if (tty() && m)
122
+ clackLog[kind](m[2]);
123
+ else
124
+ console.log(line);
125
+ }
126
+ // One live-updating task line instead of a spinner per step.
127
+ async function withInteractiveTask(title, work) {
128
+ if (!tty())
129
+ return work(() => { });
130
+ closeRL();
131
+ let result;
132
+ const spin = clackSpinner();
133
+ spin.start(title);
134
+ try {
135
+ result = await work((message) => spin.message(message));
136
+ spin.stop('done');
137
+ }
138
+ catch (e) {
139
+ spin.error(shortSpinnerError(e));
140
+ throw e;
141
+ }
142
+ finally {
143
+ }
104
144
  return result;
105
145
  }
146
+ function shortSpinnerError(e) {
147
+ const msg = e instanceof Error ? e.message : String(e);
148
+ return (msg.split('\n')[0] || 'failed').slice(0, 120);
149
+ }
106
150
  async function execNetwork(label, cmd, args, opts = {}) {
107
151
  return withInteractiveSpinner(label, () => execP(cmd, args, opts));
108
152
  }
109
- export { ask, closeRL, tty, withInteractiveSpinner, execNetwork, askInteractiveChoice, askInteractiveConfirm, confirmDestructive, runInteractivePhase, };
153
+ export { ask, closeRL, tty, withInteractiveSpinner, withInteractiveTask, execNetwork, sayTagged, askInteractiveChoice, askInteractiveConfirm, confirmDestructive, runInteractivePhase, };
package/cli/manifest.js CHANGED
@@ -1,14 +1,17 @@
1
- // cli/manifest.ts — the one list of what an install writes into a host's
2
- // extension tree. The installer, `doctor --fix extensions`, the doctor's
3
- // per-host tree row, and the loadability test all read it, so none of them
4
- // can drift from what actually ships.
1
+ // Single list of install writes, shared by installer, doctor, and tests.
5
2
  import path from 'node:path';
6
3
  import { fileURLToPath } from 'node:url';
7
4
  export const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
5
+ /** Root entrypoint OpenCode resolves for the installed tree; re-exports the versioned entry beside it. Built by join so the path never reads as a relative import (repo-integrity scans literals). */
6
+ const SERVER_SPEC = `./${['opencode', 'server.ts'].join('/')}`;
7
+ export const OPENCODE_SERVER_SHIM = `// Generated by \`tersio install --host opencode\`: OpenCode resolves <pluginDir>/server.ts.
8
+ export { default } from "${SERVER_SPEC}";
9
+ `;
8
10
  /** Every file in the tree, as paths relative to the extensions directory. */
9
11
  export const TREE_FILES = [
10
12
  'shared/host.ts',
11
13
  'shared/session-state.ts',
14
+ 'shared/status.ts',
12
15
  'shared/types.ts',
13
16
  'shared/plugin-settings.ts',
14
17
  'shared/usage-ledger.ts',
@@ -21,6 +24,7 @@ export const TREE_FILES = [
21
24
  'combo-toggle/index.ts',
22
25
  'tersio-commands/index.ts',
23
26
  'ai-addons-updater/index.ts',
27
+ 'opencode/server.ts',
24
28
  ];
25
29
  export function sourcePath(relative) {
26
30
  return path.join(EXT_DIR, ...relative.split('/'));
package/cli/profile.js CHANGED
@@ -1,14 +1,20 @@
1
- // cli/profile.ts — session-start defaults profile (combo/caveman/rtk/ponytail)
2
- // plus the display-currency default for usage/dashboard reports.
3
- // Stored in ~/.tersio/settings.json, which every host reads. Extracted from
4
- // cli/install.ts so both install and settings share it.
5
- import { existsSync, promises as fs } from 'node:fs';
1
+ // cli/profile.ts — session-start defaults and the display-currency default,
2
+ import { existsSync, promises as fs, readFileSync } from 'node:fs';
6
3
  import path from 'node:path';
7
4
  import { CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, PACKAGE_NAME, PONYTAIL_DEFAULTS, verbose, } from './common.js';
8
5
  import os from 'node:os';
9
6
  import { DEFAULT_CURRENCY, isCurrencyCode } from './currency.js';
10
7
  import { readTextIfExists } from '../extensions/lib/utils.js';
11
8
  import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
9
+ function readTextIfExistsSync(file) {
10
+ try {
11
+ return existsSync(file) ? readFileSync(file, 'utf8') : null;
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export const BACKUP_SCHEDULES = new Set(['manual', 'daily', 'weekly', 'monthly']);
12
18
  function defaultProfile() {
13
19
  return {
14
20
  comboDefault: 'off',
@@ -16,18 +22,20 @@ function defaultProfile() {
16
22
  rtkDefault: false,
17
23
  ponytailDefault: 'off',
18
24
  currency: DEFAULT_CURRENCY,
25
+ backupSchedule: 'monthly',
26
+ subagentMarkers: [],
19
27
  };
20
28
  }
21
- // Stored profile: ~/.tersio/settings.json. An install that predates that file
22
- // kept the same values as OMP plugin settings, so seed from there once and
23
- // never lose a user's choice.
24
- async function storedProfile() {
29
+ // Seed once from OMP plugin settings so a pre-~/.tersio install keeps its values. Sync: awaiting let piped stdin arrive before `ask()` attached, so a scripted answer was swallowed and a destructive confirm defaulted to abort.
30
+ function storedProfileSync() {
25
31
  const base = defaultProfile();
26
- const stored = (await readTextIfExists(tersioSettingsFile())) !== null
27
- ? parseStored(await readTextIfExists(tersioSettingsFile()))
28
- : parseStored(await readTextIfExists(legacyOmpLockPath()));
32
+ const stored = parseStored(readTextIfExistsSync(tersioSettingsFile()) ?? readTextIfExistsSync(legacyOmpLockPath()));
29
33
  if (!stored)
30
34
  return base;
35
+ applyStored(base, stored);
36
+ return base;
37
+ }
38
+ function applyStored(base, stored) {
31
39
  if (typeof stored.comboDefault === 'string' && stored.comboDefault in COMBO_PRESET_MODES)
32
40
  base.comboDefault = stored.comboDefault;
33
41
  if (typeof stored.cavemanDefault === 'string' && CAVEMAN_DEFAULTS.has(stored.cavemanDefault))
@@ -39,7 +47,16 @@ async function storedProfile() {
39
47
  if (typeof stored.currency === 'string' && isCurrencyCode(stored.currency.trim().toUpperCase())) {
40
48
  base.currency = stored.currency.trim().toUpperCase();
41
49
  }
42
- return base;
50
+ if (typeof stored.backupSchedule === 'string' && BACKUP_SCHEDULES.has(stored.backupSchedule)) {
51
+ base.backupSchedule = stored.backupSchedule;
52
+ }
53
+ if (Array.isArray(stored.subagentMarkers)) {
54
+ // An empty marker would match every turn.
55
+ base.subagentMarkers = stored.subagentMarkers.filter((m) => typeof m === 'string' && m.trim() !== '');
56
+ }
57
+ }
58
+ async function storedProfile() {
59
+ return storedProfileSync();
43
60
  }
44
61
  function legacyOmpLockPath() {
45
62
  const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
@@ -63,8 +80,7 @@ function parseStored(raw) {
63
80
  return null;
64
81
  }
65
82
  }
66
- // Persist the profile where every host reads it. `omp plugin config get` no
67
- // longer reports it; `tersio settings` does.
83
+ // Persist the profile where every host reads it. `omp plugin config get` no longer reports it; `tersio settings` does.
68
84
  async function writePluginSettings(profile, options) {
69
85
  const file = tersioSettingsFile();
70
86
  const values = {
@@ -75,6 +91,8 @@ async function writePluginSettings(profile, options) {
75
91
  rtkDefault: profile.rtkDefault,
76
92
  ponytailDefault: profile.ponytailDefault,
77
93
  currency: profile.currency,
94
+ backupSchedule: profile.backupSchedule,
95
+ ...(profile.subagentMarkers.length > 0 ? { subagentMarkers: profile.subagentMarkers } : {}),
78
96
  };
79
97
  if (options.dryRun) {
80
98
  if (verbose && !options.quiet)
@@ -88,4 +106,19 @@ async function writePluginSettings(profile, options) {
88
106
  function formatProfile(profile) {
89
107
  return `combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault}) · currency=${profile.currency}`;
90
108
  }
91
- export { defaultProfile, formatProfile, storedProfile, writePluginSettings };
109
+ export { defaultProfile, formatProfile, storedProfile, storedProfileSync, writePluginSettings };
110
+ /** The status line the extensions print, read from the persisted profile. */
111
+ export function formatCliStatus(profile) {
112
+ const { comboDefault, cavemanDefault, rtkDefault, ponytailDefault } = profile;
113
+ if (comboDefault === 'off' && cavemanDefault === 'off' && !rtkDefault && ponytailDefault === 'off') {
114
+ return '🧩 combo OFF';
115
+ }
116
+ const preset = COMBO_PRESET_MODES[comboDefault];
117
+ const isPreset = preset !== undefined
118
+ && preset.caveman === cavemanDefault
119
+ && preset.rtk === rtkDefault
120
+ && preset.ponytail === ponytailDefault;
121
+ const level = isPreset ? comboDefault.toUpperCase() : 'CUSTOM';
122
+ const rtk = rtkDefault ? 'ON' : 'OFF';
123
+ return `🧩 combo ${level}: 🪨caveman=${cavemanDefault.toUpperCase()} ⚡rtk=${rtk} 🦥ponytail=${ponytailDefault.toUpperCase()}`;
124
+ }
package/cli/reset.js CHANGED
@@ -1,10 +1,6 @@
1
- // cli/reset.ts — clear tersio-owned statistics: the usage ledger plus a reset
2
- // watermark that filters session-derived and RTK-metered rows out of every
3
- // derived view. Session transcripts (~/.omp/agent/sessions) and the RTK
4
- // database are host/tool-owned and are never touched; the watermark only
5
- // changes what tersio shows.
1
+ // Clears tersio-owned stats only; transcripts and RTK data stay.
6
2
  import { dryRun, yes } from './common.js';
7
- import { confirmDestructive } from './interactive.js';
3
+ import { confirmDestructive, sayTagged } from './interactive.js';
8
4
  import { clearUsageLedger, importSessionTokens, ledgerPath, markReset, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
9
5
  import { clearUsageDb, usageDbPath } from '../extensions/shared/usage-store.js';
10
6
  import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
@@ -29,7 +25,7 @@ async function runReset() {
29
25
  const cleared = clearUsageLedger();
30
26
  const dbCleared = clearUsageDb();
31
27
  const ts = markReset();
32
- console.log(`[ok] reset — removed ${cleared} usage rows${dbCleared ? ' + usage.db' : ''}; statistics view starts at ${new Date(ts).toISOString()}`);
28
+ sayTagged(`[ok] reset — removed ${cleared} usage rows${dbCleared ? ' + usage.db' : ''}; statistics view starts at ${new Date(ts).toISOString()}`);
33
29
  return true;
34
30
  }
35
31
  export { runReset };
package/cli/rtk-wiring.js CHANGED
@@ -1,9 +1,4 @@
1
- // cli/rtk-wiring.ts — wire the installed rtk binary into OMP.
2
- // `rtk init -g --agent omp` writes rtk's tool_call extension to
3
- // ~/.omp/agent/extensions/rtk.ts but does not register it, and OMP loads only
4
- // listed extensions, so we add the path to config.yml. Every rewritten bash
5
- // call then lands in rtk's history.db. Fail-open: a failed wire only downgrades
6
- // to manual `rtk` prefixing. No cli/common.ts import (argv side effects).
1
+ // OMP loads only listed extensions, so register rtk's path; fail-open.
7
2
  import { promises as fs } from 'node:fs';
8
3
  import os from 'node:os';
9
4
  import path from 'node:path';
@@ -31,25 +26,39 @@ function execFileP(cmd, args, timeout) {
31
26
  });
32
27
  });
33
28
  }
34
- // Idempotent: rtk rewrites its file on every init run.
35
- export async function wireRtkOmp(rtkBin, options = {}) {
29
+ async function runWire(rtkBin, spec, options) {
36
30
  if (!options.dryRun)
37
- debugWire(options, 'Wiring rtk → OMP (bash tool_call rewrite)…');
31
+ debugWire(options, `Wiring rtk → ${spec.host} (${spec.mechanism} rewrite)…`);
38
32
  if (options.dryRun)
39
33
  return true;
40
- const wired = await runRtkInit(rtkBin, options);
41
- if (!wired)
34
+ try {
35
+ await tryRtkInit(rtkBin, spec.initArgs);
36
+ }
37
+ catch (e) {
38
+ console.log(` [warn] could not wire rtk → ${spec.host}: ${shortWiringError(e)}`);
39
+ console.log(` [hint] Manual: ${spec.hint}`);
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+ export async function wireRtkOmp(rtkBin, options = {}) {
45
+ if (!(await runWire(rtkBin, { host: 'OMP', mechanism: 'bash tool_call', initArgs: ['init', '-g', '--agent', 'omp'], hint: 'rtk init -g --agent omp (needs rtk >= 0.49)' }, options)))
42
46
  return false;
43
47
  await ensureRtkInConfig(options);
44
48
  return true;
45
49
  }
50
+ export async function wireRtkOpencode(rtkBin, options = {}) {
51
+ return runWire(rtkBin, { host: 'OpenCode', mechanism: 'tool.execute.before', initArgs: ['init', '-g', '--opencode'], hint: 'rtk init -g --opencode' }, options);
52
+ }
53
+ export async function wireRtkPi(rtkBin, options = {}) {
54
+ return runWire(rtkBin, { host: 'pi', mechanism: 'bash tool_call', initArgs: ['init', '-g', '--agent', 'pi'], hint: 'rtk init -g --agent pi' }, options);
55
+ }
46
56
  // The rtk.ts path rtk init writes (OMP_AGENT_DIR inlined; see header note).
47
57
  function rtkExtensionPath() {
48
58
  const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
49
59
  return path.join(home, '.omp', 'agent', 'extensions', 'rtk.ts');
50
60
  }
51
- // Append rtk.ts after the existing entries (or create the key) so the wire
52
- // takes effect on the next OMP start.
61
+ // Append rtk.ts after the existing entries (or create the key) so the wire takes effect on the next OMP start.
53
62
  export async function ensureRtkInConfig(options) {
54
63
  const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
55
64
  const configPath = path.join(home, '.omp', 'agent', 'config.yml');
@@ -86,26 +95,14 @@ export async function ensureRtkInConfig(options) {
86
95
  console.log(` [warn] could not register rtk in config.yml: ${e.message}`);
87
96
  }
88
97
  }
89
- async function runRtkInit(rtkBin, options) {
98
+ async function tryRtkInit(rtkBin, initArgs) {
90
99
  try {
91
- await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
92
- debugWire(options, 'rtk OMP extension wired');
93
- return true;
100
+ await execFileP(rtkBin, initArgs, 30000);
101
+ return;
94
102
  }
95
- catch (first) {
96
- // A fresh binary can lose its first exec to macOS Gatekeeper, so settle and
97
- // retry once before reporting failure.
98
- await new Promise((resolve) => setTimeout(resolve, 2000));
99
- try {
100
- await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
101
- debugWire(options, 'rtk OMP extension wired');
102
- return true;
103
- }
104
- catch (e) {
105
- void first;
106
- console.log(` [warn] could not wire rtk → OMP: ${shortWiringError(e)}`);
107
- console.log(' [hint] Manual: rtk init -g --agent omp (needs rtk >= 0.49)');
108
- return false;
109
- }
103
+ catch {
104
+ // A fresh binary can lose its first exec to macOS Gatekeeper: settle, then retry once.
110
105
  }
106
+ await new Promise((resolve) => setTimeout(resolve, 2000));
107
+ await execFileP(rtkBin, initArgs, 30000);
111
108
  }
package/cli/settings.js CHANGED
@@ -1,14 +1,10 @@
1
- // cli/settings.ts — view/edit session-start defaults (combo/caveman/rtk/ponytail)
2
- // plus the display-currency default for usage/dashboard reports.
3
- // Non-interactive: `tersio settings --combo-default balanced` (plus per-mode
4
- // override flags and --currency). Interactive: clack selects seeded from the
5
- // stored profile.
1
+ // cli/settings.ts — view/edit session-start defaults and the display currency.
6
2
  import path from 'node:path';
7
- import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, diagScheduleFlag, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, settingArg, } from './common.js';
3
+ import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, diagScheduleFlag, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, settingArg, subagentMarkerFlags, subagentMarkersGiven, } from './common.js';
8
4
  import { CURRENCY_CODES } from './currency.js';
9
5
  import { textTable } from './usage.js';
10
- import { askInteractiveChoice, closeRL, tty } from './interactive.js';
11
- import { formatProfile, storedProfile, writePluginSettings } from './profile.js';
6
+ import { ask, askInteractiveChoice, closeRL, tty } from './interactive.js';
7
+ import { formatCliStatus, formatProfile, storedProfile, writePluginSettings } from './profile.js';
12
8
  import { readDiagSchedule, setDiagSchedule } from './dashboard.js';
13
9
  function applyFlags(base) {
14
10
  const next = { ...base };
@@ -24,6 +20,8 @@ function applyFlags(base) {
24
20
  next.rtkDefault = rtkDefaultFlag === 'on';
25
21
  if (ponytailDefaultFlag !== undefined)
26
22
  next.ponytailDefault = ponytailDefaultFlag;
23
+ if (subagentMarkersGiven)
24
+ next.subagentMarkers = subagentMarkerFlags;
27
25
  if (currencyGiven)
28
26
  next.currency = currency;
29
27
  return next;
@@ -44,6 +42,8 @@ async function askProfile(current) {
44
42
  rtkDefault: preset.rtk,
45
43
  ponytailDefault: preset.ponytail,
46
44
  currency: current.currency,
45
+ backupSchedule: current.backupSchedule,
46
+ subagentMarkers: current.subagentMarkers,
47
47
  };
48
48
  const caveman = await askInteractiveChoice('Caveman default', [...CAVEMAN_DEFAULTS].map((v) => ({
49
49
  value: v, label: v,
@@ -70,8 +70,26 @@ async function askProfile(current) {
70
70
  if (cur.status !== 'selected')
71
71
  return null;
72
72
  next.currency = cur.value;
73
+ const markers = await askSubagentMarkers(next.subagentMarkers);
74
+ if (markers === null)
75
+ return null;
76
+ next.subagentMarkers = markers;
73
77
  return next;
74
78
  }
79
+ // One marker per line; an empty line ends the list. Null aborts.
80
+ async function askSubagentMarkers(current) {
81
+ console.log(' Subagent prompt markers (one per line, empty line to finish).');
82
+ if (current.length > 0)
83
+ console.log(` Current: ${current.join(' | ')}`);
84
+ const entered = [];
85
+ for (;;) {
86
+ const line = (await ask(` marker ${entered.length + 1}: `)).trim();
87
+ if (line === '')
88
+ break;
89
+ entered.push(line);
90
+ }
91
+ return entered;
92
+ }
75
93
  async function askDiagSchedule(current) {
76
94
  const picked = await askInteractiveChoice('Diagnosis auto-check', [
77
95
  { value: 'manual', label: 'manual', hint: 'check on open' },
@@ -83,12 +101,11 @@ async function askDiagSchedule(current) {
83
101
  return null;
84
102
  return picked.value;
85
103
  }
86
- const SETTING_NAMES = ['combo', 'caveman', 'rtk', 'ponytail', 'currency', 'diagnosis'];
104
+ const SETTING_NAMES = ['combo', 'caveman', 'rtk', 'ponytail', 'currency', 'diagnosis', 'markers'];
87
105
  function settingsUsage() {
88
- console.log(' Usage: tersio settings [combo|caveman|rtk|ponytail|currency|diagnosis] [--combo-default off|medium|balanced|max] [--caveman-default off|lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra] [--rtk-default on|off] [--ponytail-default off|lite|full|ultra] [--currency USD|PHP|EUR|GBP|JPY|KRW|SGD|AUD|CAD|INR] [--diag-schedule manual|daily|weekly|monthly] [--dry-run]');
106
+ console.log(' Usage: tersio settings [combo|caveman|rtk|ponytail|currency|diagnosis|markers] [--combo-default off|medium|balanced|max] [--caveman-default off|lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra] [--rtk-default on|off] [--ponytail-default off|lite|full|ultra] [--currency USD|PHP|EUR|GBP|JPY|KRW|SGD|AUD|CAD|INR] [--diag-schedule manual|daily|weekly|monthly] [--subagent-marker <text>]... [--dry-run]');
89
107
  }
90
- // Single-setting jump: `tersio settings diagnosis` prompts only that value
91
- // instead of walking the whole chain. Returns false when aborted.
108
+ // Single-setting jump: `tersio settings diagnosis` prompts only that value instead of walking the whole chain. Returns false when aborted.
92
109
  async function runSingleSetting(name, current, nextDiag) {
93
110
  const next = { ...current };
94
111
  let diag = nextDiag;
@@ -153,6 +170,13 @@ async function runSingleSetting(name, current, nextDiag) {
153
170
  diag = picked;
154
171
  break;
155
172
  }
173
+ case 'markers': {
174
+ const markers = await askSubagentMarkers(next.subagentMarkers);
175
+ if (markers === null)
176
+ return null;
177
+ next.subagentMarkers = markers;
178
+ break;
179
+ }
156
180
  default: {
157
181
  console.error(`[fail] Invalid setting: ${name}. Valid: ${SETTING_NAMES.join(', ')}`);
158
182
  process.exit(1);
@@ -162,15 +186,14 @@ async function runSingleSetting(name, current, nextDiag) {
162
186
  }
163
187
  function printSettingsTable(current) {
164
188
  console.log('\n=== Tersio Settings ===');
165
- // Currency lives here no longer: the dashboard owns displaying and
166
- // persisting it (its picker POSTs to /currency); settings can still set
167
- // the stored default via --currency or the prompt below.
189
+ // The dashboard owns displaying currency; settings only sets the default.
168
190
  const rows = [
169
191
  ['combo', current.comboDefault, [...COMBO_DEFAULTS].join(' | ')],
170
192
  ['caveman', current.cavemanDefault, [...CAVEMAN_DEFAULTS].join(' | ')],
171
193
  ['rtk', current.rtkDefault ? 'on' : 'off', 'on | off'],
172
194
  ['ponytail', current.ponytailDefault, [...PONYTAIL_DEFAULTS].join(' | ')],
173
195
  ['diagnosis', readDiagSchedule(), 'manual | daily | weekly | monthly'],
196
+ ['markers', current.subagentMarkers.length === 0 ? 'default' : current.subagentMarkers.join(' | '), 'host subagent prompt text'],
174
197
  ];
175
198
  for (const l of textTable(['Setting', 'Current', 'Valid values'], rows, [false, false, false], 64)) {
176
199
  console.log(l);
@@ -206,7 +229,7 @@ async function runSettings() {
206
229
  next = single.profile;
207
230
  nextDiag = single.diag;
208
231
  }
209
- else if (profileFlagsGiven || currencyGiven || diagScheduleFlag !== undefined) {
232
+ else if (profileFlagsGiven || currencyGiven || diagScheduleFlag !== undefined || subagentMarkersGiven) {
210
233
  next = applyFlags(current);
211
234
  }
212
235
  else if (tty()) {
@@ -237,6 +260,7 @@ async function runSettings() {
237
260
  }
238
261
  await writePluginSettings(next, {});
239
262
  setDiagSchedule(nextDiag);
263
+ console.log(` ${formatCliStatus(next)}`);
240
264
  console.log(` Defaults: ${formatProfile(next)} · diagnosis=${nextDiag}`);
241
265
  console.log(' Applies to fresh sessions only — anything persisted with /combo, /caveman, or /rtk wins.');
242
266
  console.log(' Currency applies to usage/dashboard reports; --currency overrides it per run.');