glab-agent 0.2.0 → 0.2.2

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": "glab-agent",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Multi-agent GitLab To-Do watcher with YAML-defined agents, skills, and GitLab registry.",
6
6
  "license": "MIT",
@@ -31,6 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "scripts": {
34
+ "prepare": "git config core.hooksPath scripts/hooks",
34
35
  "build": "tsc -p tsconfig.build.json",
35
36
  "test": "vitest run",
36
37
  "agent": "tsx src/local-agent/cli.ts",
@@ -83,8 +83,8 @@ Options:
83
83
  --project <dir> Target project directory (default: cwd)
84
84
 
85
85
  Commands:
86
- init [name] Initialize .glab-agent/ and optionally create an agent
87
- --provider <claude|codex> --labels <l1,l2> --token-env <VAR>
86
+ init [name] Initialize agent config (--repo for full Agents repo setup)
87
+ --provider <claude|codex> --labels <l1,l2> --repo -h
88
88
  list List all agent definitions
89
89
  start <name|--all> Start agent(s) as background watcher process
90
90
  stop <name|--all> Stop agent(s)
@@ -104,6 +104,9 @@ Commands:
104
104
  skills import <url> Import skills from GitHub repo (--force to overwrite)
105
105
  wiki setup Populate project GitLab Wiki with documentation pages
106
106
  doctor Check prerequisites and environment
107
+
108
+ Options:
109
+ --version, -v Show version
107
110
  `);
108
111
  }
109
112
 
@@ -457,6 +460,12 @@ export async function validateGitlabToken(host: string, token: string): Promise<
457
460
  }
458
461
 
459
462
  async function cmdInit(name?: string, options?: { provider?: string; labels?: string; tokenEnv?: string; repo?: boolean; botUsername?: string }): Promise<void> {
463
+ // Warn if not in a git repository
464
+ if (!existsSync(path.join(PROJECT_DIR, ".git"))) {
465
+ console.warn(`⚠️ No .git/ found in ${PROJECT_DIR}. Are you in the right directory?`);
466
+ console.warn(` Agents repos should be git repositories. Run 'git init' first.\n`);
467
+ }
468
+
460
469
  const base = agentBaseDir(PROJECT_DIR);
461
470
  const dirs = [
462
471
  path.join(base, "agents"),
@@ -570,6 +579,7 @@ heartbeat/
570
579
  console.log(`\nNext steps:`);
571
580
  console.log(` 1. Create an agent: glab-agent init <name> --provider claude`);
572
581
  console.log(` 2. Check dependencies: glab-agent doctor`);
582
+ console.log(`\nTip: Use 'glab-agent init --repo' for a complete Agents management repo with README and defaults.`);
573
583
  if (remote) {
574
584
  console.log(`\nDocumentation: ${wikiUrl(remote.host, remote.projectPath, "getting-started")}`);
575
585
  console.log(` (Run 'glab-agent wiki setup' to populate the project wiki)`);
@@ -618,9 +628,78 @@ heartbeat/
618
628
 
619
629
  // --repo: generate self-contained product landing page README (done after all init so we have remote/provider/botUsername)
620
630
  if (options?.repo) {
631
+ // If no agent name given with --repo, create a default one
632
+ let effectiveName = name;
633
+ if (!effectiveName) {
634
+ const defaultName = "my-bot";
635
+ const provider = (resolvedProvider === "claude" || resolvedProvider === "codex") ? resolvedProvider as "claude" | "codex" : "claude";
636
+ const yamlPath = path.join(base, "agents", `${defaultName}.yaml`);
637
+ try {
638
+ await readFile(yamlPath, "utf8");
639
+ } catch {
640
+ const yamlContent = generateAgentYaml(defaultName, provider, [], "GITLAB_TOKEN", { host: remote?.host });
641
+ await writeFile(yamlPath, yamlContent, "utf8");
642
+ console.log(`Created default agent: ${yamlPath}`);
643
+ effectiveName = defaultName;
644
+ }
645
+ }
646
+
647
+ // Create .env.example
648
+ const envExamplePath = path.join(base, ".env.example");
649
+ try {
650
+ await readFile(envExamplePath, "utf8");
651
+ } catch {
652
+ const host = remote?.host ?? "gitlab.com";
653
+ const protocol = host.startsWith("localhost") || host.startsWith("127.") ? "http" : "https";
654
+ const envExample = `# GitLab Personal Access Token
655
+ # Create at: ${protocol}://${host}/-/user_settings/personal_access_tokens
656
+ # Required scopes: api, read_user, read_repository, write_repository
657
+ # Tip: Use a bot account's token so agent actions appear as the bot, not you.
658
+ GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
659
+ `;
660
+ await writeFile(envExamplePath, envExample, "utf8");
661
+ console.log(`Created ${envExamplePath}`);
662
+ }
663
+
664
+ // Create default skill
665
+ const defaultSkillPath = path.join(base, "skills", "test-before-commit.md");
666
+ try {
667
+ await readFile(defaultSkillPath, "utf8");
668
+ } catch {
669
+ const skillContent = `---
670
+ name: test-before-commit
671
+ description: Always run tests before committing
672
+ ---
673
+
674
+ Before committing any code change, run the project's test suite.
675
+ If tests fail, fix the code before committing.
676
+ Do not skip tests or use --no-verify to bypass hooks.
677
+ `;
678
+ await writeFile(defaultSkillPath, skillContent, "utf8");
679
+ console.log(`Created default skill: ${defaultSkillPath}`);
680
+ }
681
+
682
+ // Create root .gitignore (for the Agents repo itself)
683
+ const rootGitignorePath = path.join(PROJECT_DIR, ".gitignore");
684
+ try {
685
+ await readFile(rootGitignorePath, "utf8");
686
+ } catch {
687
+ const rootGitignore = `.glab-agent/.env
688
+ .glab-agent/state/
689
+ .glab-agent/pid/
690
+ .glab-agent/logs/
691
+ .glab-agent/worktrees/
692
+ .glab-agent/metrics/
693
+ .glab-agent/heartbeat/
694
+ .DS_Store
695
+ `;
696
+ await writeFile(rootGitignorePath, rootGitignore, "utf8");
697
+ console.log(`Created .gitignore`);
698
+ }
699
+
621
700
  const readmePath = path.join(PROJECT_DIR, "README.md");
622
701
  const readmeContent = buildRepoReadme({
623
- agentName: name ?? undefined,
702
+ agentName: effectiveName ?? undefined,
624
703
  provider: resolvedProvider,
625
704
  host: remote?.host,
626
705
  projectPath: remote?.projectPath,
@@ -634,6 +713,41 @@ heartbeat/
634
713
  await writeFile(readmePath, readmeContent, "utf8");
635
714
  console.log("Created README.md — product landing page for AI agents.");
636
715
  }
716
+
717
+ // Auto-run wiki setup if remote and token are available
718
+ if (remote) {
719
+ const token = process.env[options?.tokenEnv ?? "GITLAB_TOKEN"];
720
+ if (token) {
721
+ console.log("\nPopulating GitLab Wiki with documentation...");
722
+ try {
723
+ const client = new GitlabGlabClient({ host: remote.host, token });
724
+ const { stdout } = await execFileAsync("glab", ["api", `projects/${encodeURIComponent(remote.projectPath)}`], {
725
+ encoding: "utf8",
726
+ env: { ...process.env, GITLAB_HOST: remote.host, GITLAB_TOKEN: token }
727
+ });
728
+ const projectId = JSON.parse(stdout).id as number;
729
+ const pages = buildWikiPages(remote.host, remote.projectPath);
730
+ const existingPages = await client.listWikiPages(projectId);
731
+ const existingSlugs = new Set(existingPages.map(p => p.slug));
732
+ let created = 0;
733
+ let updated = 0;
734
+ for (const page of pages) {
735
+ try {
736
+ if (existingSlugs.has(page.title)) {
737
+ await client.updateWikiPage(projectId, page.title, page.title, page.content);
738
+ updated++;
739
+ } else {
740
+ await client.createWikiPage(projectId, page.title, page.content);
741
+ created++;
742
+ }
743
+ } catch { /* skip failed pages */ }
744
+ }
745
+ console.log(`✅ Wiki: ${created} created, ${updated} updated. ${wikiUrl(remote.host, remote.projectPath, "home")}`);
746
+ } catch {
747
+ console.log(`Skipped wiki setup (run 'glab-agent wiki setup' manually after pushing to GitLab).`);
748
+ }
749
+ }
750
+ }
637
751
  }
638
752
 
639
753
  // Also append to README.md for universal AI tool compatibility (non-repo mode)
@@ -741,7 +855,7 @@ async function cmdDoctor(): Promise<void> {
741
855
  label: "Agent definitions",
742
856
  ok: false,
743
857
  optional: false,
744
- message: "no .yaml files found in .glab-agent/agents/"
858
+ message: "no agent definitions found in .glab-agent/agents/ — run 'glab-agent init <name> --provider claude' to create one"
745
859
  });
746
860
  }
747
861
 
@@ -1940,6 +2054,28 @@ export function buildClaudeMdSection(agentName?: string, host?: string, projectP
1940
2054
  "- Agent definitions: `.glab-agent/agents/*.yaml`",
1941
2055
  "- Shared skills: `.glab-agent/skills/*.md`",
1942
2056
  "- Tokens: `.glab-agent/.env` (gitignored)",
2057
+ "",
2058
+ "### Agent YAML Format",
2059
+ "",
2060
+ "```yaml",
2061
+ "name: my-bot",
2062
+ "provider: claude # or codex",
2063
+ "gitlab:",
2064
+ " token_env: GITLAB_TOKEN",
2065
+ "prompt:",
2066
+ " preamble: |",
2067
+ " Project-specific instructions here.",
2068
+ "triggers:",
2069
+ " labels: [] # empty = all issues, or [\"bug\", \"feature\"]",
2070
+ " exclude_labels: [\"Done\"]",
2071
+ "skills:",
2072
+ " - test-before-commit",
2073
+ "poll_interval_seconds: 60",
2074
+ "```",
2075
+ "",
2076
+ "### Setup New Agents Repo",
2077
+ "",
2078
+ "`glab-agent init --repo` creates a complete Agents management repo with README, default agent, skills, and .env template.",
1943
2079
  );
1944
2080
  if (host && projectPath) {
1945
2081
  const protocol = host.startsWith("localhost") || host.startsWith("127.") ? "http" : "https";
@@ -1982,6 +2118,11 @@ export function buildAgentSkill(): string {
1982
2118
  "- `glab-agent history <name>` — Show recent execution history",
1983
2119
  "- `glab-agent report <name> --since 7d` — Performance report (add `--publish` to write to GitLab Wiki)",
1984
2120
  "",
2121
+ "## Setup",
2122
+ "- `glab-agent init <name> --provider claude` — Create a single agent",
2123
+ "- `glab-agent init --repo` — Create a full Agents management repo (README + default agent + skills)",
2124
+ "- `glab-agent wiki setup` — Populate GitLab Wiki with documentation",
2125
+ "",
1985
2126
  "## Configuration",
1986
2127
  "- Agent YAML: `.glab-agent/agents/<name>.yaml`",
1987
2128
  "- Shared skills: `.glab-agent/skills/<name>.md`",
@@ -2078,8 +2219,39 @@ async function main(): Promise<void> {
2078
2219
  const command = args[0];
2079
2220
  const target = args[1];
2080
2221
 
2222
+ // Handle --version / version / -v
2223
+ if (command === "--version" || command === "version" || command === "-v") {
2224
+ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
2225
+ try {
2226
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
2227
+ console.log(`glab-agent v${pkg.version}`);
2228
+ } catch {
2229
+ console.log("glab-agent (version unknown)");
2230
+ }
2231
+ return;
2232
+ }
2233
+
2081
2234
  switch (command) {
2082
2235
  case "init": {
2236
+ if (args.includes("--help") || args.includes("-h")) {
2237
+ console.log(`Usage: glab-agent init [<name>] [options]
2238
+
2239
+ Create agent configuration in the current project.
2240
+
2241
+ Options:
2242
+ <name> Agent name (e.g., my-bot). Omit for base init only.
2243
+ --provider <p> AI provider: claude (default) or codex
2244
+ --labels <l> Comma-separated trigger labels
2245
+ --token-env <var> Token env variable name (default: GITLAB_TOKEN)
2246
+ --repo Generate full Agents management repo (README + default agent + skills + .env.example)
2247
+ --help, -h Show this help
2248
+
2249
+ Examples:
2250
+ glab-agent init my-bot --provider claude # Create a Claude agent
2251
+ glab-agent init --repo # Full Agents repo with README
2252
+ glab-agent init my-bot --repo # Agents repo + named agent`);
2253
+ break;
2254
+ }
2083
2255
  const initProvider = extractFlag(args, "--provider");
2084
2256
  const initLabels = extractFlag(args, "--labels");
2085
2257
  const initTokenEnv = extractFlag(args, "--token-env");
@@ -50,6 +50,7 @@ const ACCEPTED_TODO_ACTIONS = new Set(["mentioned", "directly_addressed"]);
50
50
  export interface LocalAgentConfig {
51
51
  gitlabHost: string;
52
52
  gitlabProjectId?: number;
53
+ gitlabProjectIdExplicit: boolean; // true when YAML explicitly sets project_id
53
54
  gitlabToken: string;
54
55
  agentProvider: AgentProvider;
55
56
  agentRepoPath: string;
@@ -334,7 +335,7 @@ export async function runWatcherCycle(
334
335
 
335
336
  await updateAgentUserStatus(dependencies, "idle", undefined, config);
336
337
 
337
- const todos = config.gitlabProjectId
338
+ const todos = config.gitlabProjectIdExplicit && config.gitlabProjectId
338
339
  ? await dependencies.gitlabClient.listPendingTodos(config.gitlabProjectId)
339
340
  : await dependencies.gitlabClient.listAllPendingTodos();
340
341
  logger.info(`Polled ${todos.length} pending todo(s).`);
@@ -823,7 +824,7 @@ async function notifyPendingTodosWhileBusy(
823
824
  let todos: GitlabTodoItem[];
824
825
 
825
826
  try {
826
- todos = config.gitlabProjectId
827
+ todos = (config.gitlabProjectIdExplicit && config.gitlabProjectId)
827
828
  ? await dependencies.gitlabClient.listMentionedIssueTodos(config.gitlabProjectId)
828
829
  : await dependencies.gitlabClient.listAllPendingTodos();
829
830
  } catch (error) {
@@ -1039,14 +1040,16 @@ export function loadConfigFromAgentDefinition(
1039
1040
  throw new Error(`gitlab.host is required for agent "${def.name}" or must be inferable from git remote.`);
1040
1041
  }
1041
1042
 
1043
+ const explicitProjectId = def.gitlab.project_id;
1042
1044
  const gitlabProjectId =
1043
- def.gitlab.project_id ??
1045
+ explicitProjectId ??
1044
1046
  inferGitlabProjectId(gitlabHost, gitlabToken, agentRepoPath, execFileSyncImpl) ??
1045
1047
  undefined;
1046
1048
 
1047
1049
  return {
1048
1050
  gitlabHost,
1049
1051
  gitlabProjectId,
1052
+ gitlabProjectIdExplicit: explicitProjectId != null,
1050
1053
  gitlabToken,
1051
1054
  agentProvider: def.provider,
1052
1055
  agentRepoPath,
@@ -1181,7 +1184,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
1181
1184
  process.exit(1);
1182
1185
  }
1183
1186
 
1184
- logger.info(`Watcher started. mode=${mode} provider=${config.agentProvider} host=${config.gitlabHost}${config.gitlabProjectId ? ` project=${config.gitlabProjectId}` : " project=auto (from todos)"}`);
1187
+ logger.info(`Watcher started. mode=${mode} provider=${config.agentProvider} host=${config.gitlabHost}${config.gitlabProjectId ? ` project=${config.gitlabProjectId}${config.gitlabProjectIdExplicit ? "" : " (inferred, todo polling unfiltered)"}` : " project=auto (from todos)"}`);
1185
1188
 
1186
1189
  if (mode === "run-once") {
1187
1190
  await runWatcherCycle(config, dependencies);