@crouton-kit/crouter 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { registerUpdateCommand } from './commands/update.js';
11
11
  import { registerDoctorCommand } from './commands/doctor.js';
12
12
  import { registerPlanCommand } from './commands/plan.js';
13
13
  import { registerSpecCommand } from './commands/spec.js';
14
+ import { maybeAutoUpdate } from './core/auto-update.js';
14
15
  function readPackageVersion() {
15
16
  const here = dirname(fileURLToPath(import.meta.url));
16
17
  const pkgPath = join(here, '..', 'package.json');
@@ -32,6 +33,7 @@ registerUpdateCommand(program);
32
33
  registerDoctorCommand(program);
33
34
  registerPlanCommand(program);
34
35
  registerSpecCommand(program);
36
+ maybeAutoUpdate(process.argv);
35
37
  program.parseAsync().catch((err) => {
36
38
  process.stderr.write(`crtr: ${err instanceof Error ? err.message : String(err)}\n`);
37
39
  process.exit(1);
@@ -35,6 +35,14 @@ function setNestedValue(cfg, key, value) {
35
35
  cfg.auto_update.content = value;
36
36
  return;
37
37
  }
38
+ if (key === 'auto_update.crtr') {
39
+ const coerced = value === true ? 'notify' : value;
40
+ if (coerced !== 'notify' && coerced !== 'apply' && coerced !== false) {
41
+ throw usage(`auto_update.crtr must be 'notify', 'apply', or false`);
42
+ }
43
+ cfg.auto_update.crtr = coerced;
44
+ return;
45
+ }
38
46
  if (parts.length === 1) {
39
47
  cfg[topKey] = value;
40
48
  return;
@@ -9,6 +9,7 @@ import { resolveSkill, listAllSkills, listInstalledPlugins, findPluginByName, pa
9
9
  import { updateConfig, ensureScopeInitialized } from '../core/config.js';
10
10
  import { parseFrontmatter, serializeFrontmatter } from '../core/frontmatter.js';
11
11
  import { ensureDir, pathExists, readText, walkFiles } from '../core/fs-utils.js';
12
+ import { skillPrompt } from '../prompts/skill.js';
12
13
  const KNOWN_VERBS = new Set([
13
14
  'list',
14
15
  'show',
@@ -35,7 +36,7 @@ export function registerSkillCommands(program) {
35
36
  .option('--frontmatter', 'include YAML frontmatter in the printed body')
36
37
  .action(async (nameOrVerb, _rest, opts) => {
37
38
  if (nameOrVerb === undefined) {
38
- skill.help();
39
+ out(skillPrompt());
39
40
  return;
40
41
  }
41
42
  if (!KNOWN_VERBS.has(nameOrVerb)) {
@@ -1,2 +1,4 @@
1
1
  import { Command } from 'commander';
2
+ export declare function selfCheck(): void;
3
+ export declare function contentCheck(): void;
2
4
  export declare function registerUpdateCommand(program: Command): void;
@@ -24,7 +24,7 @@ function selfUpdate() {
24
24
  throw general('npm install failed');
25
25
  }
26
26
  }
27
- function selfCheck() {
27
+ export function selfCheck() {
28
28
  const res = spawnSync('npm', ['view', '@crouton-kit/crtr', 'version'], { encoding: 'utf8' });
29
29
  if (res.status !== 0) {
30
30
  warn('could not check for crtr updates (network unavailable)');
@@ -66,7 +66,7 @@ function contentUpdate() {
66
66
  });
67
67
  }
68
68
  }
69
- function contentCheck() {
69
+ export function contentCheck() {
70
70
  const marketplaces = listAllMarketplaces();
71
71
  for (const mkt of marketplaces) {
72
72
  const fetchRes = fetch(mkt.root, mkt.ref);
@@ -0,0 +1 @@
1
+ export declare function maybeAutoUpdate(argv: string[]): void;
@@ -0,0 +1,86 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readConfig, readState, updateState } from './config.js';
3
+ import { nowIso } from './fs-utils.js';
4
+ import { info } from './output.js';
5
+ import { selfCheck, contentCheck } from '../commands/update.js';
6
+ const HOUR_MS = 60 * 60 * 1000;
7
+ const SKIP_SUBCOMMANDS = new Set([
8
+ 'update',
9
+ 'help',
10
+ '--help',
11
+ '-h',
12
+ '--version',
13
+ '-v',
14
+ ]);
15
+ function shouldSkipForArgv(argv) {
16
+ const sub = argv[2];
17
+ if (sub === undefined)
18
+ return true;
19
+ return SKIP_SUBCOMMANDS.has(sub);
20
+ }
21
+ function withinInterval(lastIso, intervalHours) {
22
+ if (!lastIso)
23
+ return false;
24
+ const last = Date.parse(lastIso);
25
+ if (!Number.isFinite(last))
26
+ return false;
27
+ return Date.now() - last < intervalHours * HOUR_MS;
28
+ }
29
+ function spawnDetachedSelfUpdate() {
30
+ const child = spawn('npm', ['i', '-g', '@crouton-kit/crtr@latest'], {
31
+ detached: true,
32
+ stdio: 'ignore',
33
+ });
34
+ child.unref();
35
+ }
36
+ function spawnDetachedContentUpdate() {
37
+ const child = spawn('crtr', ['update', '--content'], {
38
+ detached: true,
39
+ stdio: 'ignore',
40
+ });
41
+ child.unref();
42
+ }
43
+ export function maybeAutoUpdate(argv) {
44
+ try {
45
+ if (process.env.CRTR_NO_AUTO_UPDATE === '1')
46
+ return;
47
+ if (shouldSkipForArgv(argv))
48
+ return;
49
+ const cfg = readConfig('user');
50
+ const { crtr, content, interval_hours } = cfg.auto_update;
51
+ if (crtr === false && content === false)
52
+ return;
53
+ const state = readState('user');
54
+ if (state.last_self_check === undefined) {
55
+ updateState('user', (s) => {
56
+ s.last_self_check = nowIso();
57
+ });
58
+ return;
59
+ }
60
+ if (withinInterval(state.last_self_check, interval_hours))
61
+ return;
62
+ updateState('user', (s) => {
63
+ s.last_self_check = nowIso();
64
+ });
65
+ if (crtr === 'notify') {
66
+ selfCheck();
67
+ }
68
+ else if (crtr === 'apply') {
69
+ info('applying self-update in background');
70
+ spawnDetachedSelfUpdate();
71
+ }
72
+ if (content === 'notify') {
73
+ contentCheck();
74
+ }
75
+ else if (content === 'apply') {
76
+ info('applying content updates in background');
77
+ spawnDetachedContentUpdate();
78
+ }
79
+ }
80
+ catch (e) {
81
+ if (process.env.CRTR_DEBUG === '1') {
82
+ const msg = e instanceof Error ? e.message : String(e);
83
+ process.stderr.write(`crtr: auto-update hook error: ${msg}\n`);
84
+ }
85
+ }
86
+ }
@@ -55,6 +55,15 @@ export function ensureScopeInitialized(scope, root) {
55
55
  writeJson(cfgPath, defaultScopeConfig());
56
56
  }
57
57
  }
58
+ function normalizeMode(value, fallback) {
59
+ if (value === true)
60
+ return 'notify';
61
+ if (value === false)
62
+ return false;
63
+ if (value === 'notify' || value === 'apply')
64
+ return value;
65
+ return fallback;
66
+ }
58
67
  function mergeConfig(partial) {
59
68
  const defaults = defaultScopeConfig();
60
69
  const schema_version = partial.schema_version === undefined ? defaults.schema_version : partial.schema_version;
@@ -62,10 +71,14 @@ function mergeConfig(partial) {
62
71
  const plugins = partial.plugins === undefined ? {} : partial.plugins;
63
72
  const skills = partial.skills === undefined ? {} : partial.skills;
64
73
  const au = partial.auto_update;
74
+ const rawInterval = au && typeof au.interval_hours === 'number' ? au.interval_hours : undefined;
75
+ const interval_hours = rawInterval !== undefined && Number.isFinite(rawInterval) && rawInterval >= 0
76
+ ? rawInterval
77
+ : defaults.auto_update.interval_hours;
65
78
  const auto_update = {
66
- crtr: au && au.crtr !== undefined ? au.crtr : defaults.auto_update.crtr,
67
- content: au && au.content !== undefined ? au.content : defaults.auto_update.content,
68
- interval_hours: au && au.interval_hours !== undefined ? au.interval_hours : defaults.auto_update.interval_hours,
79
+ crtr: normalizeMode(au?.crtr, defaults.auto_update.crtr),
80
+ content: normalizeMode(au?.content, defaults.auto_update.content),
81
+ interval_hours,
69
82
  };
70
83
  return { schema_version, marketplaces, plugins, skills, auto_update };
71
84
  }
@@ -11,29 +11,103 @@ export function parseFrontmatter(source) {
11
11
  function parseSimpleYaml(yaml) {
12
12
  const lines = yaml.split(/\r?\n/);
13
13
  const out = {};
14
- let currentKey = null;
15
- let listBuffer = null;
16
- for (const raw of lines) {
17
- if (!raw.trim())
18
- continue;
19
- if (raw.startsWith(' - ') || raw.startsWith('- ')) {
20
- const value = raw.replace(/^\s*-\s+/, '').trim();
21
- if (currentKey && listBuffer)
22
- listBuffer.push(stripQuotes(value));
14
+ let i = 0;
15
+ while (i < lines.length) {
16
+ const raw = lines[i];
17
+ if (!raw.trim()) {
18
+ i++;
23
19
  continue;
24
20
  }
25
21
  const idx = raw.indexOf(':');
26
- if (idx === -1)
22
+ if (idx === -1) {
23
+ i++;
27
24
  continue;
28
- if (currentKey && listBuffer) {
29
- out[currentKey] = listBuffer;
30
- listBuffer = null;
31
25
  }
32
26
  const key = raw.slice(0, idx).trim();
33
27
  const rest = raw.slice(idx + 1).trim();
34
- currentKey = key;
28
+ // Block scalar: `key: |` or `key: >` with optional chomp indicator (-/+)
29
+ const blockMatch = rest.match(/^([|>])([-+]?)\s*$/);
30
+ if (blockMatch) {
31
+ const style = blockMatch[1];
32
+ const chomp = blockMatch[2];
33
+ const collected = [];
34
+ let blockIndent = null;
35
+ let j = i + 1;
36
+ while (j < lines.length) {
37
+ const r = lines[j];
38
+ if (r.trim() === '') {
39
+ collected.push('');
40
+ j++;
41
+ continue;
42
+ }
43
+ const ind = r.match(/^(\s*)/)?.[1].length ?? 0;
44
+ if (blockIndent === null) {
45
+ if (ind === 0)
46
+ break;
47
+ blockIndent = ind;
48
+ }
49
+ if (ind < blockIndent)
50
+ break;
51
+ collected.push(r.slice(blockIndent));
52
+ j++;
53
+ }
54
+ while (collected.length > 0 && collected[collected.length - 1] === '')
55
+ collected.pop();
56
+ let value;
57
+ if (style === '|') {
58
+ value = collected.join('\n');
59
+ }
60
+ else {
61
+ const parts = [];
62
+ let para = [];
63
+ for (const ln of collected) {
64
+ if (ln === '') {
65
+ if (para.length > 0) {
66
+ parts.push(para.join(' '));
67
+ para = [];
68
+ }
69
+ parts.push('');
70
+ }
71
+ else {
72
+ para.push(ln);
73
+ }
74
+ }
75
+ if (para.length > 0)
76
+ parts.push(para.join(' '));
77
+ const folded = [];
78
+ for (let k = 0; k < parts.length; k++) {
79
+ if (parts[k] === '' && (k === 0 || parts[k - 1] === ''))
80
+ continue;
81
+ folded.push(parts[k]);
82
+ }
83
+ value = folded.join('\n').replace(/\n+$/, '');
84
+ }
85
+ if (chomp !== '+')
86
+ value = value.replace(/\n+$/, '');
87
+ out[key] = value;
88
+ i = j;
89
+ continue;
90
+ }
91
+ // Empty value: could be a list on subsequent lines
35
92
  if (rest === '') {
36
- listBuffer = [];
93
+ const buf = [];
94
+ let j = i + 1;
95
+ while (j < lines.length) {
96
+ const r = lines[j];
97
+ if (r.trim() === '') {
98
+ j++;
99
+ continue;
100
+ }
101
+ if (/^\s*-\s+/.test(r)) {
102
+ buf.push(stripQuotes(r.replace(/^\s*-\s+/, '').trim()));
103
+ j++;
104
+ continue;
105
+ }
106
+ break;
107
+ }
108
+ if (buf.length > 0)
109
+ out[key] = buf;
110
+ i = j;
37
111
  continue;
38
112
  }
39
113
  if (rest.startsWith('[') && rest.endsWith(']')) {
@@ -42,14 +116,12 @@ function parseSimpleYaml(yaml) {
42
116
  .split(',')
43
117
  .map((s) => stripQuotes(s.trim()))
44
118
  .filter(Boolean);
45
- currentKey = null;
119
+ i++;
46
120
  continue;
47
121
  }
48
122
  out[key] = stripQuotes(rest);
49
- currentKey = null;
123
+ i++;
50
124
  }
51
- if (currentKey && listBuffer)
52
- out[currentKey] = listBuffer;
53
125
  const fm = {
54
126
  name: typeof out.name === 'string' ? out.name : '',
55
127
  description: typeof out.description === 'string' ? out.description : undefined,
@@ -95,5 +95,12 @@ EOF
95
95
 
96
96
  Your turn ends after the save command succeeds. No need to summarize the plan
97
97
  in chat — the user can read the file.
98
+
99
+ ## See also
100
+
101
+ - \`crtr plan list\` — list saved plans for the current directory
102
+ - \`crtr plan show <name>\` — print the body of a saved plan
103
+ - \`crtr plan edit <name>\` — open a saved plan in \$EDITOR
104
+ - \`crtr plan path [name]\` — absolute path of a plan or the plans directory
98
105
  `;
99
106
  }
@@ -0,0 +1 @@
1
+ export declare function skillPrompt(): string;
@@ -0,0 +1,49 @@
1
+ export function skillPrompt() {
2
+ return `# Skill workflow
3
+
4
+ \`crtr\` ships skills — markdown reference with frontmatter that you pull on
5
+ demand. When the user's task matches a skill's description, run
6
+ \`crtr skill show <name>\` and apply the guidance. Ambiguous names exit \`4\` —
7
+ disambiguate with \`<plugin>:<name>\`.
8
+
9
+ ## Discover
10
+
11
+ \`\`\`
12
+ crtr skill list # one per line: <scope>:<plugin>/<name> — <description>
13
+ crtr skill search <query> # rank by name, description, keywords
14
+ crtr skill grep <pattern> # regex search across SKILL.md bodies
15
+ \`\`\`
16
+
17
+ ## Load
18
+
19
+ \`\`\`
20
+ crtr skill show <name> # print SKILL.md body to stdout
21
+ crtr skill show <plugin>:<name> # disambiguate when names collide
22
+ crtr skill path <name> # absolute path to SKILL.md
23
+ crtr skill where <name> # {scope, plugin, path} as JSON
24
+ \`\`\`
25
+
26
+ \`show\` is the default verb: \`crtr skill <name>\` (with no verb) also prints
27
+ the body.
28
+
29
+ ## Author
30
+
31
+ \`\`\`
32
+ crtr skill new <plugin>:<name> --description "..." # scaffold a new skill
33
+ crtr skill show authoring-skills # the SKILL.md authoring guide
34
+ \`\`\`
35
+
36
+ ## Toggle
37
+
38
+ \`\`\`
39
+ crtr skill enable <name> # clear any disable in the chosen scope
40
+ crtr skill disable <name> # hide from list and agent discovery
41
+ \`\`\`
42
+
43
+ ## Exit codes
44
+
45
+ - \`0\` — success
46
+ - \`3\` — skill not found
47
+ - \`4\` — ambiguous name; use \`<plugin>:<name>\`
48
+ `;
49
+ }
@@ -102,5 +102,12 @@ EOF
102
102
 
103
103
  Your turn ends after the save command succeeds. No need to summarize the spec
104
104
  in chat — the user can read the file.
105
+
106
+ ## See also
107
+
108
+ - \`crtr spec list\` — list saved specs for the current directory
109
+ - \`crtr spec show <name>\` — print the body of a saved spec
110
+ - \`crtr spec edit <name>\` — open a saved spec in \$EDITOR
111
+ - \`crtr spec path [name]\` — absolute path of a spec or the specs directory
105
112
  `;
106
113
  }
package/dist/types.d.ts CHANGED
@@ -47,9 +47,10 @@ export interface ConfigPluginEntry {
47
47
  export interface ConfigSkillEntry {
48
48
  enabled: boolean;
49
49
  }
50
+ export type AutoUpdateMode = 'notify' | 'apply' | false;
50
51
  export interface AutoUpdateConfig {
51
- crtr: boolean;
52
- content: 'notify' | 'apply' | false;
52
+ crtr: AutoUpdateMode;
53
+ content: AutoUpdateMode;
53
54
  interval_hours: number;
54
55
  }
55
56
  export interface ScopeConfig {
package/dist/types.js CHANGED
@@ -22,7 +22,7 @@ export function defaultScopeConfig() {
22
22
  marketplaces: {},
23
23
  plugins: {},
24
24
  skills: {},
25
- auto_update: { crtr: true, content: 'notify', interval_hours: 24 },
25
+ auto_update: { crtr: 'notify', content: 'notify', interval_hours: 24 },
26
26
  };
27
27
  }
28
28
  export function skillConfigKey(plugin, name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/crouter",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "crtr — fast access to skills, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",