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 +2 -1
- package/src/local-agent/cli.ts +176 -4
- package/src/local-agent/watcher.ts +7 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glab-agent",
|
|
3
|
-
"version": "0.2.
|
|
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",
|
package/src/local-agent/cli.ts
CHANGED
|
@@ -83,8 +83,8 @@ Options:
|
|
|
83
83
|
--project <dir> Target project directory (default: cwd)
|
|
84
84
|
|
|
85
85
|
Commands:
|
|
86
|
-
init [name] Initialize
|
|
87
|
-
--provider <claude|codex> --labels <l1,l2> --
|
|
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:
|
|
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
|
|
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
|
-
|
|
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);
|