@gmickel/gno 0.3.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.
- package/README.md +256 -0
- package/assets/skill/SKILL.md +112 -0
- package/assets/skill/cli-reference.md +327 -0
- package/assets/skill/examples.md +234 -0
- package/assets/skill/mcp-reference.md +159 -0
- package/package.json +90 -0
- package/src/app/constants.ts +313 -0
- package/src/cli/colors.ts +65 -0
- package/src/cli/commands/ask.ts +545 -0
- package/src/cli/commands/cleanup.ts +105 -0
- package/src/cli/commands/collection/add.ts +120 -0
- package/src/cli/commands/collection/index.ts +10 -0
- package/src/cli/commands/collection/list.ts +108 -0
- package/src/cli/commands/collection/remove.ts +64 -0
- package/src/cli/commands/collection/rename.ts +95 -0
- package/src/cli/commands/context/add.ts +67 -0
- package/src/cli/commands/context/check.ts +153 -0
- package/src/cli/commands/context/index.ts +10 -0
- package/src/cli/commands/context/list.ts +109 -0
- package/src/cli/commands/context/rm.ts +52 -0
- package/src/cli/commands/doctor.ts +393 -0
- package/src/cli/commands/embed.ts +462 -0
- package/src/cli/commands/get.ts +356 -0
- package/src/cli/commands/index-cmd.ts +119 -0
- package/src/cli/commands/index.ts +102 -0
- package/src/cli/commands/init.ts +328 -0
- package/src/cli/commands/ls.ts +217 -0
- package/src/cli/commands/mcp/config.ts +300 -0
- package/src/cli/commands/mcp/index.ts +24 -0
- package/src/cli/commands/mcp/install.ts +203 -0
- package/src/cli/commands/mcp/paths.ts +470 -0
- package/src/cli/commands/mcp/status.ts +222 -0
- package/src/cli/commands/mcp/uninstall.ts +158 -0
- package/src/cli/commands/mcp.ts +20 -0
- package/src/cli/commands/models/clear.ts +103 -0
- package/src/cli/commands/models/index.ts +32 -0
- package/src/cli/commands/models/list.ts +214 -0
- package/src/cli/commands/models/path.ts +51 -0
- package/src/cli/commands/models/pull.ts +199 -0
- package/src/cli/commands/models/use.ts +85 -0
- package/src/cli/commands/multi-get.ts +400 -0
- package/src/cli/commands/query.ts +220 -0
- package/src/cli/commands/ref-parser.ts +108 -0
- package/src/cli/commands/reset.ts +191 -0
- package/src/cli/commands/search.ts +136 -0
- package/src/cli/commands/shared.ts +156 -0
- package/src/cli/commands/skill/index.ts +19 -0
- package/src/cli/commands/skill/install.ts +197 -0
- package/src/cli/commands/skill/paths-cmd.ts +81 -0
- package/src/cli/commands/skill/paths.ts +191 -0
- package/src/cli/commands/skill/show.ts +73 -0
- package/src/cli/commands/skill/uninstall.ts +141 -0
- package/src/cli/commands/status.ts +205 -0
- package/src/cli/commands/update.ts +68 -0
- package/src/cli/commands/vsearch.ts +188 -0
- package/src/cli/context.ts +64 -0
- package/src/cli/errors.ts +64 -0
- package/src/cli/format/search-results.ts +211 -0
- package/src/cli/options.ts +183 -0
- package/src/cli/program.ts +1330 -0
- package/src/cli/run.ts +213 -0
- package/src/cli/ui.ts +92 -0
- package/src/config/defaults.ts +20 -0
- package/src/config/index.ts +55 -0
- package/src/config/loader.ts +161 -0
- package/src/config/paths.ts +87 -0
- package/src/config/saver.ts +153 -0
- package/src/config/types.ts +280 -0
- package/src/converters/adapters/markitdownTs/adapter.ts +140 -0
- package/src/converters/adapters/officeparser/adapter.ts +126 -0
- package/src/converters/canonicalize.ts +89 -0
- package/src/converters/errors.ts +218 -0
- package/src/converters/index.ts +51 -0
- package/src/converters/mime.ts +163 -0
- package/src/converters/native/markdown.ts +115 -0
- package/src/converters/native/plaintext.ts +56 -0
- package/src/converters/path.ts +48 -0
- package/src/converters/pipeline.ts +159 -0
- package/src/converters/registry.ts +74 -0
- package/src/converters/types.ts +123 -0
- package/src/converters/versions.ts +24 -0
- package/src/index.ts +27 -0
- package/src/ingestion/chunker.ts +238 -0
- package/src/ingestion/index.ts +32 -0
- package/src/ingestion/language.ts +276 -0
- package/src/ingestion/sync.ts +671 -0
- package/src/ingestion/types.ts +219 -0
- package/src/ingestion/walker.ts +235 -0
- package/src/llm/cache.ts +467 -0
- package/src/llm/errors.ts +191 -0
- package/src/llm/index.ts +58 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +133 -0
- package/src/llm/nodeLlamaCpp/embedding.ts +165 -0
- package/src/llm/nodeLlamaCpp/generation.ts +88 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +317 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +94 -0
- package/src/llm/registry.ts +86 -0
- package/src/llm/types.ts +129 -0
- package/src/mcp/resources/index.ts +151 -0
- package/src/mcp/server.ts +229 -0
- package/src/mcp/tools/get.ts +220 -0
- package/src/mcp/tools/index.ts +160 -0
- package/src/mcp/tools/multi-get.ts +263 -0
- package/src/mcp/tools/query.ts +226 -0
- package/src/mcp/tools/search.ts +119 -0
- package/src/mcp/tools/status.ts +81 -0
- package/src/mcp/tools/vsearch.ts +198 -0
- package/src/pipeline/chunk-lookup.ts +44 -0
- package/src/pipeline/expansion.ts +256 -0
- package/src/pipeline/explain.ts +115 -0
- package/src/pipeline/fusion.ts +185 -0
- package/src/pipeline/hybrid.ts +535 -0
- package/src/pipeline/index.ts +64 -0
- package/src/pipeline/query-language.ts +118 -0
- package/src/pipeline/rerank.ts +223 -0
- package/src/pipeline/search.ts +261 -0
- package/src/pipeline/types.ts +328 -0
- package/src/pipeline/vsearch.ts +348 -0
- package/src/store/index.ts +41 -0
- package/src/store/migrations/001-initial.ts +196 -0
- package/src/store/migrations/index.ts +20 -0
- package/src/store/migrations/runner.ts +187 -0
- package/src/store/sqlite/adapter.ts +1242 -0
- package/src/store/sqlite/index.ts +7 -0
- package/src/store/sqlite/setup.ts +129 -0
- package/src/store/sqlite/types.ts +28 -0
- package/src/store/types.ts +506 -0
- package/src/store/vector/index.ts +13 -0
- package/src/store/vector/sqlite-vec.ts +373 -0
- package/src/store/vector/stats.ts +152 -0
- package/src/store/vector/types.ts +115 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI command utilities.
|
|
3
|
+
* Common initialization and formatting helpers.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/commands/shared
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getIndexDbPath } from '../../app/constants';
|
|
9
|
+
import { getConfigPaths, isInitialized, loadConfig } from '../../config';
|
|
10
|
+
import type { Collection, Config } from '../../config/types';
|
|
11
|
+
import type { SyncResult } from '../../ingestion';
|
|
12
|
+
import { SqliteAdapter } from '../../store/sqlite/adapter';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Result of CLI store initialization.
|
|
16
|
+
*/
|
|
17
|
+
export type InitStoreResult =
|
|
18
|
+
| {
|
|
19
|
+
ok: true;
|
|
20
|
+
store: SqliteAdapter;
|
|
21
|
+
config: Config;
|
|
22
|
+
collections: Collection[];
|
|
23
|
+
/** Actual config path used (for status reporting) */
|
|
24
|
+
actualConfigPath: string;
|
|
25
|
+
}
|
|
26
|
+
| { ok: false; error: string };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for store initialization.
|
|
30
|
+
*/
|
|
31
|
+
export interface InitStoreOptions {
|
|
32
|
+
/** Override config path */
|
|
33
|
+
configPath?: string;
|
|
34
|
+
/** Index name (defaults to 'default') */
|
|
35
|
+
indexName?: string;
|
|
36
|
+
/** Filter to single collection by name */
|
|
37
|
+
collection?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize store for CLI commands.
|
|
42
|
+
* Handles: isInitialized check, loadConfig, DB open, syncCollections, syncContexts.
|
|
43
|
+
*
|
|
44
|
+
* Caller is responsible for calling store.close() when done.
|
|
45
|
+
*/
|
|
46
|
+
export async function initStore(
|
|
47
|
+
options: InitStoreOptions = {}
|
|
48
|
+
): Promise<InitStoreResult> {
|
|
49
|
+
// Check if initialized
|
|
50
|
+
const initialized = await isInitialized(options.configPath);
|
|
51
|
+
if (!initialized) {
|
|
52
|
+
return { ok: false, error: 'GNO not initialized. Run: gno init' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Load config
|
|
56
|
+
const configResult = await loadConfig(options.configPath);
|
|
57
|
+
if (!configResult.ok) {
|
|
58
|
+
return { ok: false, error: configResult.error.message };
|
|
59
|
+
}
|
|
60
|
+
const config = configResult.value;
|
|
61
|
+
|
|
62
|
+
// Filter to single collection if specified
|
|
63
|
+
let collections = config.collections;
|
|
64
|
+
if (options.collection) {
|
|
65
|
+
collections = collections.filter((c) => c.name === options.collection);
|
|
66
|
+
if (collections.length === 0) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: `Collection not found: ${options.collection}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (collections.length === 0) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: 'No collections configured. Run: gno collection add <path>',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Ensure data directory exists (may have been deleted by reset)
|
|
82
|
+
const { ensureDirectories } = await import('../../config');
|
|
83
|
+
await ensureDirectories();
|
|
84
|
+
|
|
85
|
+
// Open database (honor indexName option)
|
|
86
|
+
const store = new SqliteAdapter();
|
|
87
|
+
const dbPath = getIndexDbPath(options.indexName);
|
|
88
|
+
const paths = getConfigPaths();
|
|
89
|
+
|
|
90
|
+
// Actual config path used (options.configPath overrides default)
|
|
91
|
+
const actualConfigPath = options.configPath ?? paths.configFile;
|
|
92
|
+
|
|
93
|
+
// Set configPath for status output
|
|
94
|
+
store.setConfigPath(actualConfigPath);
|
|
95
|
+
|
|
96
|
+
const openResult = await store.open(dbPath, config.ftsTokenizer);
|
|
97
|
+
if (!openResult.ok) {
|
|
98
|
+
return { ok: false, error: openResult.error.message };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Sync collections from config to DB
|
|
102
|
+
const syncCollResult = await store.syncCollections(config.collections);
|
|
103
|
+
if (!syncCollResult.ok) {
|
|
104
|
+
await store.close();
|
|
105
|
+
return { ok: false, error: syncCollResult.error.message };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Sync contexts from config to DB
|
|
109
|
+
const syncCtxResult = await store.syncContexts(config.contexts ?? []);
|
|
110
|
+
if (!syncCtxResult.ok) {
|
|
111
|
+
await store.close();
|
|
112
|
+
return { ok: false, error: syncCtxResult.error.message };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { ok: true, store, config, collections, actualConfigPath };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Format sync result lines (shared between update and index commands).
|
|
120
|
+
*/
|
|
121
|
+
export function formatSyncResultLines(
|
|
122
|
+
syncResult: SyncResult,
|
|
123
|
+
options: { verbose?: boolean }
|
|
124
|
+
): string[] {
|
|
125
|
+
const lines: string[] = [];
|
|
126
|
+
|
|
127
|
+
for (const c of syncResult.collections) {
|
|
128
|
+
lines.push(`${c.collection}:`);
|
|
129
|
+
lines.push(
|
|
130
|
+
` ${c.filesAdded} added, ${c.filesUpdated} updated, ${c.filesUnchanged} unchanged`
|
|
131
|
+
);
|
|
132
|
+
if (c.filesErrored > 0) {
|
|
133
|
+
lines.push(` ${c.filesErrored} errors`);
|
|
134
|
+
}
|
|
135
|
+
if (c.filesMarkedInactive > 0) {
|
|
136
|
+
lines.push(` ${c.filesMarkedInactive} marked inactive`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (options.verbose && c.errors.length > 0) {
|
|
140
|
+
for (const err of c.errors) {
|
|
141
|
+
lines.push(` [${err.code}] ${err.relPath}: ${err.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push(
|
|
148
|
+
`Total: ${syncResult.totalFilesAdded} added, ${syncResult.totalFilesUpdated} updated` +
|
|
149
|
+
(syncResult.totalFilesErrored > 0
|
|
150
|
+
? `, ${syncResult.totalFilesErrored} errors`
|
|
151
|
+
: '')
|
|
152
|
+
);
|
|
153
|
+
lines.push(`Duration: ${syncResult.totalDurationMs}ms`);
|
|
154
|
+
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill command exports.
|
|
3
|
+
*
|
|
4
|
+
* @module src/cli/commands/skill
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { type InstallOptions, installSkill } from './install.js';
|
|
8
|
+
export {
|
|
9
|
+
resolveAllPaths,
|
|
10
|
+
resolveSkillPaths,
|
|
11
|
+
type SkillPathOptions,
|
|
12
|
+
type SkillPaths,
|
|
13
|
+
type SkillScope,
|
|
14
|
+
type SkillTarget,
|
|
15
|
+
validatePathForDeletion,
|
|
16
|
+
} from './paths.js';
|
|
17
|
+
export { type PathsOptions, showPaths } from './paths-cmd.js';
|
|
18
|
+
export { type ShowOptions, showSkill } from './show.js';
|
|
19
|
+
export { type UninstallOptions, uninstallSkill } from './uninstall.js';
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install GNO agent skill to Claude Code or Codex.
|
|
3
|
+
* Atomic install via temp directory + rename.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/commands/skill/install
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdir, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { CliError } from '../../errors.js';
|
|
12
|
+
import { getGlobals } from '../../program.js';
|
|
13
|
+
import {
|
|
14
|
+
resolveSkillPaths,
|
|
15
|
+
type SkillScope,
|
|
16
|
+
type SkillTarget,
|
|
17
|
+
validatePathForDeletion,
|
|
18
|
+
} from './paths.js';
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Source Path Resolution
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get path to skill source files.
|
|
26
|
+
* Works in both dev (src/) and after build (dist/).
|
|
27
|
+
*/
|
|
28
|
+
function getSkillSourceDir(): string {
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
// From src/cli/commands/skill/ -> assets/skill/
|
|
31
|
+
// Or from dist/cli/commands/skill/ -> assets/skill/
|
|
32
|
+
return join(__dirname, '../../../../assets/skill');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// Install Command
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export interface InstallOptions {
|
|
40
|
+
scope?: SkillScope;
|
|
41
|
+
target?: SkillTarget | 'all';
|
|
42
|
+
force?: boolean;
|
|
43
|
+
/** Override for testing */
|
|
44
|
+
cwd?: string;
|
|
45
|
+
/** Override for testing */
|
|
46
|
+
homeDir?: string;
|
|
47
|
+
/** JSON output (defaults to globals.json) */
|
|
48
|
+
json?: boolean;
|
|
49
|
+
/** Non-interactive mode (defaults to globals.yes) */
|
|
50
|
+
yes?: boolean;
|
|
51
|
+
/** Quiet mode (defaults to globals.quiet) */
|
|
52
|
+
quiet?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface InstallResult {
|
|
56
|
+
target: SkillTarget;
|
|
57
|
+
scope: SkillScope;
|
|
58
|
+
path: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Install skill to a single target.
|
|
63
|
+
*/
|
|
64
|
+
async function installToTarget(
|
|
65
|
+
scope: SkillScope,
|
|
66
|
+
target: SkillTarget,
|
|
67
|
+
force: boolean,
|
|
68
|
+
overrides?: { cwd?: string; homeDir?: string }
|
|
69
|
+
): Promise<InstallResult> {
|
|
70
|
+
const sourceDir = getSkillSourceDir();
|
|
71
|
+
const paths = resolveSkillPaths({ scope, target, ...overrides });
|
|
72
|
+
|
|
73
|
+
// Check if already exists (directory or SKILL.md)
|
|
74
|
+
const skillMdExists = await Bun.file(join(paths.gnoDir, 'SKILL.md')).exists();
|
|
75
|
+
let dirExists = false;
|
|
76
|
+
try {
|
|
77
|
+
const dirStat = await stat(paths.gnoDir);
|
|
78
|
+
dirExists = dirStat.isDirectory();
|
|
79
|
+
} catch {
|
|
80
|
+
// Directory doesn't exist
|
|
81
|
+
}
|
|
82
|
+
const destExists = skillMdExists || dirExists;
|
|
83
|
+
|
|
84
|
+
if (destExists && !force) {
|
|
85
|
+
throw new CliError(
|
|
86
|
+
'VALIDATION',
|
|
87
|
+
`Skill already installed at ${paths.gnoDir}. Use --force to overwrite.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Read source files
|
|
92
|
+
const sourceFiles = await readdir(sourceDir);
|
|
93
|
+
if (sourceFiles.length === 0) {
|
|
94
|
+
throw new CliError('RUNTIME', `No skill files found in ${sourceDir}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create temp directory with unique name to avoid collisions
|
|
98
|
+
const randomSuffix = Math.random().toString(36).slice(2, 10);
|
|
99
|
+
const tmpName = `.gno-skill.tmp.${Date.now()}-${process.pid}-${randomSuffix}`;
|
|
100
|
+
const tmpDir = join(paths.skillsDir, tmpName);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Ensure skills directory exists
|
|
104
|
+
await mkdir(paths.skillsDir, { recursive: true });
|
|
105
|
+
|
|
106
|
+
// Create temp directory
|
|
107
|
+
await mkdir(tmpDir, { recursive: true });
|
|
108
|
+
|
|
109
|
+
// Copy all files to temp (binary-safe)
|
|
110
|
+
for (const file of sourceFiles) {
|
|
111
|
+
const content = await Bun.file(join(sourceDir, file)).arrayBuffer();
|
|
112
|
+
await Bun.write(join(tmpDir, file), content);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Remove existing if present (with safety check)
|
|
116
|
+
if (destExists) {
|
|
117
|
+
const validationError = validatePathForDeletion(paths.gnoDir, paths.base);
|
|
118
|
+
if (validationError) {
|
|
119
|
+
throw new CliError(
|
|
120
|
+
'RUNTIME',
|
|
121
|
+
`Safety check failed for ${paths.gnoDir}: ${validationError}`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
await rm(paths.gnoDir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Atomic rename
|
|
128
|
+
await rename(tmpDir, paths.gnoDir);
|
|
129
|
+
|
|
130
|
+
return { target, scope, path: paths.gnoDir };
|
|
131
|
+
} catch (err) {
|
|
132
|
+
// Best-effort cleanup
|
|
133
|
+
try {
|
|
134
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
135
|
+
} catch {
|
|
136
|
+
// Ignore cleanup errors
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (err instanceof CliError) {
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
throw new CliError(
|
|
144
|
+
'RUNTIME',
|
|
145
|
+
`Failed to install skill: ${err instanceof Error ? err.message : String(err)}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get globals with fallback for testing.
|
|
152
|
+
*/
|
|
153
|
+
function safeGetGlobals(): { json: boolean; yes: boolean; quiet: boolean } {
|
|
154
|
+
try {
|
|
155
|
+
return getGlobals();
|
|
156
|
+
} catch {
|
|
157
|
+
return { json: false, yes: false, quiet: false };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Install GNO skill.
|
|
163
|
+
*/
|
|
164
|
+
export async function installSkill(opts: InstallOptions = {}): Promise<void> {
|
|
165
|
+
const scope = opts.scope ?? 'project';
|
|
166
|
+
const target = opts.target ?? 'claude';
|
|
167
|
+
const force = opts.force ?? false;
|
|
168
|
+
const globals = safeGetGlobals();
|
|
169
|
+
const json = opts.json ?? globals.json;
|
|
170
|
+
const yes = opts.yes ?? globals.yes;
|
|
171
|
+
const quiet = opts.quiet ?? globals.quiet;
|
|
172
|
+
|
|
173
|
+
const targets: SkillTarget[] =
|
|
174
|
+
target === 'all' ? ['claude', 'codex'] : [target];
|
|
175
|
+
|
|
176
|
+
const results: InstallResult[] = [];
|
|
177
|
+
|
|
178
|
+
for (const t of targets) {
|
|
179
|
+
const result = await installToTarget(scope, t, force || yes, {
|
|
180
|
+
cwd: opts.cwd,
|
|
181
|
+
homeDir: opts.homeDir,
|
|
182
|
+
});
|
|
183
|
+
results.push(result);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Output
|
|
187
|
+
if (json) {
|
|
188
|
+
process.stdout.write(
|
|
189
|
+
`${JSON.stringify({ installed: results }, null, 2)}\n`
|
|
190
|
+
);
|
|
191
|
+
} else if (!quiet) {
|
|
192
|
+
for (const r of results) {
|
|
193
|
+
process.stdout.write(`Installed GNO skill to ${r.path}\n`);
|
|
194
|
+
}
|
|
195
|
+
process.stdout.write('\nRestart your agent to load the skill.\n');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show resolved skill installation paths.
|
|
3
|
+
* Debugging helper for skill install/uninstall.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/commands/skill/paths-cmd
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { getGlobals } from '../../program.js';
|
|
10
|
+
import { resolveAllPaths, type SkillScope, type SkillTarget } from './paths.js';
|
|
11
|
+
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
// Paths Command
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface PathsOptions {
|
|
17
|
+
scope?: SkillScope | 'all';
|
|
18
|
+
target?: SkillTarget | 'all';
|
|
19
|
+
/** Override for testing */
|
|
20
|
+
cwd?: string;
|
|
21
|
+
/** Override for testing */
|
|
22
|
+
homeDir?: string;
|
|
23
|
+
/** JSON output (defaults to globals.json) */
|
|
24
|
+
json?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface PathInfo {
|
|
28
|
+
target: SkillTarget;
|
|
29
|
+
scope: SkillScope;
|
|
30
|
+
path: string;
|
|
31
|
+
exists: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get globals with fallback for testing.
|
|
36
|
+
*/
|
|
37
|
+
function safeGetGlobals(): { json: boolean } {
|
|
38
|
+
try {
|
|
39
|
+
return getGlobals();
|
|
40
|
+
} catch {
|
|
41
|
+
return { json: false };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Show resolved skill paths.
|
|
47
|
+
*/
|
|
48
|
+
export async function showPaths(opts: PathsOptions = {}): Promise<void> {
|
|
49
|
+
const scope = opts.scope ?? 'all';
|
|
50
|
+
const target = opts.target ?? 'all';
|
|
51
|
+
const globals = safeGetGlobals();
|
|
52
|
+
const json = opts.json ?? globals.json;
|
|
53
|
+
|
|
54
|
+
const resolved = resolveAllPaths(scope, target, {
|
|
55
|
+
cwd: opts.cwd,
|
|
56
|
+
homeDir: opts.homeDir,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const results: PathInfo[] = [];
|
|
60
|
+
|
|
61
|
+
for (const r of resolved) {
|
|
62
|
+
const skillMdPath = join(r.paths.gnoDir, 'SKILL.md');
|
|
63
|
+
const exists = await Bun.file(skillMdPath).exists();
|
|
64
|
+
results.push({
|
|
65
|
+
target: r.target,
|
|
66
|
+
scope: r.scope,
|
|
67
|
+
path: r.paths.gnoDir,
|
|
68
|
+
exists,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (json) {
|
|
73
|
+
process.stdout.write(`${JSON.stringify({ paths: results }, null, 2)}\n`);
|
|
74
|
+
} else {
|
|
75
|
+
process.stdout.write('GNO Skill Paths:\n\n');
|
|
76
|
+
for (const r of results) {
|
|
77
|
+
const status = r.exists ? '(installed)' : '(not installed)';
|
|
78
|
+
process.stdout.write(` ${r.target}/${r.scope}: ${r.path} ${status}\n`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path resolution for skill installation.
|
|
3
|
+
* Supports Claude Code and Codex targets with project/user scopes.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/commands/skill/paths
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { isAbsolute, join, normalize, relative, sep } from 'node:path';
|
|
10
|
+
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
// Environment Variables
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** Override home dir for user scope (testing) */
|
|
16
|
+
export const ENV_SKILLS_HOME_OVERRIDE = 'GNO_SKILLS_HOME_OVERRIDE';
|
|
17
|
+
|
|
18
|
+
/** Override Claude skills directory */
|
|
19
|
+
export const ENV_CLAUDE_SKILLS_DIR = 'CLAUDE_SKILLS_DIR';
|
|
20
|
+
|
|
21
|
+
/** Override Codex skills directory */
|
|
22
|
+
export const ENV_CODEX_SKILLS_DIR = 'CODEX_SKILLS_DIR';
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Types
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export type SkillScope = 'project' | 'user';
|
|
29
|
+
export type SkillTarget = 'claude' | 'codex';
|
|
30
|
+
|
|
31
|
+
export interface SkillPathOptions {
|
|
32
|
+
scope: SkillScope;
|
|
33
|
+
target: SkillTarget;
|
|
34
|
+
/** Override cwd for project scope (testing) */
|
|
35
|
+
cwd?: string;
|
|
36
|
+
/** Override home dir for user scope (testing) */
|
|
37
|
+
homeDir?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SkillPaths {
|
|
41
|
+
/** Base directory (e.g., ~/.claude or ./.claude) */
|
|
42
|
+
base: string;
|
|
43
|
+
/** Skills directory (e.g., ~/.claude/skills) */
|
|
44
|
+
skillsDir: string;
|
|
45
|
+
/** GNO skill directory (e.g., ~/.claude/skills/gno) */
|
|
46
|
+
gnoDir: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
// Constants
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/** Skill name for the gno skill directory */
|
|
54
|
+
export const SKILL_NAME = 'gno';
|
|
55
|
+
|
|
56
|
+
/** Directory name for skills within agent config */
|
|
57
|
+
const SKILLS_SUBDIR = 'skills';
|
|
58
|
+
|
|
59
|
+
/** Agent config directory names */
|
|
60
|
+
const AGENT_DIRS: Record<SkillTarget, string> = {
|
|
61
|
+
claude: '.claude',
|
|
62
|
+
codex: '.codex',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
// Path Resolution
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve skill installation paths for a given scope and target.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveSkillPaths(opts: SkillPathOptions): SkillPaths {
|
|
73
|
+
const { scope, target, cwd, homeDir } = opts;
|
|
74
|
+
|
|
75
|
+
// Check for env overrides first
|
|
76
|
+
const envOverride =
|
|
77
|
+
target === 'claude'
|
|
78
|
+
? process.env[ENV_CLAUDE_SKILLS_DIR]
|
|
79
|
+
: process.env[ENV_CODEX_SKILLS_DIR];
|
|
80
|
+
|
|
81
|
+
if (envOverride) {
|
|
82
|
+
// Require absolute path for security
|
|
83
|
+
if (!isAbsolute(envOverride)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`${target === 'claude' ? ENV_CLAUDE_SKILLS_DIR : ENV_CODEX_SKILLS_DIR} must be an absolute path`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const skillsDir = normalize(envOverride);
|
|
89
|
+
return {
|
|
90
|
+
base: join(skillsDir, '..'),
|
|
91
|
+
skillsDir,
|
|
92
|
+
gnoDir: join(skillsDir, SKILL_NAME),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Resolve base directory
|
|
97
|
+
const agentDir = AGENT_DIRS[target];
|
|
98
|
+
let base: string;
|
|
99
|
+
|
|
100
|
+
if (scope === 'user') {
|
|
101
|
+
const home = homeDir ?? process.env[ENV_SKILLS_HOME_OVERRIDE] ?? homedir();
|
|
102
|
+
base = join(home, agentDir);
|
|
103
|
+
} else {
|
|
104
|
+
const projectRoot = cwd ?? process.cwd();
|
|
105
|
+
base = join(projectRoot, agentDir);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const skillsDir = join(base, SKILLS_SUBDIR);
|
|
109
|
+
const gnoDir = join(skillsDir, SKILL_NAME);
|
|
110
|
+
|
|
111
|
+
return { base, skillsDir, gnoDir };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve paths for all targets given scope options.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveAllPaths(
|
|
118
|
+
scope: SkillScope | 'all',
|
|
119
|
+
target: SkillTarget | 'all',
|
|
120
|
+
overrides?: { cwd?: string; homeDir?: string }
|
|
121
|
+
): Array<{ scope: SkillScope; target: SkillTarget; paths: SkillPaths }> {
|
|
122
|
+
const scopes: SkillScope[] = scope === 'all' ? ['project', 'user'] : [scope];
|
|
123
|
+
const targets: SkillTarget[] =
|
|
124
|
+
target === 'all' ? ['claude', 'codex'] : [target];
|
|
125
|
+
|
|
126
|
+
const results: Array<{
|
|
127
|
+
scope: SkillScope;
|
|
128
|
+
target: SkillTarget;
|
|
129
|
+
paths: SkillPaths;
|
|
130
|
+
}> = [];
|
|
131
|
+
|
|
132
|
+
for (const s of scopes) {
|
|
133
|
+
for (const t of targets) {
|
|
134
|
+
results.push({
|
|
135
|
+
scope: s,
|
|
136
|
+
target: t,
|
|
137
|
+
paths: resolveSkillPaths({ scope: s, target: t, ...overrides }),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
146
|
+
// Safety Validation
|
|
147
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Expected path suffix for gno skill directory.
|
|
151
|
+
* Platform-aware (handles Windows backslash).
|
|
152
|
+
*/
|
|
153
|
+
function getExpectedSuffix(): string {
|
|
154
|
+
return `${sep}${SKILLS_SUBDIR}${sep}${SKILL_NAME}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Validate that a path is safe to delete.
|
|
159
|
+
* Returns null if safe, or error message if unsafe.
|
|
160
|
+
*/
|
|
161
|
+
export function validatePathForDeletion(
|
|
162
|
+
destDir: string,
|
|
163
|
+
base: string
|
|
164
|
+
): string | null {
|
|
165
|
+
const normalized = normalize(destDir);
|
|
166
|
+
const normalizedBase = normalize(base);
|
|
167
|
+
const expectedSuffix = getExpectedSuffix();
|
|
168
|
+
|
|
169
|
+
// Must end with /skills/gno (or \skills\gno on Windows)
|
|
170
|
+
if (!normalized.endsWith(expectedSuffix)) {
|
|
171
|
+
return `Path does not end with expected suffix (${expectedSuffix})`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Minimum length sanity check
|
|
175
|
+
if (normalized.length < 20) {
|
|
176
|
+
return 'Path is suspiciously short';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Must not equal base
|
|
180
|
+
if (normalized === normalizedBase) {
|
|
181
|
+
return 'Path equals base directory';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Must be strictly inside expected base (proper containment check)
|
|
185
|
+
const rel = relative(normalizedBase, normalized);
|
|
186
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
187
|
+
return 'Path is not inside expected base directory';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}
|