auggy 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.
Files changed (121) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/README.md +161 -0
  4. package/package.json +76 -0
  5. package/src/agent-card.ts +39 -0
  6. package/src/agent.ts +283 -0
  7. package/src/agentmail-client.ts +138 -0
  8. package/src/augments/bash/index.ts +463 -0
  9. package/src/augments/bash/skill/SKILL.md +156 -0
  10. package/src/augments/budgets/budget-store.ts +513 -0
  11. package/src/augments/budgets/index.ts +134 -0
  12. package/src/augments/budgets/preamble.ts +93 -0
  13. package/src/augments/budgets/types.ts +89 -0
  14. package/src/augments/file-memory/index.ts +71 -0
  15. package/src/augments/filesystem/index.ts +533 -0
  16. package/src/augments/filesystem/skill/SKILL.md +142 -0
  17. package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
  18. package/src/augments/layered-memory/extractor/buffer.ts +56 -0
  19. package/src/augments/layered-memory/extractor/frequency.ts +79 -0
  20. package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
  21. package/src/augments/layered-memory/extractor/parse.ts +75 -0
  22. package/src/augments/layered-memory/extractor/prompt.md +26 -0
  23. package/src/augments/layered-memory/index.ts +757 -0
  24. package/src/augments/layered-memory/skill/SKILL.md +153 -0
  25. package/src/augments/layered-memory/storage/migrations/README.md +16 -0
  26. package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
  27. package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
  28. package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
  29. package/src/augments/layered-memory/storage/types.ts +98 -0
  30. package/src/augments/link/index.ts +489 -0
  31. package/src/augments/link/translate.ts +261 -0
  32. package/src/augments/notify/adapters/agentmail.ts +70 -0
  33. package/src/augments/notify/adapters/telegram.ts +60 -0
  34. package/src/augments/notify/adapters/webhook.ts +55 -0
  35. package/src/augments/notify/index.ts +284 -0
  36. package/src/augments/notify/skill/SKILL.md +150 -0
  37. package/src/augments/org-context/index.ts +721 -0
  38. package/src/augments/org-context/skill/SKILL.md +96 -0
  39. package/src/augments/skills/index.ts +103 -0
  40. package/src/augments/supabase-memory/index.ts +151 -0
  41. package/src/augments/telegram-transport/index.ts +312 -0
  42. package/src/augments/telegram-transport/polling.ts +55 -0
  43. package/src/augments/telegram-transport/webhook.ts +56 -0
  44. package/src/augments/turn-control/index.ts +61 -0
  45. package/src/augments/turn-control/skill/SKILL.md +155 -0
  46. package/src/augments/visitor-auth/email-validation.ts +66 -0
  47. package/src/augments/visitor-auth/index.ts +779 -0
  48. package/src/augments/visitor-auth/rate-limiter.ts +90 -0
  49. package/src/augments/visitor-auth/skill/SKILL.md +55 -0
  50. package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
  51. package/src/augments/visitor-auth/storage/types.ts +164 -0
  52. package/src/augments/visitor-auth/types.ts +123 -0
  53. package/src/augments/visitor-auth/verify-page.ts +179 -0
  54. package/src/augments/web-fetch/index.ts +331 -0
  55. package/src/augments/web-fetch/skill/SKILL.md +100 -0
  56. package/src/cli/agent-index.ts +289 -0
  57. package/src/cli/augment-catalog.ts +320 -0
  58. package/src/cli/augment-resolver.ts +597 -0
  59. package/src/cli/commands/add-skill.ts +194 -0
  60. package/src/cli/commands/add.ts +87 -0
  61. package/src/cli/commands/chat.ts +207 -0
  62. package/src/cli/commands/create.ts +462 -0
  63. package/src/cli/commands/dev.ts +139 -0
  64. package/src/cli/commands/eval.ts +180 -0
  65. package/src/cli/commands/ls.ts +66 -0
  66. package/src/cli/commands/remove.ts +95 -0
  67. package/src/cli/commands/restart.ts +40 -0
  68. package/src/cli/commands/start.ts +123 -0
  69. package/src/cli/commands/status.ts +104 -0
  70. package/src/cli/commands/stop.ts +84 -0
  71. package/src/cli/commands/visitors-revoke.ts +155 -0
  72. package/src/cli/commands/visitors.ts +101 -0
  73. package/src/cli/config-parser.ts +1034 -0
  74. package/src/cli/engine-resolver.ts +68 -0
  75. package/src/cli/index.ts +178 -0
  76. package/src/cli/model-picker.ts +89 -0
  77. package/src/cli/pid-registry.ts +146 -0
  78. package/src/cli/plist-generator.ts +117 -0
  79. package/src/cli/resolve-config.ts +56 -0
  80. package/src/cli/scaffold-skills.ts +158 -0
  81. package/src/cli/scaffold.ts +291 -0
  82. package/src/cli/skill-frontmatter.ts +51 -0
  83. package/src/cli/skill-validator.ts +151 -0
  84. package/src/cli/types.ts +228 -0
  85. package/src/cli/yaml-helpers.ts +66 -0
  86. package/src/engines/_shared/cost.ts +55 -0
  87. package/src/engines/_shared/schema-normalize.ts +75 -0
  88. package/src/engines/anthropic/pricing.ts +117 -0
  89. package/src/engines/anthropic.ts +483 -0
  90. package/src/engines/openai/pricing.ts +67 -0
  91. package/src/engines/openai.ts +446 -0
  92. package/src/engines/openrouter/pricing.ts +83 -0
  93. package/src/engines/openrouter.ts +185 -0
  94. package/src/helpers.ts +24 -0
  95. package/src/http.ts +387 -0
  96. package/src/index.ts +165 -0
  97. package/src/kernel/capability-table.ts +172 -0
  98. package/src/kernel/context-allocator.ts +161 -0
  99. package/src/kernel/history-manager.ts +198 -0
  100. package/src/kernel/lifecycle-manager.ts +106 -0
  101. package/src/kernel/output-validator.ts +35 -0
  102. package/src/kernel/preamble.ts +23 -0
  103. package/src/kernel/route-collector.ts +97 -0
  104. package/src/kernel/timeout.ts +21 -0
  105. package/src/kernel/tool-selector.ts +47 -0
  106. package/src/kernel/trace-emitter.ts +66 -0
  107. package/src/kernel/transport-queue.ts +147 -0
  108. package/src/kernel/turn-loop.ts +1148 -0
  109. package/src/memory/context-synthesis.ts +83 -0
  110. package/src/memory/memory-bus.ts +61 -0
  111. package/src/memory/registry.ts +80 -0
  112. package/src/memory/tools.ts +320 -0
  113. package/src/memory/types.ts +8 -0
  114. package/src/parts.ts +30 -0
  115. package/src/scaffold-templates/identity.md +31 -0
  116. package/src/telegram-client.ts +145 -0
  117. package/src/tokenizer.ts +14 -0
  118. package/src/transports/ag-ui-events.ts +253 -0
  119. package/src/transports/visitor-token.ts +82 -0
  120. package/src/transports/web-transport.ts +948 -0
  121. package/src/types.ts +1009 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * auggy eval [suite|agent] — run an eval suite.
3
+ *
4
+ * Suite routing:
5
+ * auggy eval auto-save # auto-save fixture validation (dry-run)
6
+ * auggy eval auto-save --dry-run # explicit dry-run (same as above)
7
+ *
8
+ * Security eval (default when no suite name is given):
9
+ * auggy eval # default fixture (canonical test agent)
10
+ * auggy eval zip # registered agent
11
+ * auggy eval --config path/to/agent.yaml
12
+ * auggy eval zip --suite security-only
13
+ * auggy eval zip --trials 5
14
+ *
15
+ * Config-path resolution order (highest precedence first):
16
+ * 1. --config <path> (resolved against cwd)
17
+ * 2. [agent] argument (looked up in `~/.auggy/agents.json`)
18
+ * 3. Default: the canonical fixture at `evals/security/fixtures/test-agent.yaml`
19
+ */
20
+
21
+ import { existsSync } from "node:fs";
22
+ import { join, resolve } from "node:path";
23
+ import { Command } from "commander";
24
+ import { runEvalSuite, getDefaultFixtureConfigPath } from "../../../evals/security/run";
25
+ import { runAutoSaveEval } from "../../../evals/auto-save/run";
26
+ import { getAgent } from "../agent-index";
27
+
28
+ /** Known suite names that route to specialized runners (not the security runner). */
29
+ const NAMED_SUITES = ["auto-save"] as const;
30
+ type NamedSuite = (typeof NAMED_SUITES)[number];
31
+
32
+ interface ResolveEvalConfigOptions {
33
+ /** Override `~/.auggy/` for tests. */
34
+ auggyDir?: string;
35
+ /** Inject a fixture-path resolver for tests. Defaults to the runner's resolver. */
36
+ defaultFixtureConfigPath?: () => string;
37
+ }
38
+
39
+ /**
40
+ * Resolve the agent.yaml path for `auggy eval`. Mirrors `resolveConfigPath`
41
+ * but allows the agent name to be omitted — falling back to the bundled
42
+ * fixture when neither name nor explicit --config is supplied.
43
+ */
44
+ export function resolveEvalConfigPath(
45
+ args: { agentName?: string; explicitConfig?: string },
46
+ opts: ResolveEvalConfigOptions = {},
47
+ ): string {
48
+ if (args.explicitConfig) {
49
+ const absPath = resolve(args.explicitConfig);
50
+ if (!existsSync(absPath)) {
51
+ throw new Error(`Config file not found: ${absPath}`);
52
+ }
53
+ return absPath;
54
+ }
55
+
56
+ if (args.agentName) {
57
+ const entry = getAgent(args.agentName, opts);
58
+ if (!entry) {
59
+ throw new Error(
60
+ `Agent "${args.agentName}" not found.\n\n` +
61
+ ` Run \`auggy ls\` to see registered agents,\n` +
62
+ ` or use --config <path> for a one-off path.`,
63
+ );
64
+ }
65
+ const cfg = join(entry.localDir, "agent.yaml");
66
+ if (!existsSync(cfg)) {
67
+ throw new Error(
68
+ `agent.yaml missing at indexed path: ${cfg}\n\n` +
69
+ ` The agent directory may have been deleted or moved manually.\n` +
70
+ ` Run \`auggy remove ${args.agentName}\` to clean up the index entry.`,
71
+ );
72
+ }
73
+ return cfg;
74
+ }
75
+
76
+ const fixtureResolver = opts.defaultFixtureConfigPath ?? getDefaultFixtureConfigPath;
77
+ return fixtureResolver();
78
+ }
79
+
80
+ export interface EvalCommandDeps {
81
+ /** Inject for tests so we don't make real API calls. */
82
+ runEvalSuite?: typeof runEvalSuite;
83
+ /** Inject auto-save runner for tests. */
84
+ runAutoSaveEval?: typeof runAutoSaveEval;
85
+ /** Override exit so tests can assert the exit code without crashing the runner. */
86
+ exit?: (code: number) => void;
87
+ /** Override `~/.auggy/` for tests. */
88
+ auggyDir?: string;
89
+ }
90
+
91
+ export function evalCommand(deps: EvalCommandDeps = {}): Command {
92
+ const runner = deps.runEvalSuite ?? runEvalSuite;
93
+ const autoSaveRunner = deps.runAutoSaveEval ?? runAutoSaveEval;
94
+ const exit = deps.exit ?? ((code: number) => process.exit(code));
95
+
96
+ return new Command("eval")
97
+ .description("Run an eval suite: auto-save (fixture validation) or security (default)")
98
+ .argument(
99
+ "[suite-or-agent]",
100
+ "suite name (auto-save) or registered agent name for security eval (defaults to the bundled fixture)",
101
+ )
102
+ .option(
103
+ "--config <path>",
104
+ "explicit agent.yaml path (overrides agent name lookup; security eval only)",
105
+ )
106
+ .option(
107
+ "--suite <which>",
108
+ "security-only | benign-only | all (default; security eval only)",
109
+ "all",
110
+ )
111
+ .option("--trials <n>", "trials per case (default: 3; security eval only)")
112
+ .option("--dry-run", "validate auto-save fixtures without LLM calls (auto-save suite only)")
113
+ .action(
114
+ async (
115
+ suiteOrAgent: string | undefined,
116
+ opts: { config?: string; suite: string; trials?: string; dryRun?: boolean },
117
+ ) => {
118
+ // Route named suites to their own runners.
119
+ if (suiteOrAgent != null && NAMED_SUITES.includes(suiteOrAgent as NamedSuite)) {
120
+ const suiteName = suiteOrAgent as NamedSuite;
121
+
122
+ if (suiteName === "auto-save") {
123
+ const result = await autoSaveRunner({ dryRun: opts.dryRun !== false });
124
+ exit(result.exitCode);
125
+ return;
126
+ }
127
+
128
+ // Future named suites routed here.
129
+ console.error(`Error: unknown suite "${suiteName}"`);
130
+ exit(1);
131
+ return;
132
+ }
133
+
134
+ // Security eval path (default when no named suite is given).
135
+ const agentName = suiteOrAgent;
136
+ let configPath: string;
137
+ try {
138
+ configPath = resolveEvalConfigPath(
139
+ { agentName, explicitConfig: opts.config },
140
+ { auggyDir: deps.auggyDir },
141
+ );
142
+ } catch (err) {
143
+ console.error(`Error: ${(err as Error).message}`);
144
+ exit(1);
145
+ return;
146
+ }
147
+
148
+ if (
149
+ opts.suite !== "all" &&
150
+ opts.suite !== "security-only" &&
151
+ opts.suite !== "benign-only"
152
+ ) {
153
+ console.error(
154
+ `Error: --suite must be one of: all, security-only, benign-only (got "${opts.suite}")`,
155
+ );
156
+ exit(1);
157
+ return;
158
+ }
159
+
160
+ let trialsOverride: number | undefined;
161
+ if (opts.trials !== undefined) {
162
+ const n = Number.parseInt(opts.trials, 10);
163
+ if (!Number.isInteger(n) || n < 1) {
164
+ console.error(`Error: --trials must be a positive integer (got "${opts.trials}")`);
165
+ exit(1);
166
+ return;
167
+ }
168
+ trialsOverride = n;
169
+ }
170
+
171
+ const result = await runner({
172
+ configPath,
173
+ runSecurity: opts.suite !== "benign-only",
174
+ runBenign: opts.suite !== "security-only",
175
+ trialsOverride,
176
+ });
177
+ exit(result.exitCode);
178
+ },
179
+ );
180
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * auggy ls — list all registered agents with their location and status.
3
+ *
4
+ * Status is derived from PID manifests + filesystem: running, stopped, or
5
+ * missing-dir (indexed but localDir gone).
6
+ */
7
+
8
+ import { existsSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { listAgents } from "../agent-index";
11
+ import { readPidManifest, isProcessAlive } from "../pid-registry";
12
+
13
+ interface LsOptions {
14
+ auggyDir?: string;
15
+ }
16
+
17
+ interface AgentRow {
18
+ name: string;
19
+ location: string;
20
+ status: string;
21
+ }
22
+
23
+ function tildify(path: string): string {
24
+ const home = homedir();
25
+ if (path === home || path.startsWith(`${home}/`)) {
26
+ return `~${path.slice(home.length)}`;
27
+ }
28
+ return path;
29
+ }
30
+
31
+ function statusFor(name: string, localDir: string): string {
32
+ if (!existsSync(localDir)) return "missing-dir";
33
+ const pid = readPidManifest(name);
34
+ if (pid && isProcessAlive(pid.pid)) {
35
+ return `running (${pid.mode}, :${pid.port})`;
36
+ }
37
+ return "stopped";
38
+ }
39
+
40
+ export async function runLs(opts: LsOptions = {}): Promise<void> {
41
+ const agents = listAgents({ auggyDir: opts.auggyDir });
42
+
43
+ if (agents.length === 0) {
44
+ console.log("No agents registered.");
45
+ console.log();
46
+ console.log("Run `auggy create <name>` to scaffold one.");
47
+ return;
48
+ }
49
+
50
+ const rows: AgentRow[] = agents.map((a) => ({
51
+ name: a.name,
52
+ location: tildify(a.localDir),
53
+ status: statusFor(a.name, a.localDir),
54
+ }));
55
+
56
+ // Compute column widths.
57
+ const nameW = Math.max(4, ...rows.map((r) => r.name.length));
58
+ const locW = Math.max(8, ...rows.map((r) => r.location.length));
59
+
60
+ const pad = (s: string, w: number): string => s + " ".repeat(Math.max(0, w - s.length));
61
+
62
+ console.log(`${pad("NAME", nameW)} ${pad("LOCATION", locW)} STATUS`);
63
+ for (const row of rows) {
64
+ console.log(`${pad(row.name, nameW)} ${pad(row.location, locW)} ${row.status}`);
65
+ }
66
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * auggy remove <name> — delete an agent directory and clear the index entry.
3
+ *
4
+ * Refuses if the agent is running. Prompts before deletion (skipped with
5
+ * --yes). Tolerates missing localDir (still cleans the index entry).
6
+ */
7
+
8
+ import { existsSync, readFileSync, rmSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { confirm } from "@inquirer/prompts";
11
+ import { getAgent, removeAgent } from "../agent-index";
12
+ import { readPidManifest, isProcessAlive, removePidManifest } from "../pid-registry";
13
+
14
+ function readConfigName(localDir: string): string | null {
15
+ try {
16
+ const yamlPath = join(localDir, "agent.yaml");
17
+ if (!existsSync(yamlPath)) return null;
18
+ const content = readFileSync(yamlPath, "utf-8");
19
+ const match = content.match(/^name:\s*(.+)$/m);
20
+ return match?.[1]?.trim() ?? null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ interface RemoveOptions {
27
+ /** Skip the y/N prompt. */
28
+ yes?: boolean;
29
+ /** Override `~/.auggy/` for tests. */
30
+ auggyDir?: string;
31
+ }
32
+
33
+ export async function runRemove(name: string, opts: RemoveOptions = {}): Promise<void> {
34
+ const entry = getAgent(name, { auggyDir: opts.auggyDir });
35
+ if (!entry) {
36
+ throw new Error(
37
+ `Agent "${name}" is not registered.\n\n Run \`auggy ls\` to see registered agents.`,
38
+ );
39
+ }
40
+
41
+ // Refuse if the agent is running. Stale manifests (dead PID) are tolerated
42
+ // — we clean them up below. Check under both the CLI-arg name AND the
43
+ // agent.yaml's config.name (operator may have edited the yaml after create,
44
+ // in which case `auggy dev` writes the manifest under config.name).
45
+ const pidByCli = readPidManifest(name);
46
+ const configName = readConfigName(entry.localDir);
47
+ const pidByConfig = configName && configName !== name ? readPidManifest(configName) : null;
48
+
49
+ const aliveCli = pidByCli && isProcessAlive(pidByCli.pid);
50
+ const aliveConfig = pidByConfig && isProcessAlive(pidByConfig.pid);
51
+
52
+ if (aliveCli || aliveConfig) {
53
+ const liveName = aliveCli ? name : configName!;
54
+ throw new Error(`Agent "${liveName}" is running. Stop it first:\n\n auggy stop ${liveName}`);
55
+ }
56
+
57
+ if (!opts.yes) {
58
+ const ok = await confirm({
59
+ message:
60
+ `This will permanently delete:\n ${entry.localDir}\n\n` +
61
+ `And remove the registry entry for "${name}".\n\nContinue?`,
62
+ default: false,
63
+ });
64
+ if (!ok) {
65
+ console.log("Aborted.");
66
+ return;
67
+ }
68
+ }
69
+
70
+ // Delete the local dir if present (tolerate missing). Sanity-check that the
71
+ // dir contains agent.yaml first — refuse to recursively delete if not, since
72
+ // a tampered or corrupt index entry could otherwise nuke arbitrary paths.
73
+ if (existsSync(entry.localDir)) {
74
+ const yamlPath = join(entry.localDir, "agent.yaml");
75
+ if (!existsSync(yamlPath)) {
76
+ throw new Error(
77
+ `Refusing to delete "${entry.localDir}" — it does not contain agent.yaml.\n\n` +
78
+ ` This may indicate a tampered or stale index entry. If the agent dir was\n` +
79
+ ` modified outside auggy, clean up manually and re-run \`auggy remove\` to\n` +
80
+ ` clear the index entry.`,
81
+ );
82
+ }
83
+ rmSync(entry.localDir, { recursive: true, force: true });
84
+ }
85
+
86
+ // Clean up stale PID manifest(s) if any. Remove under whichever name we
87
+ // actually found a manifest at.
88
+ if (pidByCli) removePidManifest(name);
89
+ if (pidByConfig && configName) removePidManifest(configName);
90
+
91
+ // Clear index entry.
92
+ removeAgent(name, { auggyDir: opts.auggyDir });
93
+
94
+ console.log(`Removed agent "${name}" (was at ${entry.localDir}).`);
95
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * auggy restart <name> — stop + start in one command.
3
+ *
4
+ * Reads the PID manifest to determine mode (dev vs launchd),
5
+ * stops the agent, then restarts it in the same mode.
6
+ */
7
+
8
+ import { readPidManifest } from "../pid-registry";
9
+ import { runStop } from "./stop";
10
+ import { runStart } from "./start";
11
+ import { runDev } from "./dev";
12
+
13
+ export async function runRestart(name: string, opts: { config?: string }): Promise<void> {
14
+ const manifest = readPidManifest(name);
15
+
16
+ if (!manifest) {
17
+ console.log(
18
+ `Agent "${name}" is not running. Use "auggy dev ${name}" or "auggy start ${name}" to start it.`,
19
+ );
20
+ return;
21
+ }
22
+
23
+ const mode = manifest.mode;
24
+ const configPath = opts.config ?? manifest.configPath;
25
+
26
+ console.log(`Restarting "${name}" (${mode} mode)...`);
27
+
28
+ // Stop the agent.
29
+ await runStop(name);
30
+
31
+ // Brief pause for port release.
32
+ await Bun.sleep(1000);
33
+
34
+ // Restart in the same mode.
35
+ if (mode === "launchd") {
36
+ await runStart(name, { config: configPath });
37
+ } else {
38
+ await runDev(name, { config: configPath });
39
+ }
40
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * auggy start <name> — install an agent as a launchd service.
3
+ *
4
+ * Generates a plist, symlinks it to ~/Library/LaunchAgents/, and
5
+ * loads it via launchctl. The plist invokes `auggy dev <name>` so
6
+ * launchd handles daemonization and restart.
7
+ */
8
+
9
+ import { mkdirSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
10
+ import { dirname, resolve } from "node:path";
11
+ import { $ } from "bun";
12
+ import { parseConfig } from "../config-parser";
13
+ import {
14
+ generatePlist,
15
+ plistLabel,
16
+ plistStorePath,
17
+ plistInstallPath,
18
+ logDir,
19
+ } from "../plist-generator";
20
+ import { readPidManifest, tryClaimName } from "../pid-registry";
21
+ import { resolveConfigPath } from "../resolve-config";
22
+
23
+ function resolveBunPath(): string {
24
+ return process.execPath;
25
+ }
26
+
27
+ function resolveCliEntryPoint(): string {
28
+ return resolve(import.meta.dir, "../index.ts");
29
+ }
30
+
31
+ export async function runStart(name: string, opts: { config?: string }): Promise<void> {
32
+ const configPath = resolveConfigPath(name, opts.config);
33
+ const agentDir = dirname(configPath);
34
+
35
+ // Validate config before installing.
36
+ const config = parseConfig(configPath);
37
+ const agentName = config.name;
38
+
39
+ // Check if already running.
40
+ if (!tryClaimName(agentName)) {
41
+ throw new Error(
42
+ `Agent "${agentName}" is already running. Use "auggy stop ${agentName}" first.`,
43
+ );
44
+ }
45
+
46
+ // Unload existing plist if present.
47
+ const label = plistLabel(agentName);
48
+ const installPath = plistInstallPath(agentName);
49
+ try {
50
+ const result = await $`launchctl list`.quiet();
51
+ if (result.stdout.toString().includes(label)) {
52
+ await $`launchctl unload ${installPath}`.quiet();
53
+ }
54
+ } catch {}
55
+
56
+ // Clean up old plist files.
57
+ const storePath = plistStorePath(agentName);
58
+ try {
59
+ unlinkSync(installPath);
60
+ } catch {}
61
+ try {
62
+ unlinkSync(storePath);
63
+ } catch {}
64
+
65
+ // Generate plist.
66
+ const plist = generatePlist({
67
+ name: agentName,
68
+ agentDir: resolve(agentDir),
69
+ configPath: resolve(configPath),
70
+ bunPath: resolveBunPath(),
71
+ cliEntryPoint: resolveCliEntryPoint(),
72
+ });
73
+
74
+ // Ensure directories exist.
75
+ mkdirSync(dirname(storePath), { recursive: true });
76
+ mkdirSync(dirname(installPath), { recursive: true });
77
+ mkdirSync(logDir(), { recursive: true });
78
+
79
+ // Write plist and symlink to LaunchAgents.
80
+ writeFileSync(storePath, plist);
81
+ symlinkSync(storePath, installPath);
82
+
83
+ // Load into launchd.
84
+ try {
85
+ await $`launchctl load ${installPath}`.quiet();
86
+ } catch (err: unknown) {
87
+ const stderr = (err as { stderr?: { toString(): string } }).stderr?.toString() ?? "";
88
+ if (!stderr.includes("service already loaded")) {
89
+ throw new Error(`launchctl load failed: ${stderr.trim() || (err as Error).message}`);
90
+ }
91
+ }
92
+
93
+ // Poll for the agent to start.
94
+ const MAX_WAIT = 8_000;
95
+ const POLL_INTERVAL = 500;
96
+ let waited = 0;
97
+
98
+ while (waited < MAX_WAIT) {
99
+ await Bun.sleep(POLL_INTERVAL);
100
+ waited += POLL_INTERVAL;
101
+
102
+ const manifest = readPidManifest(agentName);
103
+ if (manifest) {
104
+ console.log(`Agent "${agentName}" installed and running (PID ${manifest.pid})`);
105
+ console.log();
106
+ console.log(` Mode: always-on (launchd-managed)`);
107
+ console.log(` Restart: automatic on crash`);
108
+ if (manifest.port) {
109
+ console.log(` URL: http://localhost:${manifest.port}`);
110
+ }
111
+ console.log(` Logs: ${logDir()}/${agentName}.{log,err}`);
112
+ console.log();
113
+ console.log(` To stop: auggy stop ${agentName}`);
114
+ console.log(` To status: auggy status ${agentName}`);
115
+ console.log(` To logs: tail -f ${logDir()}/${agentName}.log`);
116
+ return;
117
+ }
118
+ }
119
+
120
+ console.error(`Agent "${agentName}" did not start within ${MAX_WAIT / 1000}s.`);
121
+ console.error(`Check logs: tail -20 ${logDir()}/${agentName}.err`);
122
+ process.exit(1);
123
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * auggy status [name] — show running agents or detail one.
3
+ *
4
+ * With no name: list all running agents in a table.
5
+ * With a name: show detailed status including health check.
6
+ */
7
+
8
+ import { listPidManifests, readPidManifest, isProcessAlive } from "../pid-registry";
9
+
10
+ function formatUptime(startedAt: string): string {
11
+ const ms = Date.now() - new Date(startedAt).getTime();
12
+ const seconds = Math.floor(ms / 1000);
13
+ if (seconds < 60) return `${seconds}s`;
14
+ const minutes = Math.floor(seconds / 60);
15
+ if (minutes < 60) return `${minutes}m`;
16
+ const hours = Math.floor(minutes / 60);
17
+ if (hours < 24) return `${hours}h ${minutes % 60}m`;
18
+ const days = Math.floor(hours / 24);
19
+ return `${days}d ${hours % 24}h`;
20
+ }
21
+
22
+ async function fetchHealth(port: number): Promise<Record<string, unknown> | null> {
23
+ try {
24
+ const res = await fetch(`http://localhost:${port}/health`, {
25
+ signal: AbortSignal.timeout(2000),
26
+ });
27
+ if (res.ok) return (await res.json()) as Record<string, unknown>;
28
+ } catch {}
29
+ return null;
30
+ }
31
+
32
+ export async function runStatus(name?: string): Promise<void> {
33
+ if (name) {
34
+ await showDetail(name);
35
+ } else {
36
+ await showAll();
37
+ }
38
+ }
39
+
40
+ async function showAll(): Promise<void> {
41
+ const manifests = listPidManifests();
42
+
43
+ if (manifests.length === 0) {
44
+ console.log("No agents running.");
45
+ console.log('Run "auggy create <name>" to create one.');
46
+ return;
47
+ }
48
+
49
+ // Table header.
50
+ console.log(
51
+ pad("NAME", 20) +
52
+ pad("STATUS", 10) +
53
+ pad("PID", 8) +
54
+ pad("PORT", 8) +
55
+ pad("MODE", 10) +
56
+ pad("UPTIME", 10),
57
+ );
58
+ console.log("-".repeat(66));
59
+
60
+ for (const m of manifests) {
61
+ const alive = isProcessAlive(m.pid);
62
+ const status = alive ? "running" : "dead";
63
+ console.log(
64
+ pad(m.name, 20) +
65
+ pad(status, 10) +
66
+ pad(String(m.pid), 8) +
67
+ pad(m.port ? String(m.port) : "-", 8) +
68
+ pad(m.mode, 10) +
69
+ pad(formatUptime(m.startedAt), 10),
70
+ );
71
+ }
72
+ }
73
+
74
+ async function showDetail(name: string): Promise<void> {
75
+ const manifest = readPidManifest(name);
76
+
77
+ if (!manifest) {
78
+ console.log(`Agent "${name}" is not running.`);
79
+ return;
80
+ }
81
+
82
+ const alive = isProcessAlive(manifest.pid);
83
+ console.log(`Agent: ${manifest.name}`);
84
+ console.log(`Status: ${alive ? "running" : "dead"}`);
85
+ console.log(`PID: ${manifest.pid}`);
86
+ console.log(`Mode: ${manifest.mode}`);
87
+ console.log(`Uptime: ${formatUptime(manifest.startedAt)}`);
88
+ console.log(`Config: ${manifest.configPath}`);
89
+ console.log(`Agent dir: ${manifest.agentDir}`);
90
+
91
+ if (manifest.port) {
92
+ console.log(`Port: ${manifest.port}`);
93
+ const health = await fetchHealth(manifest.port);
94
+ if (health) {
95
+ console.log(`Health: ${JSON.stringify(health)}`);
96
+ } else {
97
+ console.log("Health: unreachable");
98
+ }
99
+ }
100
+ }
101
+
102
+ function pad(str: string, width: number): string {
103
+ return str.padEnd(width);
104
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * auggy stop <name> — stop a running agent.
3
+ *
4
+ * Handles both dev (foreground) and launchd modes:
5
+ * - dev: send SIGTERM, wait, SIGKILL if needed
6
+ * - launchd: launchctl unload, clean up plist + symlink
7
+ */
8
+
9
+ import { unlinkSync } from "node:fs";
10
+ import { $ } from "bun";
11
+ import { readPidManifest, removePidManifest, isProcessAlive } from "../pid-registry";
12
+ import { plistStorePath, plistInstallPath } from "../plist-generator";
13
+
14
+ export async function runStop(name: string): Promise<void> {
15
+ const manifest = readPidManifest(name);
16
+
17
+ if (!manifest) {
18
+ console.log(`Agent "${name}" is not running.`);
19
+ return;
20
+ }
21
+
22
+ if (manifest.mode === "launchd") {
23
+ await stopLaunchd(name, manifest.pid);
24
+ } else {
25
+ await stopDev(name, manifest.pid);
26
+ }
27
+ }
28
+
29
+ async function stopLaunchd(name: string, pid: number): Promise<void> {
30
+ const installPath = plistInstallPath(name);
31
+ const storePath = plistStorePath(name);
32
+
33
+ // Unload the launchd service.
34
+ try {
35
+ await $`launchctl unload ${installPath}`.quiet();
36
+ } catch {}
37
+
38
+ // Wait for the process to exit.
39
+ let waited = 0;
40
+ while (isProcessAlive(pid) && waited < 5000) {
41
+ await Bun.sleep(250);
42
+ waited += 250;
43
+ }
44
+
45
+ // Clean up plist files.
46
+ try {
47
+ unlinkSync(installPath);
48
+ } catch {}
49
+ try {
50
+ unlinkSync(storePath);
51
+ } catch {}
52
+
53
+ // Clean up PID manifest.
54
+ removePidManifest(name);
55
+
56
+ console.log(`Agent "${name}" stopped (was launchd-managed).`);
57
+ }
58
+
59
+ async function stopDev(name: string, pid: number): Promise<void> {
60
+ if (!isProcessAlive(pid)) {
61
+ removePidManifest(name);
62
+ console.log(`Agent "${name}" was not running (stale PID ${pid} cleaned up).`);
63
+ return;
64
+ }
65
+
66
+ // Send SIGTERM for graceful shutdown.
67
+ process.kill(pid, "SIGTERM");
68
+
69
+ // Wait up to 5s for graceful shutdown.
70
+ let waited = 0;
71
+ while (isProcessAlive(pid) && waited < 5000) {
72
+ await Bun.sleep(250);
73
+ waited += 250;
74
+ }
75
+
76
+ // Force kill if still alive.
77
+ if (isProcessAlive(pid)) {
78
+ process.kill(pid, "SIGKILL");
79
+ await Bun.sleep(500);
80
+ }
81
+
82
+ removePidManifest(name);
83
+ console.log(`Agent "${name}" stopped.`);
84
+ }