@krtclcdy/tersio 2.25.4 → 2.25.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -63
- package/dashboard/dist/index.html +18 -16
- package/dist/cli/backups.js +187 -0
- package/dist/cli/banner.js +4 -3
- package/dist/cli/common.js +29 -13
- package/dist/cli/currency.js +1 -1
- package/dist/cli/dashboard.js +168 -110
- package/dist/cli/doctor-fix.js +232 -50
- package/dist/cli/doctor.js +281 -32
- package/dist/cli/hosts.js +29 -9
- package/dist/cli/install.js +204 -69
- package/dist/cli/interactive.js +3 -3
- package/dist/cli/manifest.js +125 -17
- package/dist/cli/profile.js +14 -9
- package/dist/cli/rtk-wiring.js +1 -1
- package/dist/cli/settings.js +20 -5
- package/dist/cli/uninstall.js +24 -8
- package/dist/cli/update.js +9 -10
- package/dist/cli/usage.js +75 -7
- package/dist/extensions/{caveman-session → caveman}/index.js +4 -2
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-megacave.md +5 -3
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-ultra.md +4 -4
- package/dist/extensions/caveman/rule.md +89 -0
- package/dist/extensions/{combo-toggle → combo}/index.js +62 -21
- package/dist/extensions/{tersio-commands → commands}/index.js +2 -2
- package/dist/extensions/pstack/index.js +119 -0
- package/dist/extensions/pstack/rule.md +65 -0
- package/dist/extensions/{rtk-filter/index.js → rtk/filter.js} +1 -2
- package/dist/extensions/{rtk-session/index.js → rtk/session.js} +5 -5
- package/dist/extensions/{opencode/server.js → server.js} +98 -14
- package/dist/extensions/shared/agent-stats.js +1198 -0
- package/dist/extensions/shared/brand-marks.js +153 -0
- package/dist/extensions/shared/carbon.js +1 -1
- package/dist/extensions/shared/plugin-settings.js +12 -7
- package/dist/extensions/shared/pricing.js +86 -30
- package/dist/extensions/shared/rtk-gain.js +4 -9
- package/dist/extensions/shared/session-state.js +15 -10
- package/dist/extensions/shared/sqlite.js +168 -0
- package/dist/extensions/shared/status.js +8 -8
- package/dist/extensions/shared/types.js +1 -1
- package/dist/extensions/shared/usage-ledger.js +113 -39
- package/dist/extensions/shared/usage-store.js +191 -48
- package/dist/extensions/{lib → shared}/utils.js +6 -6
- package/dist/extensions/timing/index.js +31 -0
- package/dist/extensions/{ai-addons-updater → updater}/index.js +13 -6
- package/dist/probe-catalog.js +48 -0
- package/dist/probe-catalog188.js +44 -0
- package/dist/tersio.js +9 -4
- package/extensions/{caveman-session → caveman}/index.ts +4 -2
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-megacave.md +5 -3
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-ultra.md +4 -4
- package/extensions/caveman/rule.md +89 -0
- package/extensions/{combo-toggle → combo}/index.ts +62 -19
- package/extensions/{tersio-commands → commands}/index.ts +2 -2
- package/extensions/pstack/index.ts +128 -0
- package/extensions/pstack/rule.md +65 -0
- package/extensions/{rtk-filter/index.ts → rtk/filter.ts} +1 -2
- package/extensions/{rtk-session/index.ts → rtk/session.ts} +5 -5
- package/extensions/{opencode/server.ts → server.ts} +81 -16
- package/extensions/shared/agent-stats.ts +1605 -0
- package/extensions/shared/brand-marks.ts +163 -0
- package/extensions/shared/carbon.ts +1 -1
- package/extensions/shared/plugin-settings.ts +13 -7
- package/extensions/shared/pricing.ts +95 -32
- package/extensions/shared/rtk-gain.ts +4 -9
- package/extensions/shared/session-state.ts +18 -13
- package/extensions/shared/sqlite.ts +168 -0
- package/extensions/shared/status.ts +9 -8
- package/extensions/shared/types.ts +2 -1
- package/extensions/shared/usage-ledger.ts +134 -46
- package/extensions/shared/usage-store.ts +202 -50
- package/extensions/{lib → shared}/utils.ts +6 -6
- package/extensions/timing/index.ts +45 -0
- package/extensions/{ai-addons-updater → updater}/index.ts +15 -9
- package/package.json +27 -18
- package/dist/extensions/caveman-session/rule.md +0 -79
- package/extensions/caveman-session/rule.md +0 -79
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// /pstack session mode: injects the pstack rigor body. Every preset above off turns it on.
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.js';
|
|
6
|
+
import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
|
|
7
|
+
import { announceStatus } from '../shared/status.js';
|
|
8
|
+
import { readPstackDefault } from '../shared/plugin-settings.js';
|
|
9
|
+
const PSTACK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const RULE_FILE = 'rule.md';
|
|
11
|
+
const DEFAULT_MODE = 'off';
|
|
12
|
+
// The body is immutable for the process, so read it once; reinstalling needs a restart.
|
|
13
|
+
let cachedInstruction;
|
|
14
|
+
function buildInstruction() {
|
|
15
|
+
if (cachedInstruction)
|
|
16
|
+
return cachedInstruction;
|
|
17
|
+
// Fail loud: a missing body would otherwise read to the model as an active mode.
|
|
18
|
+
let body;
|
|
19
|
+
try {
|
|
20
|
+
body = readFileSync(join(PSTACK_DIR, RULE_FILE), 'utf8');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return `pstack mode requested, but ${RULE_FILE} is missing. Run: tersio doctor --fix extensions`;
|
|
24
|
+
}
|
|
25
|
+
cachedInstruction = `pstack mode active for this session.\n${body}`;
|
|
26
|
+
return cachedInstruction;
|
|
27
|
+
}
|
|
28
|
+
function resolveMode(entries, fallback = DEFAULT_MODE) {
|
|
29
|
+
return lastCustomValue(entries, 'pstack-mode', (data) => normalizeMode('pstack', data?.mode)) ?? fallback;
|
|
30
|
+
}
|
|
31
|
+
// The shared off phrase; every mode honours it, so one word clears the session.
|
|
32
|
+
function isOffCommand(text) {
|
|
33
|
+
return normalizeInputCommand(text) === 'normal mode';
|
|
34
|
+
}
|
|
35
|
+
export default function pstackSessionExtension(pi) {
|
|
36
|
+
let currentMode = DEFAULT_MODE;
|
|
37
|
+
let lastInjected = undefined;
|
|
38
|
+
function setMode(mode, ctx) {
|
|
39
|
+
const normalized = normalizeMode('pstack', mode);
|
|
40
|
+
if (!normalized)
|
|
41
|
+
return false;
|
|
42
|
+
currentMode = normalized;
|
|
43
|
+
pi.appendEntry?.('pstack-mode', { mode: normalized });
|
|
44
|
+
setSharedComboMode('pstack', normalized);
|
|
45
|
+
announceStatus(ctx);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
setExtensionLabel(pi, 'pstack rigor mode');
|
|
49
|
+
// Adopt a /tersio or /combo switch at once, so the next turn needs no reload.
|
|
50
|
+
function syncFromShared(state) {
|
|
51
|
+
const mode = normalizeMode('pstack', state.pstack);
|
|
52
|
+
if (mode)
|
|
53
|
+
currentMode = mode;
|
|
54
|
+
}
|
|
55
|
+
setSharedComboListener('pstack', syncFromShared);
|
|
56
|
+
pi.registerCommand?.('pstack', {
|
|
57
|
+
description: 'Toggle the pstack rigor mode for this session',
|
|
58
|
+
handler: async (args, ctx) => {
|
|
59
|
+
const arg = String(args || '').trim().toLowerCase();
|
|
60
|
+
if (!arg || arg === 'on') {
|
|
61
|
+
setMode('on', ctx);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (arg === 'status') {
|
|
65
|
+
announceStatus(ctx);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!setMode(arg, ctx)) {
|
|
69
|
+
ctx?.ui?.notify?.('Usage: /pstack [on|off|status]', 'warning');
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
onHostEvent(pi, 'input', async (event) => {
|
|
74
|
+
if (event?.source === 'extension')
|
|
75
|
+
return;
|
|
76
|
+
if (currentMode !== 'off' && isOffCommand(event?.text))
|
|
77
|
+
setMode('off');
|
|
78
|
+
});
|
|
79
|
+
function restoreMode(ctx) {
|
|
80
|
+
const entries = sessionEntries(ctx);
|
|
81
|
+
// A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
|
|
82
|
+
if (hasModeState(entries))
|
|
83
|
+
reconcileSharedComboEntries(entries);
|
|
84
|
+
// Persisted state wins, then a mode already chosen, then the configured default. `off` is the start value, not unset.
|
|
85
|
+
const persisted = resolveMode(entries, '');
|
|
86
|
+
const alreadyChosen = currentMode !== DEFAULT_MODE;
|
|
87
|
+
currentMode = persisted || (alreadyChosen ? currentMode : normalizeMode('pstack', readPstackDefault())) || DEFAULT_MODE;
|
|
88
|
+
announceStatus(ctx);
|
|
89
|
+
}
|
|
90
|
+
// Start, resume, and branch all land here, so every session type shows it.
|
|
91
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
92
|
+
restoreMode(ctx);
|
|
93
|
+
});
|
|
94
|
+
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
95
|
+
restoreMode(ctx);
|
|
96
|
+
});
|
|
97
|
+
pi.on('session_tree', async (_event, ctx) => {
|
|
98
|
+
restoreMode(ctx);
|
|
99
|
+
});
|
|
100
|
+
onHostEvent(pi, 'session_switch', async (_event, ctx) => {
|
|
101
|
+
restoreMode(ctx);
|
|
102
|
+
});
|
|
103
|
+
onHostEvent(pi, 'session_compact', async (_event, ctx) => {
|
|
104
|
+
restoreMode(ctx);
|
|
105
|
+
});
|
|
106
|
+
pi.on('before_agent_start', async (event) => {
|
|
107
|
+
const mode = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().pstack : currentMode;
|
|
108
|
+
if (!mode || mode === 'off')
|
|
109
|
+
return;
|
|
110
|
+
const instruction = buildInstruction();
|
|
111
|
+
// Match the whole instruction, so a re-presented prompt cannot stack it.
|
|
112
|
+
if (systemPromptIncludes(event.systemPrompt, instruction))
|
|
113
|
+
return;
|
|
114
|
+
// The previous block is replaced, not stacked on.
|
|
115
|
+
const stale = lastInjected;
|
|
116
|
+
lastInjected = instruction;
|
|
117
|
+
return injectPromptText(pi, event, { text: instruction, staleMarker: stale, sectionTag: 'pstack' });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pstack
|
|
3
|
+
description: >
|
|
4
|
+
Rigor mode: apply the named principle at the moment it fits, and verify
|
|
5
|
+
against the real artifact instead of a proxy. Use for /pstack, "pstack",
|
|
6
|
+
"poteto mode". Stays on until "/pstack off" or "normal mode".
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# pstack
|
|
10
|
+
|
|
11
|
+
Stay in this mode for every response of the session, until the user says `/pstack off` or `normal mode`.
|
|
12
|
+
|
|
13
|
+
This mode owns the work, not the voice: where an active style mode disagrees with these rules about the shape of the reply, the style mode wins.
|
|
14
|
+
|
|
15
|
+
Where ponytail disagrees with these rules about how much to change, ponytail wins. The change is the size the task requires, and no larger. These rules own the approach, the verification, and the design.
|
|
16
|
+
|
|
17
|
+
## Non-negotiables
|
|
18
|
+
|
|
19
|
+
- About to ask the user to pick an approach → classify the fork first. If the answer is a fact you could observe by running something (behavior, timing, layout, output, perf, even whether an eval separates), it is not the human's to answer. Sketch it and let the result decide. Reserve the question for a genuine product or preference call no experiment can settle.
|
|
20
|
+
- Contested design → get an adversarial read before shipping. A second opinion is the same prompt against a different model.
|
|
21
|
+
- An automated review commented → skeptical posture. They catch real bugs and also file non-issues and nitpicks. Assess each on its merits and dismiss noise with a concrete reason instead of churning code.
|
|
22
|
+
- Asked to land or ship a green stack → green is not safe: nothing gets armed before an independent per-PR verdict.
|
|
23
|
+
- Broken skill or rule file mid-task → fix it in its own PR. Don't block. Don't silently work around it.
|
|
24
|
+
|
|
25
|
+
## Principles
|
|
26
|
+
|
|
27
|
+
Apply the entry whose trigger matches the moment.
|
|
28
|
+
|
|
29
|
+
**Core**
|
|
30
|
+
|
|
31
|
+
- **Foundational Thinking.** Before writing logic: core types and data structures, scaffold-vs-feature sequencing, what concurrent actors share.
|
|
32
|
+
- **Attack the Premise.** Two or more fixes that share one premise have failed the same gate. Take a census of which actors hold the imbalance before the next fix, then question the premise instead of writing another fix that assumes it.
|
|
33
|
+
- **Minimize Reader Load.** Reviewing or shaping code that's hard to trace. Count layers and hidden state, collapse one-caller wrappers, shrink mutable scope.
|
|
34
|
+
- **Outcome-Oriented Execution.** Planned rewrites and migrations with explicit phase boundaries. Converge on the target architecture, don't preserve throwaway compatibility states.
|
|
35
|
+
- **Experience First.** Product, UX, or feature-scope tradeoffs. Choose user delight over implementation convenience.
|
|
36
|
+
- **Exhaust the Design Space.** A novel interaction or architectural decision with no precedent. Build 2-3 competing prototypes and compare before committing.
|
|
37
|
+
|
|
38
|
+
**Architecture**
|
|
39
|
+
|
|
40
|
+
- **Model the Domain.** Writing stateful logic, or code that branches a lot or repeats a shape assumption across files. Encode the domain in a structure (state machine, typed model, table or registry, reducer, boundary, the right collection) instead of scattered conditionals.
|
|
41
|
+
- **Type System Discipline.** Designing types or a signature in any typed language. Make illegal states unrepresentable, brand primitives, parse external data at boundaries.
|
|
42
|
+
- **Make Operations Idempotent.** Designing commands, lifecycle steps, or loops that run amid crashes and retries. Converge to the same end state.
|
|
43
|
+
- **Migrate Callers Then Delete Legacy APIs.** Introducing a new internal API while old callers exist. Migrate and delete in one wave.
|
|
44
|
+
- **Separate Before Serializing Shared State.** Concurrent actors might write the same file, branch, key, or object. Eliminate the sharing first.
|
|
45
|
+
|
|
46
|
+
**Verification**
|
|
47
|
+
|
|
48
|
+
- **Sequence Work into Verifiable Units.** Multi-step work (sweeps, migrations, runs of similar edits). Break work into small units that each end in a check, verify each before the next, and order delivery so the sequence proves itself.
|
|
49
|
+
- **Test Behavior, Not Implementation.** Writing, changing, or keeping a test. Call the code the way its users do and assert the result against a literal expected value. If the test would still pass when every imported function returns `undefined`, rewrite the assertion or delete the test.
|
|
50
|
+
- **Explain the Number.** Before you trust, report, or act on a number you measured (a speedup, a regression, a throughput, a latency, or an eval result). Find what limits it, and rule out that it measured something other than the work you think.
|
|
51
|
+
|
|
52
|
+
**Delegation**
|
|
53
|
+
|
|
54
|
+
- **Guard the Context Window.** Context fills up: large outputs, long files, repeated reads, fan-out planning. Route bulk to subagents, keep summaries in the main thread.
|
|
55
|
+
- **Never Block on the Human.** Tempted to ask "should I do X?" on reversible work. Proceed, present the result, let the human course-correct.
|
|
56
|
+
|
|
57
|
+
**Meta**
|
|
58
|
+
|
|
59
|
+
- **Encode Lessons in Structure.** You catch yourself writing the same instruction a second time. Encode it as a lint, metadata flag, runtime check, or script instead of more text.
|
|
60
|
+
|
|
61
|
+
## Autonomy
|
|
62
|
+
|
|
63
|
+
**No is an acceptable answer.** Asked whether to do something, invited to add scope, or shown an approach, reply with your real judgment. Decline, push back, or say "this doesn't earn its place" when true. A recommendation is a judgment, not a validation. Agreement is not the default, candor over sycophancy.
|
|
64
|
+
|
|
65
|
+
Forked from pstack's poteto-mode skill by Lauren Tan, MIT Licence. Source: cursor/plugins at commit dbf49813a39c1a5389c5e28cb8e4c1c2379e2454, path pstack/skills/poteto-mode/SKILL.md. Every rule above is that skill's own text, trimmed. Omitted because Tersio's caveman and ponytail blocks already inject them: the Laziness Protocol, Subtract Before You Add, Boundary Discipline, Prove It Works, and Fix Root Causes entries, plus the Comments section. Omitted because they route to skills or playbooks Tersio does not ship: the Playbooks table, the Subagents section, and the remaining trigger entries. Omitted as reply-shape rules that fight caveman: Writing the reply, and the instruction to name each principle in the reply. Omitted because their triggers fire on ordinary work and ponytail owns the size of a change: the Redesign from First Principles and Build the Lever entries. The precedence lines are Tersio's own.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { resolveRtkBinary } from '../
|
|
5
|
+
import { resolveRtkBinary } from '../shared/utils.js';
|
|
6
6
|
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
7
7
|
import { getSharedComboState, lastCustomValue, sessionEntries } from '../shared/session-state.js';
|
|
8
8
|
/** Tool name to `rtk pipe --filter` name. Exported so the OpenCode host reuses one map. */
|
|
@@ -34,7 +34,6 @@ export function rtkActive(entries) {
|
|
|
34
34
|
export function filterableText(parts) {
|
|
35
35
|
if (!Array.isArray(parts) || parts.length === 0)
|
|
36
36
|
return null;
|
|
37
|
-
// An image or a mixed result is not text to filter.
|
|
38
37
|
if (parts.some((part) => part?.type !== 'text'))
|
|
39
38
|
return null;
|
|
40
39
|
return parts.map((part) => part?.text ?? '').join('\n');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.js';
|
|
2
2
|
import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.js';
|
|
3
3
|
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
4
|
-
import { resolveRtkBinary } from '../
|
|
4
|
+
import { resolveRtkBinary } from '../shared/utils.js';
|
|
5
5
|
import { announceStatus } from '../shared/status.js';
|
|
6
6
|
const DEFAULT_ENABLED = false;
|
|
7
7
|
function asBoolean(value) {
|
|
@@ -22,8 +22,8 @@ function setRtkProcessEnabled(enabled) {
|
|
|
22
22
|
else
|
|
23
23
|
process.env.RTK_DISABLED = '1';
|
|
24
24
|
}
|
|
25
|
-
// rtk's
|
|
26
|
-
const RTK_PROMPT = `RTK guidance active.
|
|
25
|
+
// rtk's contract: name the rewritten surface, never the binary — a hand-typed `rtk` fights the rewrite.
|
|
26
|
+
const RTK_PROMPT = `RTK guidance active. What you run is condensed automatically: searches (grep, rg), listings (ls, find, cat as read), package and test runs (bun, npm run, pnpm run), version control (git), and type and lint checks (tsc, eslint). Every signal kept, costly noise dropped. Treat a condensed result as complete. Re-run something unfiltered only when its result is empty, garbled, or contradicts its exit code. A command with no condensed form passes through unchanged.`;
|
|
27
27
|
export default function rtkSessionExtension(pi) {
|
|
28
28
|
let enabled = DEFAULT_ENABLED;
|
|
29
29
|
function fail(text) {
|
|
@@ -37,7 +37,7 @@ export default function rtkSessionExtension(pi) {
|
|
|
37
37
|
announceStatus(ctx);
|
|
38
38
|
}
|
|
39
39
|
setExtensionLabel(pi, 'RTK session toggle');
|
|
40
|
-
// Live mirror: a /tersio or /combo switch publishes shared state — adopt it
|
|
40
|
+
// Live mirror: a /tersio or /combo switch publishes shared state — adopt it once so the next turn and rtk_run see it.
|
|
41
41
|
function syncFromShared(state) {
|
|
42
42
|
enabled = state.rtk === 'on';
|
|
43
43
|
setRtkProcessEnabled(enabled);
|
|
@@ -97,7 +97,7 @@ export default function rtkSessionExtension(pi) {
|
|
|
97
97
|
// A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
|
|
98
98
|
if (hasModeState(entries))
|
|
99
99
|
reconcileSharedComboEntries(entries);
|
|
100
|
-
// Persisted session state wins; a fresh session falls back to the installer/user
|
|
100
|
+
// Persisted session state wins; a fresh session falls back to the installer/user default (off unless configured).
|
|
101
101
|
const persisted = resolveEnabled(entries);
|
|
102
102
|
enabled = typeof persisted === 'boolean' ? persisted : enabled || readRtkDefault();
|
|
103
103
|
setRtkProcessEnabled(enabled);
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
// OpenCode plugin entry: maps its plugin API onto the shared ExtensionApi the mode extensions consume.
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import cavemanSessionExtension from './caveman/index.js';
|
|
7
|
+
import pstackSessionExtension from './pstack/index.js';
|
|
8
|
+
import rtkSessionExtension from './rtk/session.js';
|
|
9
|
+
import rtkFilterExtension, { FILTER_BY_TOOL, MIN_FILTER_BYTES, filterThroughRtk, filterableText, rtkActive } from './rtk/filter.js';
|
|
10
|
+
import comboToggleExtension from './combo/index.js';
|
|
11
|
+
import tersioCommandsExtension from './commands/index.js';
|
|
12
|
+
import { getSharedComboState } from './shared/session-state.js';
|
|
13
|
+
import { formatStatus } from './shared/status.js';
|
|
14
|
+
import { resolveRtkBinary } from './shared/utils.js';
|
|
11
15
|
// The npm name, so the plugin list reads the same as the OMP and pi installs.
|
|
12
16
|
const plugin = {
|
|
13
17
|
id: '@krtclcdy/tersio',
|
|
@@ -20,8 +24,7 @@ const plugin = {
|
|
|
20
24
|
const own = (await ctx.storage.get(`entries:${sid}`)) ?? [];
|
|
21
25
|
if (own.length)
|
|
22
26
|
return own;
|
|
23
|
-
// Pre-scoped installs kept one global list; adopt it once
|
|
24
|
-
// Clear it after adoption so it can never shadow saved defaults later.
|
|
27
|
+
// Pre-scoped installs kept one global list; adopt it once and clear it so it cannot shadow saved defaults later.
|
|
25
28
|
const legacy = (await ctx.storage.get('entries')) ?? [];
|
|
26
29
|
if (legacy.length)
|
|
27
30
|
void ctx.storage.set('entries', []).catch(() => { });
|
|
@@ -112,7 +115,7 @@ const plugin = {
|
|
|
112
115
|
lifecycle.set(event, list);
|
|
113
116
|
},
|
|
114
117
|
};
|
|
115
|
-
for (const extension of [cavemanSessionExtension, rtkSessionExtension, rtkFilterExtension, comboToggleExtension, tersioCommandsExtension])
|
|
118
|
+
for (const extension of [cavemanSessionExtension, pstackSessionExtension, rtkSessionExtension, rtkFilterExtension, comboToggleExtension, tersioCommandsExtension])
|
|
116
119
|
extension(host);
|
|
117
120
|
// The first hook of an unseen session runs its session_start handlers.
|
|
118
121
|
const sidOf = (value) => typeof value === 'string' && value ? value : undefined;
|
|
@@ -127,8 +130,7 @@ const plugin = {
|
|
|
127
130
|
await ensureSession(sid);
|
|
128
131
|
const text = typeof input.prompt?.text === 'string' ? input.prompt.text : '';
|
|
129
132
|
const args = text.includes(' ') ? text.slice(text.indexOf(' ') + 1).trim() : '';
|
|
130
|
-
//
|
|
131
|
-
// surface. Collect it and send it back so a command never answers blank.
|
|
133
|
+
// Shared handlers notify via ui.notify, which OpenCode lacks; echo it back so a command does not answer blank.
|
|
132
134
|
const notes = [];
|
|
133
135
|
const cmdCtx = {
|
|
134
136
|
...stubCtx(sid),
|
|
@@ -163,6 +165,16 @@ const plugin = {
|
|
|
163
165
|
});
|
|
164
166
|
}
|
|
165
167
|
});
|
|
168
|
+
// The tree ships the ruleset beside the extensions, and OpenCode reads no node_modules from a plugin dir.
|
|
169
|
+
try {
|
|
170
|
+
await ctx.skill.transform((editor) => {
|
|
171
|
+
for (const skill of bundledSkills())
|
|
172
|
+
editor.add(skill);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Hosts without the skill transform keep the ruleset in the prompt alone.
|
|
177
|
+
}
|
|
166
178
|
await ctx.session.hook('context', async (event) => {
|
|
167
179
|
const sid = sidOf(event.sessionID);
|
|
168
180
|
if (sid)
|
|
@@ -198,6 +210,12 @@ const plugin = {
|
|
|
198
210
|
const input = event.input;
|
|
199
211
|
if (typeof input?.command !== 'string' || !input.command)
|
|
200
212
|
return;
|
|
213
|
+
// /rtk off must stop the rewrite too, or the mode switch only silences the prompt block.
|
|
214
|
+
const sid = typeof event.sessionID === 'string' ? event.sessionID : activeSid;
|
|
215
|
+
if (sid !== undefined)
|
|
216
|
+
await ensureSession(sid);
|
|
217
|
+
if (!rtkActive(sid === undefined ? undefined : sessionEntriesOf(sid)))
|
|
218
|
+
return;
|
|
201
219
|
const rewritten = await rewriteCommand(input.command);
|
|
202
220
|
if (rewritten)
|
|
203
221
|
input.command = rewritten;
|
|
@@ -246,6 +264,64 @@ function execRtk(cmd, args, opts) {
|
|
|
246
264
|
});
|
|
247
265
|
});
|
|
248
266
|
}
|
|
267
|
+
/** `rtk rewrite` exit codes: 0 rewrite found, 1 no equivalent, 3 advisory; the code is the contract. */
|
|
268
|
+
const RTK_REWRITE_FOUND = 0;
|
|
269
|
+
const RTK_REWRITE_ADVISORY = 3;
|
|
270
|
+
/** The bundled ruleset's skill dirs, beside the plugin entry: OpenCode reads no node_modules from a plugin. */
|
|
271
|
+
const PONYTAIL_BUNDLE_SKILLS = join(dirname(fileURLToPath(import.meta.url)), 'ponytail-bundle', 'skills');
|
|
272
|
+
/** Frontmatter a SKILL.md carries: flat keys only, the shape every bundled skill uses. */
|
|
273
|
+
function parseSkillFile(file) {
|
|
274
|
+
let raw;
|
|
275
|
+
try {
|
|
276
|
+
raw = readFileSync(file, 'utf8');
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---[^\S\n]*\r?\n?([\s\S]*)$/);
|
|
282
|
+
if (!match)
|
|
283
|
+
return null;
|
|
284
|
+
const field = (key) => {
|
|
285
|
+
const lines = match[1].split(/\r?\n/);
|
|
286
|
+
const at = lines.findIndex((line) => line.startsWith(`${key}:`));
|
|
287
|
+
if (at === -1)
|
|
288
|
+
return undefined;
|
|
289
|
+
const value = lines[at].slice(key.length + 1).trim();
|
|
290
|
+
if (value[0] !== '>' && value[0] !== '|')
|
|
291
|
+
return value;
|
|
292
|
+
// Folded (`>`) joins the indented lines; the next unindented key ends the block.
|
|
293
|
+
const block = [];
|
|
294
|
+
for (const line of lines.slice(at + 1)) {
|
|
295
|
+
if (line.trim() !== '' && !/^\s/.test(line))
|
|
296
|
+
break;
|
|
297
|
+
block.push(line.trim());
|
|
298
|
+
}
|
|
299
|
+
while (block.length > 0 && block[block.length - 1] === '')
|
|
300
|
+
block.pop();
|
|
301
|
+
return block.join(value[0] === '|' ? '\n' : ' ');
|
|
302
|
+
};
|
|
303
|
+
const id = field('name') ?? basename(dirname(file));
|
|
304
|
+
return { id, name: id, description: field('description') ?? '', path: file, content: match[2] };
|
|
305
|
+
}
|
|
306
|
+
/** Every bundled skill, or an empty list when the installer has not bundled them. */
|
|
307
|
+
function bundledSkills() {
|
|
308
|
+
let entries;
|
|
309
|
+
try {
|
|
310
|
+
entries = readdirSync(PONYTAIL_BUNDLE_SKILLS, { withFileTypes: true });
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
const out = [];
|
|
316
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
317
|
+
if (!entry.isDirectory())
|
|
318
|
+
continue;
|
|
319
|
+
const skill = parseSkillFile(join(PONYTAIL_BUNDLE_SKILLS, entry.name, 'SKILL.md'));
|
|
320
|
+
if (skill)
|
|
321
|
+
out.push(skill);
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
249
325
|
function rewriteCommand(command) {
|
|
250
326
|
const bin = resolveRtkBinary();
|
|
251
327
|
if (!bin)
|
|
@@ -256,8 +332,16 @@ function rewriteCommand(command) {
|
|
|
256
332
|
const err = error;
|
|
257
333
|
if (err && (err.killed || err.signal))
|
|
258
334
|
return resolve(null);
|
|
335
|
+
const code = typeof err?.code === 'number' ? err.code : RTK_REWRITE_FOUND;
|
|
336
|
+
// Exit 1 is the only "no equivalent" answer, so anything else with output rewrites.
|
|
337
|
+
if (code === 1)
|
|
338
|
+
return resolve(null);
|
|
259
339
|
const out = String(stdout).trim();
|
|
260
|
-
|
|
340
|
+
if (!out || out === command)
|
|
341
|
+
return resolve(null);
|
|
342
|
+
if (code !== RTK_REWRITE_FOUND && code !== RTK_REWRITE_ADVISORY)
|
|
343
|
+
return resolve(null);
|
|
344
|
+
resolve(out);
|
|
261
345
|
});
|
|
262
346
|
});
|
|
263
347
|
}
|