@remits/remits-cli 0.1.113 → 0.1.114

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/index.js CHANGED
@@ -24,7 +24,10 @@ const SESSIONS_FILE = path.join(SESSION_DIR, 'sessions.json');
24
24
  const CONFIG_FILE = path.join(SESSION_DIR, 'config.json');
25
25
  const LISTENER_PID_FILE = path.join(SESSION_DIR, 'listener.pid');
26
26
  const ACTIVITY_LOG_FILE = path.join(SESSION_DIR, 'activity.log');
27
- const SKILL_SOURCE_FILE = path.join(__dirname, 'skills', 'remits-cli', 'SKILL.md');
27
+ const SKILL_SOURCE_DIR = path.join(__dirname, 'skills', 'remits-cli');
28
+ const SKILL_SOURCE_FILE = path.join(SKILL_SOURCE_DIR, 'SKILL.md');
29
+ const SKILL_REFERENCES_DIRNAME = 'references';
30
+ const SKILL_REFERENCES_SOURCE_DIR = path.join(SKILL_SOURCE_DIR, SKILL_REFERENCES_DIRNAME);
28
31
  const ACCOUNT_REPO_INDEX_FILE = path.join(SESSION_DIR, 'account-repos.json');
29
32
  const SERVICE_STATE_FILE = path.join(SESSION_DIR, 'service-state.json');
30
33
  const ISSUES_DIR = path.join(SESSION_DIR, 'issues');
@@ -1297,7 +1300,6 @@ function updateAccountRepoIndex(cwd) {
1297
1300
  if (!info) {
1298
1301
  return;
1299
1302
  }
1300
- try { syncInstalledSkills(); } catch (_) { /* best-effort */ }
1301
1303
  const entry = buildAccountRepoEntryFromInfo(info, cwd, 'cwd');
1302
1304
  if (!entry) {
1303
1305
  return;
@@ -1371,7 +1373,6 @@ function discoverAccountRepos() {
1371
1373
  for (const root of roots) {
1372
1374
  walkForAccountInfo(root, 0, matches, visited, platformMatches);
1373
1375
  }
1374
- try { syncInstalledSkills(); } catch (_) { /* best-effort */ }
1375
1376
 
1376
1377
  const entries = [];
1377
1378
  for (const file of matches) {
@@ -1924,13 +1925,166 @@ async function loggedGet(api, cwd, endpoint, params = {}) {
1924
1925
  }
1925
1926
  }
1926
1927
 
1928
+ // ---------------------------------------------------------------------------
1929
+ // Skill delivery
1930
+ //
1931
+ // The skill is SKILL.md (the index and the rules) plus the reference files in
1932
+ // skills/remits-cli/references/. Both ship inside the npm package, so the whole
1933
+ // skill is available before the CLI has ever authenticated.
1934
+ //
1935
+ // Every delivered markdown file goes through deliverSkillMarkdown(), which is
1936
+ // the ONE delivery path: it substitutes the machine-specific placeholders and
1937
+ // resolves each file's table of contents to real line numbers. Writing a file
1938
+ // out with fs.writeFileSync + readFileSync instead would ship the same skill
1939
+ // with a different table of contents depending on which door it came out of.
1940
+ // ---------------------------------------------------------------------------
1941
+
1942
+ // GitHub-style heading anchor: lowercase, punctuation dropped, spaces to hyphens.
1943
+ function headingSlug(headingText) {
1944
+ return String(headingText || '')
1945
+ .toLowerCase()
1946
+ .replace(/[^a-z0-9 _-]/g, '')
1947
+ .trim()
1948
+ .replace(/\s/g, '-');
1949
+ }
1950
+
1951
+ function formatTocEntry(indent, target) {
1952
+ if (!target) return null;
1953
+ return indent + '- L' + target.line + ' ' + target.title;
1954
+ }
1955
+
1956
+ // Rewrites a document's "## Table of Contents" entries to carry the line number of
1957
+ // the heading they point at, turning the TOC into a jump table:
1958
+ //
1959
+ // - [Component Structure](#component-structure) -> - L59 Component Structure
1960
+ // - Line 31: Component Structure -> - L59 Component Structure
1961
+ //
1962
+ // Numbers are computed HERE, at delivery, never written into the repo file.
1963
+ // Hand-maintained line numbers rot on the first edit above them, and a WRONG
1964
+ // number is worse than none: it sends the reader to the wrong section, where it
1965
+ // may conclude the content is missing. Resolving at delivery means the numbers
1966
+ // cannot be stale, while the repo file keeps anchor links that survive edits and
1967
+ // produce clean diffs.
1968
+ //
1969
+ // INVARIANT: every rewrite is one line in, one line out. The line count must not
1970
+ // change, or the numbers computed against the input would no longer describe the
1971
+ // output. Locked down by test/skill-references.test.js.
1972
+ //
1973
+ // An entry whose target heading cannot be resolved is left exactly as written —
1974
+ // it degrades to the original text, never to a guessed number.
1975
+ function resolveTableOfContentsLineNumbers(markdown) {
1976
+ if (!markdown || !markdown.trim()) {
1977
+ return markdown;
1978
+ }
1979
+
1980
+ const lines = markdown.split('\n');
1981
+ const tocStart = lines.findIndex((line) => /^##\s+table of contents\s*$/i.test(line));
1982
+ if (tocStart < 0) {
1983
+ return markdown;
1984
+ }
1985
+
1986
+ let tocEnd = lines.length;
1987
+ for (let i = tocStart + 1; i < lines.length; i += 1) {
1988
+ if (/^##(?!#)\s+.*$/.test(lines[i])) {
1989
+ tocEnd = i;
1990
+ break;
1991
+ }
1992
+ }
1993
+
1994
+ // A `#`-prefixed line inside a fenced code block is CONTENT, not a heading. The
1995
+ // references routinely show markdown and shell samples that contain one. Indexing
1996
+ // those shifts GitHub's duplicate-slug disambiguation (-1, -2 ...) out of step with
1997
+ // the real headings, so a legitimate repeated heading resolves to the wrong line.
1998
+ const byAnchor = new Map();
1999
+ const byTitle = new Map();
2000
+ const slugCounts = new Map();
2001
+ let inFence = false;
2002
+
2003
+ lines.forEach((line, index) => {
2004
+ if (/^\s{0,3}(```|~~~)/.test(line)) {
2005
+ inFence = !inFence;
2006
+ return;
2007
+ }
2008
+ if (inFence) return;
2009
+
2010
+ const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
2011
+ if (!heading) return;
2012
+
2013
+ const title = heading[2].trim();
2014
+ const target = { line: index + 1, title };
2015
+
2016
+ const baseSlug = headingSlug(title);
2017
+ const seen = slugCounts.get(baseSlug) || 0;
2018
+ slugCounts.set(baseSlug, seen + 1);
2019
+ const slug = seen === 0 ? baseSlug : baseSlug + '-' + seen;
2020
+
2021
+ if (!byAnchor.has(slug)) byAnchor.set(slug, target);
2022
+ if (!byTitle.has(title)) byTitle.set(title, target);
2023
+ });
2024
+
2025
+ let changed = false;
2026
+ for (let i = tocStart + 1; i < tocEnd; i += 1) {
2027
+ const rewritten = resolveTocEntry(lines[i], byAnchor, byTitle);
2028
+ if (rewritten !== null) {
2029
+ lines[i] = rewritten;
2030
+ changed = true;
2031
+ }
2032
+ }
2033
+
2034
+ return changed ? lines.join('\n') : markdown;
2035
+ }
2036
+
2037
+ // The rewritten entry carries the ACTUAL heading text, not the TOC's own link text.
2038
+ // The two drift (a heading gets reworded and the TOC entry does not), and emitting
2039
+ // the heading text both self-heals that drift and guarantees the entry is a literal
2040
+ // match for the heading — so an agent can grep it as-is rather than trusting the number.
2041
+ //
2042
+ // Returns null when the line is not a resolvable TOC entry and must be left untouched.
2043
+ function resolveTocEntry(line, byAnchor, byTitle) {
2044
+ const anchorEntry = /^(\s*)[-*]\s+\[(.+)\]\(#([^)\s]+)\)\s*$/.exec(line);
2045
+ if (anchorEntry) {
2046
+ return formatTocEntry(anchorEntry[1], byAnchor.get(anchorEntry[3]));
2047
+ }
2048
+
2049
+ // Legacy hand-written form. Re-resolving it corrects files that have not been
2050
+ // audited yet, instead of shipping their stale numbers.
2051
+ const legacyEntry = /^(\s*)[-*]\s+Line\s+\d+:\s*(.+?)\s*$/.exec(line);
2052
+ if (legacyEntry) {
2053
+ const title = legacyEntry[2];
2054
+ return formatTocEntry(legacyEntry[1], byTitle.get(title) || byAnchor.get(headingSlug(title)));
2055
+ }
2056
+
2057
+ return null;
2058
+ }
2059
+
2060
+ // THE single delivery path for a skill markdown file. Substitutes the placeholders
2061
+ // that only the installing machine can fill in, then resolves the table of contents.
2062
+ function deliverSkillMarkdown(sourceText, referencesDir) {
2063
+ let body = String(sourceText);
2064
+ body = body.split('{{ACCOUNT_REPO_INDEX_PATH}}').join(ACCOUNT_REPO_INDEX_FILE);
2065
+ body = body.split('{{SKILL_REFERENCES_DIR}}').join(referencesDir);
2066
+ return resolveTableOfContentsLineNumbers(body);
2067
+ }
2068
+
1927
2069
  function resolveSkillTargets(target) {
1928
2070
  const value = String(target || 'all').toLowerCase();
1929
2071
  const home = os.homedir();
2072
+ // Gemini reads a FLAT skill file, so its references cannot sit beside it as
2073
+ // `references/`. That is why SKILL.md states the reference directory as an
2074
+ // absolute path substituted per target rather than a relative one.
1930
2075
  const targets = {
1931
- codex: path.join(home, '.codex', 'skills', 'remits-cli', 'SKILL.md'),
1932
- claude: path.join(home, '.claude', 'skills', 'remits-cli', 'SKILL.md'),
1933
- gemini: path.join(home, '.gemini', 'skills', 'remits-cli.md')
2076
+ codex: {
2077
+ skillPath: path.join(home, '.codex', 'skills', 'remits-cli', 'SKILL.md'),
2078
+ referencesDir: path.join(home, '.codex', 'skills', 'remits-cli', SKILL_REFERENCES_DIRNAME)
2079
+ },
2080
+ claude: {
2081
+ skillPath: path.join(home, '.claude', 'skills', 'remits-cli', 'SKILL.md'),
2082
+ referencesDir: path.join(home, '.claude', 'skills', 'remits-cli', SKILL_REFERENCES_DIRNAME)
2083
+ },
2084
+ gemini: {
2085
+ skillPath: path.join(home, '.gemini', 'skills', 'remits-cli.md'),
2086
+ referencesDir: path.join(home, '.gemini', 'skills', 'remits-cli', SKILL_REFERENCES_DIRNAME)
2087
+ }
1934
2088
  };
1935
2089
 
1936
2090
  if (value === 'all') {
@@ -1942,34 +2096,83 @@ function resolveSkillTargets(target) {
1942
2096
  return [[value, targets[value]]];
1943
2097
  }
1944
2098
 
1945
- function buildSkillSourceBody() {
2099
+ function listSkillReferenceSources() {
2100
+ if (!fs.existsSync(SKILL_REFERENCES_SOURCE_DIR)) {
2101
+ return [];
2102
+ }
2103
+ return fs.readdirSync(SKILL_REFERENCES_SOURCE_DIR)
2104
+ .filter((name) => name.endsWith('.md'))
2105
+ .sort();
2106
+ }
2107
+
2108
+ function buildSkillSourceBody(referencesDir) {
1946
2109
  if (!fs.existsSync(SKILL_SOURCE_FILE)) {
1947
2110
  throw new Error('Skill source file not found: ' + SKILL_SOURCE_FILE);
1948
2111
  }
2112
+ return deliverSkillMarkdown(fs.readFileSync(SKILL_SOURCE_FILE, 'utf8'), referencesDir);
2113
+ }
2114
+
2115
+ // Install the reference set beside (or, for gemini, alongside) the skill file, and
2116
+ // prune any reference this CLI version no longer ships. A stale reference is worse
2117
+ // than a missing one: SKILL.md would not name it, but an agent listing the directory
2118
+ // would still find and trust it.
2119
+ function writeSkillReferences(referencesDir, options = {}) {
2120
+ const names = listSkillReferenceSources();
2121
+ const written = [];
2122
+ if (!names.length) {
2123
+ return written;
2124
+ }
2125
+
2126
+ ensureDir(referencesDir);
2127
+ const expected = new Set(names);
2128
+
2129
+ for (const name of names) {
2130
+ const body = deliverSkillMarkdown(
2131
+ fs.readFileSync(path.join(SKILL_REFERENCES_SOURCE_DIR, name), 'utf8'),
2132
+ referencesDir
2133
+ );
2134
+ const targetPath = path.join(referencesDir, name);
2135
+ if (options.overwrite === false && fs.existsSync(targetPath)) {
2136
+ continue;
2137
+ }
2138
+ if (writeFileIfChanged(targetPath, body)) {
2139
+ written.push(targetPath);
2140
+ }
2141
+ }
2142
+
2143
+ try {
2144
+ for (const existing of fs.readdirSync(referencesDir)) {
2145
+ if (existing.endsWith('.md') && !expected.has(existing)) {
2146
+ fs.unlinkSync(path.join(referencesDir, existing));
2147
+ }
2148
+ }
2149
+ } catch (_) {
2150
+ // Best-effort pruning; a stale file never blocks delivery of the current set.
2151
+ }
1949
2152
 
1950
- let sourceBody = fs.readFileSync(SKILL_SOURCE_FILE, 'utf8');
1951
- sourceBody = sourceBody.replace('{{ACCOUNT_REPO_INDEX_PATH}}', ACCOUNT_REPO_INDEX_FILE);
1952
- return sourceBody;
2153
+ return written;
1953
2154
  }
1954
2155
 
1955
2156
  function syncInstalledSkills(target = 'all') {
1956
2157
  const targets = resolveSkillTargets(target);
1957
- const sourceBody = buildSkillSourceBody();
1958
2158
  const summary = {
1959
2159
  updated: [],
1960
2160
  failed: []
1961
2161
  };
1962
2162
 
1963
- for (const [agent, targetPath] of targets) {
2163
+ for (const [agent, spec] of targets) {
1964
2164
  try {
1965
- ensureDir(path.dirname(targetPath));
1966
- if (writeFileIfChanged(targetPath, sourceBody)) {
1967
- summary.updated.push({ agent, path: targetPath });
2165
+ ensureDir(path.dirname(spec.skillPath));
2166
+ if (writeFileIfChanged(spec.skillPath, buildSkillSourceBody(spec.referencesDir))) {
2167
+ summary.updated.push({ agent, path: spec.skillPath });
2168
+ }
2169
+ for (const referencePath of writeSkillReferences(spec.referencesDir)) {
2170
+ summary.updated.push({ agent, path: referencePath });
1968
2171
  }
1969
2172
  } catch (error) {
1970
2173
  summary.failed.push({
1971
2174
  agent,
1972
- path: targetPath,
2175
+ path: spec.skillPath,
1973
2176
  reason: error && error.message ? error.message : String(error)
1974
2177
  });
1975
2178
  }
@@ -1985,19 +2188,24 @@ async function installSkillsCommand(flags) {
1985
2188
 
1986
2189
  const overwrite = flags.overwrite === true || flags.overwrite === 'true';
1987
2190
  const targets = resolveSkillTargets(flags.target || 'all');
1988
- const sourceBody = buildSkillSourceBody();
1989
2191
 
1990
2192
  const installed = [];
1991
2193
  const skipped = [];
1992
2194
 
1993
- for (const [agent, targetPath] of targets) {
1994
- ensureDir(path.dirname(targetPath));
1995
- if (fs.existsSync(targetPath) && !overwrite) {
1996
- skipped.push({ agent, path: targetPath, reason: 'already exists (use --overwrite true)' });
1997
- continue;
2195
+ for (const [agent, spec] of targets) {
2196
+ ensureDir(path.dirname(spec.skillPath));
2197
+ if (fs.existsSync(spec.skillPath) && !overwrite) {
2198
+ skipped.push({ agent, path: spec.skillPath, reason: 'already exists (use --overwrite true)' });
2199
+ } else {
2200
+ atomicWriteFile(spec.skillPath, buildSkillSourceBody(spec.referencesDir));
2201
+ installed.push({ agent, path: spec.skillPath });
2202
+ }
2203
+ // References are refreshed even when the skill file itself was left alone: an
2204
+ // agent reading a customized SKILL.md still needs the reference set it points at.
2205
+ const references = writeSkillReferences(spec.referencesDir);
2206
+ if (references.length) {
2207
+ installed.push({ agent, path: spec.referencesDir + ' (' + references.length + ' reference file(s))' });
1998
2208
  }
1999
- atomicWriteFile(targetPath, sourceBody);
2000
- installed.push({ agent, path: targetPath });
2001
2209
  }
2002
2210
 
2003
2211
  console.log('Skill installation complete.');
@@ -9140,6 +9348,15 @@ async function main() {
9140
9348
  if (autoUpdateIfNeeded(originalArgv, { requireSuccess: requireSuccessfulAutoUpdate })) return;
9141
9349
  }
9142
9350
 
9351
+ // Refresh the installed skill on EVERY command, not just from inside an account repo. This runs
9352
+ // after the auto-update re-exec, so it is the new version writing the new skill. It was previously
9353
+ // hung off updateAccountRepoIndex(), which returns early outside a repo — so a user who upgraded and
9354
+ // then ran `remits-cli agent serve` from a plain tab (the documented way to work tickets, explicitly
9355
+ // "no need to be in an account repo") kept the PREVIOUS skill on disk indefinitely: a stale index
9356
+ // pointing at reference files that the upgrade had just renamed or removed. Reading and comparing the
9357
+ // whole corpus across all three agent targets measures ~3ms, so there is nothing to save by guarding it.
9358
+ try { syncInstalledSkills(); } catch (_) { /* best-effort */ }
9359
+
9143
9360
  // Best-effort: update account-repo index whenever running from inside a remits repo
9144
9361
  try { updateAccountRepoIndex(process.cwd()); } catch (_) { /* not in account repo */ }
9145
9362
  // Auto-start the background service if not already running.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.113",
3
+ "version": "0.1.114",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -12,7 +12,8 @@
12
12
  "files": [
13
13
  "index.js",
14
14
  "README.md",
15
- "skills/remits-cli/SKILL.md"
15
+ "skills/remits-cli/SKILL.md",
16
+ "skills/remits-cli/references"
16
17
  ],
17
18
  "publishConfig": {
18
19
  "access": "public"