gyrus 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gyrus",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Model-agnostic knowledge management CLI with MCP server support",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Skill CLI Command
3
+ * Handles all skill-related CLI operations
4
+ *
5
+ * Usage:
6
+ * gyrus skill list [--workspace <name>] [--tags <t1,t2>]
7
+ * gyrus skill read <name> [--workspace <name>]
8
+ * gyrus skill search <query> [--workspace <name>] [--tags <t1,t2>] [--trigger <t>]
9
+ * gyrus skill create <name> --title <title> --content <content> [options]
10
+ * gyrus skill update <name> [--title <title>] [--content <content>] [--tags <t1,t2>]
11
+ *
12
+ * Aliases: gyrus s
13
+ */
14
+
15
+ import { readConfig } from "../config/index.ts";
16
+ import { WorkspaceManager } from "../services/workspace.ts";
17
+ import {
18
+ listSkills,
19
+ readSkill,
20
+ searchSkills,
21
+ createSkill,
22
+ updateSkill,
23
+ } from "../operations/skill.ts";
24
+ import {
25
+ formatSkillListCli,
26
+ formatSkillReadCli,
27
+ formatSkillSearchCli,
28
+ formatSkillCreateCli,
29
+ formatSkillUpdateCli,
30
+ } from "../formatters/skill.ts";
31
+
32
+ const HELP = `
33
+ Usage: gyrus skill <subcommand> [options]
34
+
35
+ Subcommands:
36
+ list List all skills
37
+ read <name> Read a specific skill by name
38
+ search <query> Search skills by text query
39
+ create Create a new skill (with flags)
40
+ update <name> Update an existing skill
41
+
42
+ Global Options:
43
+ -w, --workspace <name> Target workspace (default: active workspace)
44
+ -h, --help Show this help message
45
+
46
+ List Options:
47
+ -t, --tags <t1,t2,...> Filter by tags (comma-separated)
48
+
49
+ Search Options:
50
+ -t, --tags <t1,t2,...> Filter by tags
51
+ --trigger <trigger> Filter by trigger pattern
52
+
53
+ Create Options (name and title required):
54
+ --name <name> Skill name in kebab-case
55
+ --title <title> Skill title
56
+ --content <content> Skill content (or use --file)
57
+ --file <path> Read content from file
58
+ -t, --tags <t1,t2,...> Tags (comma-separated)
59
+ --triggers <t1,t2,...> Trigger patterns (comma-separated)
60
+ --apply-to <a1,a2,...> Apply to file patterns (comma-separated)
61
+ --related <id1,id2,...> Related skill names
62
+ --description <desc> Brief description
63
+
64
+ Update Options:
65
+ --title <title> New title
66
+ --content <content> New content (or use --file)
67
+ --file <path> Read content from file
68
+ -t, --tags <t1,t2,...> New tags (replaces existing)
69
+ --triggers <t1,t2,...> New triggers (replaces existing)
70
+ --apply-to <a1,a2,...> New apply-to patterns (replaces existing)
71
+ --related <id1,id2,...> New related names (replaces existing)
72
+ --description <desc> New description
73
+
74
+ Examples:
75
+ gyrus skill list
76
+ gyrus skill list --tags typescript,testing
77
+ gyrus skill read code-review
78
+ gyrus skill search "testing" --tags quality
79
+ gyrus skill create --name code-review --title "Code Review Guide" --content "# Code Review..."
80
+ gyrus s list -w work
81
+
82
+ Aliases: s
83
+ `;
84
+
85
+ /**
86
+ * Parse command-line arguments into options object
87
+ */
88
+ function parseArgs(args: string[]): {
89
+ subcommand: string;
90
+ positional: string[];
91
+ options: Record<string, string | boolean>;
92
+ } {
93
+ const options: Record<string, string | boolean> = {};
94
+ const positional: string[] = [];
95
+ let subcommand = "";
96
+
97
+ let i = 0;
98
+
99
+ // First non-flag argument is the subcommand
100
+ while (i < args.length && !subcommand) {
101
+ if (!args[i].startsWith("-")) {
102
+ subcommand = args[i];
103
+ i++;
104
+ break;
105
+ }
106
+ i++;
107
+ }
108
+
109
+ // Parse remaining arguments
110
+ while (i < args.length) {
111
+ const arg = args[i];
112
+
113
+ if (arg === "-h" || arg === "--help") {
114
+ options.help = true;
115
+ i++;
116
+ } else if (arg === "-w" || arg === "--workspace") {
117
+ options.workspace = args[++i] || "";
118
+ i++;
119
+ } else if (arg === "-t" || arg === "--tags") {
120
+ options.tags = args[++i] || "";
121
+ i++;
122
+ } else if (arg === "--name") {
123
+ options.name = args[++i] || "";
124
+ i++;
125
+ } else if (arg === "--title") {
126
+ options.title = args[++i] || "";
127
+ i++;
128
+ } else if (arg === "--content") {
129
+ options.content = args[++i] || "";
130
+ i++;
131
+ } else if (arg === "--file") {
132
+ options.file = args[++i] || "";
133
+ i++;
134
+ } else if (arg === "--triggers") {
135
+ options.triggers = args[++i] || "";
136
+ i++;
137
+ } else if (arg === "--trigger") {
138
+ options.trigger = args[++i] || "";
139
+ i++;
140
+ } else if (arg === "--apply-to") {
141
+ options.applyTo = args[++i] || "";
142
+ i++;
143
+ } else if (arg === "--related") {
144
+ options.related = args[++i] || "";
145
+ i++;
146
+ } else if (arg === "--description") {
147
+ options.description = args[++i] || "";
148
+ i++;
149
+ } else if (!arg.startsWith("-")) {
150
+ positional.push(arg);
151
+ i++;
152
+ } else {
153
+ // Unknown flag, skip
154
+ i++;
155
+ }
156
+ }
157
+
158
+ return { subcommand, positional, options };
159
+ }
160
+
161
+ /**
162
+ * Parse comma-separated string into array
163
+ */
164
+ function parseList(value: string | undefined): string[] | undefined {
165
+ if (!value || typeof value !== "string") return undefined;
166
+ return value
167
+ .split(",")
168
+ .map((s) => s.trim())
169
+ .filter(Boolean);
170
+ }
171
+
172
+ /**
173
+ * Read content from file if --file option is provided
174
+ */
175
+ async function getContent(
176
+ options: Record<string, string | boolean>,
177
+ ): Promise<string | undefined> {
178
+ if (options.file && typeof options.file === "string") {
179
+ try {
180
+ const file = Bun.file(options.file);
181
+ return await file.text();
182
+ } catch (error) {
183
+ console.error(`Error reading file: ${options.file}`);
184
+ process.exit(1);
185
+ }
186
+ }
187
+ return typeof options.content === "string" ? options.content : undefined;
188
+ }
189
+
190
+ /**
191
+ * Handle the list subcommand
192
+ */
193
+ async function handleList(
194
+ manager: WorkspaceManager,
195
+ options: Record<string, string | boolean>,
196
+ ): Promise<void> {
197
+ const result = await listSkills(manager, {
198
+ workspace:
199
+ typeof options.workspace === "string" ? options.workspace : undefined,
200
+ tags: parseList(options.tags as string),
201
+ });
202
+
203
+ if (!result.success) {
204
+ console.error(`Error: ${result.error}`);
205
+ process.exit(1);
206
+ }
207
+
208
+ console.log(formatSkillListCli(result.data!));
209
+ }
210
+
211
+ /**
212
+ * Handle the read subcommand
213
+ */
214
+ async function handleRead(
215
+ manager: WorkspaceManager,
216
+ positional: string[],
217
+ options: Record<string, string | boolean>,
218
+ ): Promise<void> {
219
+ const name = positional[0];
220
+
221
+ if (!name) {
222
+ console.error("Error: Please provide a skill name");
223
+ console.error("Usage: gyrus skill read <name>");
224
+ process.exit(1);
225
+ }
226
+
227
+ const result = await readSkill(manager, {
228
+ workspace:
229
+ typeof options.workspace === "string" ? options.workspace : undefined,
230
+ name,
231
+ });
232
+
233
+ if (!result.success) {
234
+ console.error(`Error: ${result.error}`);
235
+ process.exit(1);
236
+ }
237
+
238
+ console.log(formatSkillReadCli(result.data!));
239
+ }
240
+
241
+ /**
242
+ * Handle the search subcommand
243
+ */
244
+ async function handleSearch(
245
+ manager: WorkspaceManager,
246
+ positional: string[],
247
+ options: Record<string, string | boolean>,
248
+ ): Promise<void> {
249
+ const query = positional[0];
250
+ const tags = parseList(options.tags as string);
251
+ const trigger =
252
+ typeof options.trigger === "string" ? options.trigger : undefined;
253
+
254
+ if (!query && !tags?.length && !trigger) {
255
+ console.error("Error: Please provide a search query, tags, or trigger");
256
+ console.error(
257
+ "Usage: gyrus skill search <query> [--tags <t1,t2>] [--trigger <t>]",
258
+ );
259
+ process.exit(1);
260
+ }
261
+
262
+ const result = await searchSkills(manager, {
263
+ workspace:
264
+ typeof options.workspace === "string" ? options.workspace : undefined,
265
+ query,
266
+ tags,
267
+ trigger,
268
+ });
269
+
270
+ if (!result.success) {
271
+ console.error(`Error: ${result.error}`);
272
+ process.exit(1);
273
+ }
274
+
275
+ console.log(formatSkillSearchCli(result.data!));
276
+ }
277
+
278
+ /**
279
+ * Handle the create subcommand
280
+ */
281
+ async function handleCreate(
282
+ manager: WorkspaceManager,
283
+ options: Record<string, string | boolean>,
284
+ ): Promise<void> {
285
+ const name = typeof options.name === "string" ? options.name : undefined;
286
+ const title = typeof options.title === "string" ? options.title : undefined;
287
+ const content = await getContent(options);
288
+
289
+ if (!name || !title || !content) {
290
+ console.error("Error: Missing required options for create");
291
+ console.error("Required: --name, --title, --content (or --file)");
292
+ console.error("\nExample:");
293
+ console.error(
294
+ ' gyrus skill create --name code-review --title "Code Review Guide" --content "# Instructions..."',
295
+ );
296
+ process.exit(1);
297
+ }
298
+
299
+ const result = await createSkill(manager, {
300
+ workspace:
301
+ typeof options.workspace === "string" ? options.workspace : undefined,
302
+ name,
303
+ title,
304
+ content,
305
+ description:
306
+ typeof options.description === "string" ? options.description : undefined,
307
+ tags: parseList(options.tags as string),
308
+ triggers: parseList(options.triggers as string),
309
+ applyTo: parseList(options.applyTo as string),
310
+ related: parseList(options.related as string),
311
+ });
312
+
313
+ if (!result.success) {
314
+ console.error(`Error: ${result.error}`);
315
+ process.exit(1);
316
+ }
317
+
318
+ console.log(formatSkillCreateCli(result.data!));
319
+ }
320
+
321
+ /**
322
+ * Handle the update subcommand
323
+ */
324
+ async function handleUpdate(
325
+ manager: WorkspaceManager,
326
+ positional: string[],
327
+ options: Record<string, string | boolean>,
328
+ ): Promise<void> {
329
+ const name = positional[0];
330
+
331
+ if (!name) {
332
+ console.error("Error: Please provide a skill name to update");
333
+ console.error("Usage: gyrus skill update <name> [options]");
334
+ process.exit(1);
335
+ }
336
+
337
+ const content = await getContent(options);
338
+ const hasUpdates =
339
+ options.title ||
340
+ content ||
341
+ options.tags ||
342
+ options.triggers ||
343
+ options.applyTo ||
344
+ options.related ||
345
+ options.description;
346
+
347
+ if (!hasUpdates) {
348
+ console.error("Error: No update options provided");
349
+ console.error(
350
+ "Available options: --title, --content, --file, --tags, --triggers, --apply-to, --related, --description",
351
+ );
352
+ process.exit(1);
353
+ }
354
+
355
+ const result = await updateSkill(manager, {
356
+ workspace:
357
+ typeof options.workspace === "string" ? options.workspace : undefined,
358
+ name,
359
+ title: typeof options.title === "string" ? options.title : undefined,
360
+ content,
361
+ description:
362
+ typeof options.description === "string" ? options.description : undefined,
363
+ tags: parseList(options.tags as string),
364
+ triggers: parseList(options.triggers as string),
365
+ applyTo: parseList(options.applyTo as string),
366
+ related: parseList(options.related as string),
367
+ });
368
+
369
+ if (!result.success) {
370
+ console.error(`Error: ${result.error}`);
371
+ process.exit(1);
372
+ }
373
+
374
+ console.log(formatSkillUpdateCli(result.data!));
375
+ }
376
+
377
+ /**
378
+ * Main skill command handler
379
+ */
380
+ export async function skillCommand(args: string[]): Promise<void> {
381
+ const { subcommand, positional, options } = parseArgs(args);
382
+
383
+ // Show help if requested or no subcommand
384
+ if (options.help || !subcommand) {
385
+ console.log(HELP);
386
+ process.exit(0);
387
+ }
388
+
389
+ // Load config and create workspace manager
390
+ const config = readConfig();
391
+ if (!config) {
392
+ console.error("Error: No configuration found. Run `gyrus init` first.");
393
+ process.exit(1);
394
+ }
395
+
396
+ const manager = new WorkspaceManager(config);
397
+
398
+ // Route to subcommand handler
399
+ switch (subcommand) {
400
+ case "list":
401
+ case "ls":
402
+ await handleList(manager, options);
403
+ break;
404
+
405
+ case "read":
406
+ case "get":
407
+ case "show":
408
+ await handleRead(manager, positional, options);
409
+ break;
410
+
411
+ case "search":
412
+ case "find":
413
+ await handleSearch(manager, positional, options);
414
+ break;
415
+
416
+ case "create":
417
+ case "new":
418
+ case "add":
419
+ await handleCreate(manager, options);
420
+ break;
421
+
422
+ case "update":
423
+ case "edit":
424
+ await handleUpdate(manager, positional, options);
425
+ break;
426
+
427
+ default:
428
+ console.error(`Unknown subcommand: ${subcommand}`);
429
+ console.log("\nRun `gyrus skill --help` for usage information.");
430
+ process.exit(1);
431
+ }
432
+ }
@@ -131,7 +131,10 @@ export function getOrCreateConfig(): GyrusConfig {
131
131
  /**
132
132
  * Get a workspace by name
133
133
  */
134
- export function getWorkspace(config: GyrusConfig, name: string): Workspace | null {
134
+ export function getWorkspace(
135
+ config: GyrusConfig,
136
+ name: string,
137
+ ): Workspace | null {
135
138
  return config.workspaces.find((ws) => ws.name === name) ?? null;
136
139
  }
137
140
 
@@ -148,7 +151,7 @@ export function getDefaultWorkspace(config: GyrusConfig): Workspace | null {
148
151
  */
149
152
  export function addWorkspace(
150
153
  config: GyrusConfig,
151
- workspace: Workspace
154
+ workspace: Workspace,
152
155
  ): string | null {
153
156
  // Validate name format
154
157
  if (!isValidWorkspaceName(workspace.name)) {
@@ -162,9 +165,7 @@ export function addWorkspace(
162
165
 
163
166
  // Check for duplicate path
164
167
  const expandedPath = expandPath(workspace.path);
165
- if (
166
- config.workspaces.some((ws) => expandPath(ws.path) === expandedPath)
167
- ) {
168
+ if (config.workspaces.some((ws) => expandPath(ws.path) === expandedPath)) {
168
169
  return `A workspace already exists at "${workspace.path}".`;
169
170
  }
170
171
 
@@ -178,7 +179,7 @@ export function addWorkspace(
178
179
  */
179
180
  export function removeWorkspace(
180
181
  config: GyrusConfig,
181
- name: string
182
+ name: string,
182
183
  ): string | null {
183
184
  const index = config.workspaces.findIndex((ws) => ws.name === name);
184
185
 
@@ -207,7 +208,7 @@ export function removeWorkspace(
207
208
  */
208
209
  export function setDefaultWorkspace(
209
210
  config: GyrusConfig,
210
- name: string
211
+ name: string,
211
212
  ): string | null {
212
213
  if (!config.workspaces.some((ws) => ws.name === name)) {
213
214
  return `Workspace "${name}" not found.`;
@@ -224,12 +225,14 @@ export function getWorkspacePaths(workspace: Workspace): {
224
225
  root: string;
225
226
  knowledge: string;
226
227
  adrs: string;
228
+ skills: string;
227
229
  } {
228
230
  const root = expandPath(workspace.path);
229
231
  return {
230
232
  root,
231
233
  knowledge: join(root, "knowledge"),
232
234
  adrs: join(root, "adrs"),
235
+ skills: join(root, "skills"),
233
236
  };
234
237
  }
235
238