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,68 @@
1
+ /**
2
+ * Engine resolver — EngineConfig → ModelClient.
3
+ *
4
+ * Maps the `engine.provider` string from agent.yaml to the corresponding
5
+ * engine factory. Supports "anthropic", "openai", and "openrouter".
6
+ *
7
+ * API keys are NEVER in the YAML config — each engine reads its own
8
+ * environment variable:
9
+ * - anthropic → ANTHROPIC_API_KEY (read by the Anthropic SDK)
10
+ * - openai → OPENAI_API_KEY (read by the openai SDK)
11
+ * - openrouter → OPENROUTER_API_KEY (read by createOpenRouterEngine,
12
+ * which throws explicitly if absent rather than letting the SDK fall
13
+ * through to OPENAI_API_KEY)
14
+ */
15
+
16
+ import { createAnthropicEngine } from "../engines/anthropic";
17
+ import { createOpenAIEngine } from "../engines/openai";
18
+ import { createOpenRouterEngine } from "../engines/openrouter";
19
+ import type { ModelClient } from "../types";
20
+ import type { EngineConfig } from "./types";
21
+
22
+ export function resolveEngine(config: EngineConfig): ModelClient {
23
+ // Defensive: programmatic callers may bypass the YAML parser. Catch
24
+ // missing/empty provider with a clearer message than the catch-all throw.
25
+ if (typeof config.provider !== "string" || config.provider.length === 0) {
26
+ throw new Error(`engine.provider is required (got: ${JSON.stringify(config.provider)})`);
27
+ }
28
+
29
+ if (config.provider === "anthropic") {
30
+ return createAnthropicEngine({
31
+ model: config.model,
32
+ maxContextTokens: config.maxContextTokens,
33
+ maxTokens: config.maxTokens,
34
+ baseURL: config.baseURL,
35
+ costOverride: config.costOverride,
36
+ // apiKey intentionally omitted — SDK reads ANTHROPIC_API_KEY from env.
37
+ });
38
+ }
39
+
40
+ if (config.provider === "openai") {
41
+ return createOpenAIEngine({
42
+ model: config.model,
43
+ maxContextTokens: config.maxContextTokens,
44
+ maxTokens: config.maxTokens,
45
+ baseURL: config.baseURL,
46
+ reasoningEffort: config.reasoningEffort,
47
+ costOverride: config.costOverride,
48
+ // apiKey intentionally omitted — SDK reads OPENAI_API_KEY from env.
49
+ });
50
+ }
51
+
52
+ if (config.provider === "openrouter") {
53
+ return createOpenRouterEngine({
54
+ model: config.model,
55
+ maxContextTokens: config.maxContextTokens,
56
+ maxTokens: config.maxTokens,
57
+ reasoningEffort: config.reasoningEffort,
58
+ providerRouting: config.providerRouting,
59
+ costOverride: config.costOverride,
60
+ // baseURL intentionally NOT passed — hardcoded to OpenRouter.
61
+ // apiKey intentionally omitted — engine reads OPENROUTER_API_KEY from env.
62
+ });
63
+ }
64
+
65
+ throw new Error(
66
+ `Unknown engine provider: "${config.provider}" (supported: anthropic, openai, openrouter)`,
67
+ );
68
+ }
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * auggy — CLI for the Auggy agent runtime.
4
+ *
5
+ * Commands:
6
+ * auggy create <name> Scaffold a new agent (interactive)
7
+ * auggy add <name> Add augments to an existing agent
8
+ * auggy add-skill <augment> Install a bundled skill into an agent
9
+ * auggy dev <name> [--config] Run agent in foreground
10
+ * auggy start <name> [--config] Install as launchd service (always-on)
11
+ * auggy stop <name> Stop a running agent
12
+ * auggy restart <name> Stop + start
13
+ * auggy status [name] Show running agents
14
+ * auggy ls List registered agents
15
+ * auggy remove <name> [--yes] Delete an agent (dir + index entry)
16
+ * auggy chat Launch local GUI
17
+ * auggy eval [name] Run portable security eval suite
18
+ */
19
+
20
+ import { Command } from "commander";
21
+ import { runCreate } from "./commands/create";
22
+ import { runAdd } from "./commands/add";
23
+ import { addSkillCommand } from "./commands/add-skill";
24
+ import { runDev } from "./commands/dev";
25
+ import { runStart } from "./commands/start";
26
+ import { runStop } from "./commands/stop";
27
+ import { runRestart } from "./commands/restart";
28
+ import { runStatus } from "./commands/status";
29
+ import { chatCommand } from "./commands/chat";
30
+ import { evalCommand } from "./commands/eval";
31
+ import { runRemove } from "./commands/remove";
32
+ import { runLs } from "./commands/ls";
33
+
34
+ const program = new Command();
35
+
36
+ program.name("auggy").description("Auggy agent runtime CLI").version("0.1.0");
37
+
38
+ program
39
+ .command("create <name>")
40
+ .description("Scaffold a new agent directory (interactive)")
41
+ .option("--dir <path>", "target directory (defaults to ./<name>)")
42
+ .action(async (name: string, opts: { dir?: string }) => {
43
+ try {
44
+ await runCreate(name, opts);
45
+ } catch (err) {
46
+ console.error(`Error: ${(err as Error).message}`);
47
+ process.exit(1);
48
+ }
49
+ });
50
+
51
+ program
52
+ .command("add <name>")
53
+ .description("Add augments to an existing agent")
54
+ .option("--config <path>", "path to agent.yaml")
55
+ .action(async (name: string, opts: { config?: string }) => {
56
+ try {
57
+ await runAdd(name, opts);
58
+ } catch (err) {
59
+ console.error(`Error: ${(err as Error).message}`);
60
+ process.exit(1);
61
+ }
62
+ });
63
+
64
+ program.addCommand(addSkillCommand());
65
+
66
+ program
67
+ .command("dev <name>")
68
+ .description("Run an agent in the foreground (Ctrl-C to stop)")
69
+ .option("--config <path>", "path to agent.yaml")
70
+ .option("--internal-mode <mode>", "(internal) process mode for PID manifest")
71
+ .action(async (name: string, opts: { config?: string; internalMode?: string }) => {
72
+ try {
73
+ await runDev(name, opts);
74
+ } catch (err) {
75
+ console.error(`Error: ${(err as Error).message}`);
76
+ process.exit(1);
77
+ }
78
+ });
79
+
80
+ program
81
+ .command("start <name>")
82
+ .description("Install agent as a launchd service (always-on)")
83
+ .option("--config <path>", "path to agent.yaml")
84
+ .action(async (name: string, opts: { config?: string }) => {
85
+ try {
86
+ await runStart(name, opts);
87
+ } catch (err) {
88
+ console.error(`Error: ${(err as Error).message}`);
89
+ process.exit(1);
90
+ }
91
+ });
92
+
93
+ program
94
+ .command("stop <name>")
95
+ .description("Stop a running agent")
96
+ .action(async (name: string) => {
97
+ try {
98
+ await runStop(name);
99
+ } catch (err) {
100
+ console.error(`Error: ${(err as Error).message}`);
101
+ process.exit(1);
102
+ }
103
+ });
104
+
105
+ program
106
+ .command("restart <name>")
107
+ .description("Stop and restart a running agent")
108
+ .option("--config <path>", "path to agent.yaml")
109
+ .action(async (name: string, opts: { config?: string }) => {
110
+ try {
111
+ await runRestart(name, opts);
112
+ } catch (err) {
113
+ console.error(`Error: ${(err as Error).message}`);
114
+ process.exit(1);
115
+ }
116
+ });
117
+
118
+ program
119
+ .command("status [name]")
120
+ .description("Show running agents or detail a specific one")
121
+ .action(async (name?: string) => {
122
+ try {
123
+ await runStatus(name);
124
+ } catch (err) {
125
+ console.error(`Error: ${(err as Error).message}`);
126
+ process.exit(1);
127
+ }
128
+ });
129
+
130
+ program
131
+ .command("remove <name>")
132
+ .description("Remove an agent (delete dir + clear index entry)")
133
+ .option("--yes", "skip the confirmation prompt")
134
+ .action(async (name: string, opts: { yes?: boolean }) => {
135
+ try {
136
+ await runRemove(name, { yes: opts.yes });
137
+ } catch (err) {
138
+ console.error(`Error: ${(err as Error).message}`);
139
+ process.exit(1);
140
+ }
141
+ });
142
+
143
+ program
144
+ .command("ls")
145
+ .description("List registered agents with their status")
146
+ .action(async () => {
147
+ try {
148
+ await runLs();
149
+ } catch (err) {
150
+ console.error(`Error: ${(err as Error).message}`);
151
+ process.exit(1);
152
+ }
153
+ });
154
+
155
+ program
156
+ .command("visitors <agent>")
157
+ .description("list verified visitors for an agent")
158
+ .option("--revoke <email>", "revoke a verified visitor by email")
159
+ .option("--yes", "skip the confirmation prompt for --revoke")
160
+ .action(async (agentName: string, options: { revoke?: string; yes?: boolean }) => {
161
+ try {
162
+ if (options.revoke) {
163
+ const { runVisitorsRevoke } = await import("./commands/visitors-revoke");
164
+ await runVisitorsRevoke(agentName, options.revoke, { confirm: options.yes !== true });
165
+ return;
166
+ }
167
+ const { runVisitorsList } = await import("./commands/visitors");
168
+ await runVisitorsList(agentName);
169
+ } catch (err) {
170
+ console.error(`Error: ${(err as Error).message}`);
171
+ process.exit(1);
172
+ }
173
+ });
174
+
175
+ program.addCommand(chatCommand());
176
+ program.addCommand(evalCommand());
177
+
178
+ program.parse();
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Model picker — derives `auggy create` model choices from per-provider
3
+ * pricing tables. Single source of truth for "models we can cost-track."
4
+ *
5
+ * The Custom escape hatch is handled in `commands/create.ts`, not here —
6
+ * this module only enumerates priced choices.
7
+ */
8
+
9
+ import * as anthropicPricing from "../engines/anthropic/pricing";
10
+ import * as openaiPricing from "../engines/openai/pricing";
11
+
12
+ export type Provider = "anthropic" | "openai" | "openrouter";
13
+
14
+ export interface ModelChoice {
15
+ /** Model ID as it appears in agent.yaml `engine.model`. */
16
+ id: string;
17
+ inputUsdPerMtok: number;
18
+ outputUsdPerMtok: number;
19
+ }
20
+
21
+ interface PricingTableEntry {
22
+ inputUsdPerMtok: number;
23
+ outputUsdPerMtok: number;
24
+ }
25
+
26
+ /**
27
+ * Read raw pricing tables. We import each provider's pricing module and
28
+ * call `listModels()` + `lookup()` to enumerate priced choices without
29
+ * coupling to the internal table shape.
30
+ */
31
+ function readEntry(provider: Provider, id: string): PricingTableEntry | null {
32
+ if (provider === "anthropic") return anthropicPricing.lookup(id);
33
+ if (provider === "openai") return openaiPricing.lookup(id);
34
+ return null;
35
+ }
36
+
37
+ /**
38
+ * Get the priced model choices for a provider, ordered cheapest-first.
39
+ */
40
+ export function getModelChoices(provider: Provider): ModelChoice[] {
41
+ let pairs: Array<{ id: string; entry: PricingTableEntry }> = [];
42
+
43
+ if (provider === "anthropic") {
44
+ const ids = anthropicPricing.listModels();
45
+ pairs = ids
46
+ .map((id) => ({ id, entry: readEntry(provider, id) }))
47
+ .filter((p): p is { id: string; entry: PricingTableEntry } => p.entry !== null);
48
+ } else if (provider === "openai") {
49
+ const ids = openaiPricing.listModels();
50
+ pairs = ids
51
+ .map((id) => ({ id, entry: readEntry(provider, id) }))
52
+ .filter((p): p is { id: string; entry: PricingTableEntry } => p.entry !== null);
53
+ } else if (provider === "openrouter") {
54
+ const anthropicSlugs = anthropicPricing.listModels().map((id) => ({
55
+ id: `anthropic/${id}`,
56
+ entry: readEntry("anthropic", id),
57
+ }));
58
+ const openaiSlugs = openaiPricing.listModels().map((id) => ({
59
+ id: `openai/${id}`,
60
+ entry: readEntry("openai", id),
61
+ }));
62
+ pairs = [...anthropicSlugs, ...openaiSlugs].filter(
63
+ (p): p is { id: string; entry: PricingTableEntry } => p.entry !== null,
64
+ );
65
+ }
66
+
67
+ return pairs
68
+ .map((p) => ({
69
+ id: p.id,
70
+ inputUsdPerMtok: p.entry.inputUsdPerMtok,
71
+ outputUsdPerMtok: p.entry.outputUsdPerMtok,
72
+ }))
73
+ .sort((a, b) => a.inputUsdPerMtok - b.inputUsdPerMtok);
74
+ }
75
+
76
+ /**
77
+ * Format a choice as: `<id> — $<input>/$<output> per Mtok`.
78
+ *
79
+ * Sub-dollar rates render with two decimals ($0.80, not $0.8); whole-dollar
80
+ * rates render without decimals ($3, not $3.00).
81
+ */
82
+ export function formatChoiceLabel(choice: ModelChoice): string {
83
+ const fmt = (n: number): string => {
84
+ if (Number.isInteger(n)) return `$${n}`;
85
+ if (n < 1) return `$${n.toFixed(2)}`;
86
+ return `$${n}`;
87
+ };
88
+ return `${choice.id} — ${fmt(choice.inputUsdPerMtok)}/${fmt(choice.outputUsdPerMtok)} per Mtok`;
89
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * PID registry — tracks running Auggy agents via JSON manifests.
3
+ *
4
+ * Each running agent gets a manifest at ~/.auggy/<name>.json containing
5
+ * pid, port, config path, start time, and mode. Atomic writes via the
6
+ * "wx" flag prevent concurrent starts of the same agent name.
7
+ *
8
+ * Replicates the PID guard pattern from telemetry-exporter/bin/daemon.ts.
9
+ */
10
+
11
+ import {
12
+ existsSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ readdirSync,
16
+ unlinkSync,
17
+ writeFileSync,
18
+ } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { join } from "node:path";
21
+ import type { PidManifest } from "./types";
22
+
23
+ const AUGGY_DIR = join(homedir(), ".auggy");
24
+ // No time-based staleness heuristic — always-on agents can run for weeks.
25
+ // Liveness is determined solely by whether the PID is alive.
26
+
27
+ function ensureDir(): void {
28
+ mkdirSync(AUGGY_DIR, { recursive: true });
29
+ }
30
+
31
+ function manifestPath(name: string): string {
32
+ return join(AUGGY_DIR, `${name}.json`);
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Process liveness check
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /** Check if a process with the given PID is alive. */
40
+ export function isProcessAlive(pid: number): boolean {
41
+ try {
42
+ process.kill(pid, 0);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // CRUD operations
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Write a PID manifest atomically. Throws EEXIST if the manifest
55
+ * already exists (another instance is running or stale).
56
+ *
57
+ * Call `cleanupStaleManifest` first if you want to recover from a
58
+ * stale PID file before writing.
59
+ */
60
+ export function writePidManifest(manifest: PidManifest): void {
61
+ ensureDir();
62
+ const path = manifestPath(manifest.name);
63
+ writeFileSync(path, JSON.stringify(manifest, null, 2), { flag: "wx" });
64
+ }
65
+
66
+ /** Read a PID manifest. Returns null if not found. */
67
+ export function readPidManifest(name: string): PidManifest | null {
68
+ const path = manifestPath(name);
69
+ if (!existsSync(path)) return null;
70
+ try {
71
+ return JSON.parse(readFileSync(path, "utf-8")) as PidManifest;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ /** Remove a PID manifest (called on clean shutdown). */
78
+ export function removePidManifest(name: string): void {
79
+ const path = manifestPath(name);
80
+ try {
81
+ unlinkSync(path);
82
+ } catch {
83
+ // Already gone — fine.
84
+ }
85
+ }
86
+
87
+ /**
88
+ * List all PID manifests. Dead processes are cleaned up automatically.
89
+ * Returns only manifests whose processes are still alive.
90
+ */
91
+ export function listPidManifests(): PidManifest[] {
92
+ ensureDir();
93
+ const manifests: PidManifest[] = [];
94
+
95
+ for (const file of readdirSync(AUGGY_DIR)) {
96
+ if (!file.endsWith(".json")) continue;
97
+ const path = join(AUGGY_DIR, file);
98
+ try {
99
+ const manifest = JSON.parse(readFileSync(path, "utf-8")) as PidManifest;
100
+ if (isProcessAlive(manifest.pid)) {
101
+ manifests.push(manifest);
102
+ } else {
103
+ // Dead process — clean up the stale manifest.
104
+ try {
105
+ unlinkSync(path);
106
+ } catch {}
107
+ }
108
+ } catch {
109
+ // Corrupt manifest — remove it.
110
+ try {
111
+ unlinkSync(path);
112
+ } catch {}
113
+ }
114
+ }
115
+
116
+ return manifests;
117
+ }
118
+
119
+ /**
120
+ * Try to claim a name for a new agent. If a manifest exists:
121
+ * - If the process is dead, remove the stale manifest and return true.
122
+ * - If the process is alive but older than 24h, treat it as stuck —
123
+ * remove the manifest and return true.
124
+ * - If the process is alive and recent, return false (name is taken).
125
+ */
126
+ export function tryClaimName(name: string): boolean {
127
+ const manifest = readPidManifest(name);
128
+ if (!manifest) return true;
129
+
130
+ if (!isProcessAlive(manifest.pid)) {
131
+ removePidManifest(name);
132
+ return true;
133
+ }
134
+
135
+ // Process is alive — name is taken.
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * Return the path to the auggy directory (~/.auggy/).
141
+ * Used by the plist generator for log paths.
142
+ */
143
+ export function getAuggyDir(): string {
144
+ ensureDir();
145
+ return AUGGY_DIR;
146
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * launchd plist generator — creates a macOS plist that keeps an
3
+ * Auggy agent alive via launchd.
4
+ *
5
+ * The plist invokes `auggy dev <name>` (the foreground runner) and
6
+ * lets launchd handle daemonization, restart, and log routing.
7
+ *
8
+ * Pattern replicated from telemetry-exporter/com.lo.telemetry-exporter.plist.
9
+ */
10
+
11
+ import { homedir } from "node:os";
12
+ import { dirname, join } from "node:path";
13
+
14
+ export interface PlistOptions {
15
+ /** Agent name (used in label and log file names). */
16
+ name: string;
17
+ /** Absolute path to the agent directory. */
18
+ agentDir: string;
19
+ /** Absolute path to agent.yaml. */
20
+ configPath: string;
21
+ /** Absolute path to the bun binary. */
22
+ bunPath: string;
23
+ /** Absolute path to the auggy CLI entrypoint (src/cli/index.ts). */
24
+ cliEntryPoint: string;
25
+ }
26
+
27
+ /** The launchd label for an agent. */
28
+ export function plistLabel(name: string): string {
29
+ return `com.auggy.agent.${name}`;
30
+ }
31
+
32
+ /** Where generated plists are stored. */
33
+ export function plistStorePath(name: string): string {
34
+ return join(homedir(), ".auggy", "plists", `${plistLabel(name)}.plist`);
35
+ }
36
+
37
+ /** Where the symlink goes in ~/Library/LaunchAgents/. */
38
+ export function plistInstallPath(name: string): string {
39
+ return join(homedir(), "Library", "LaunchAgents", `${plistLabel(name)}.plist`);
40
+ }
41
+
42
+ /** Log directory for agent stdout/stderr. */
43
+ export function logDir(): string {
44
+ return join(homedir(), ".auggy", "logs");
45
+ }
46
+
47
+ function escapeXml(s: string): string {
48
+ return s
49
+ .replaceAll("&", "&amp;")
50
+ .replaceAll("<", "&lt;")
51
+ .replaceAll(">", "&gt;")
52
+ .replaceAll('"', "&quot;")
53
+ .replaceAll("'", "&apos;");
54
+ }
55
+
56
+ /**
57
+ * Generate a launchd plist XML string for an Auggy agent.
58
+ *
59
+ * The plist runs `bun run <cliEntryPoint> dev <name> --config <configPath>`
60
+ * with KeepAlive=true so launchd restarts on crash.
61
+ */
62
+ export function generatePlist(opts: PlistOptions): string {
63
+ const label = plistLabel(opts.name);
64
+ const bunDir = dirname(opts.bunPath);
65
+ const home = homedir();
66
+ const logs = logDir();
67
+
68
+ return `<?xml version="1.0" encoding="UTF-8"?>
69
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
70
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
71
+ <plist version="1.0">
72
+ <dict>
73
+ <key>Label</key>
74
+ <string>${escapeXml(label)}</string>
75
+
76
+ <key>ProgramArguments</key>
77
+ <array>
78
+ <string>${escapeXml(opts.bunPath)}</string>
79
+ <string>run</string>
80
+ <string>${escapeXml(opts.cliEntryPoint)}</string>
81
+ <string>dev</string>
82
+ <string>${escapeXml(opts.name)}</string>
83
+ <string>--config</string>
84
+ <string>${escapeXml(opts.configPath)}</string>
85
+ <string>--internal-mode</string>
86
+ <string>launchd</string>
87
+ </array>
88
+
89
+ <key>WorkingDirectory</key>
90
+ <string>${escapeXml(opts.agentDir)}</string>
91
+
92
+ <key>KeepAlive</key>
93
+ <true/>
94
+
95
+ <key>ThrottleInterval</key>
96
+ <integer>10</integer>
97
+
98
+ <key>RunAtLoad</key>
99
+ <true/>
100
+
101
+ <key>StandardOutPath</key>
102
+ <string>${escapeXml(join(logs, `${opts.name}.log`))}</string>
103
+
104
+ <key>StandardErrorPath</key>
105
+ <string>${escapeXml(join(logs, `${opts.name}.err`))}</string>
106
+
107
+ <key>EnvironmentVariables</key>
108
+ <dict>
109
+ <key>PATH</key>
110
+ <string>${escapeXml(bunDir)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
111
+ <key>HOME</key>
112
+ <string>${escapeXml(home)}</string>
113
+ </dict>
114
+ </dict>
115
+ </plist>
116
+ `;
117
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Shared config path resolution for CLI commands.
3
+ *
4
+ * Resolution order:
5
+ * 1. Explicit --config <path> flag
6
+ * 2. Look up <name> in `~/.auggy/agents.json` (the index)
7
+ *
8
+ * The legacy CWD-relative fallback (`./<name>/agent.yaml`, `./agent.yaml`)
9
+ * was removed in ADR-021. Pre-ADR-021 agents are not auto-discovered;
10
+ * adoption is deferred until concrete demand.
11
+ */
12
+
13
+ import { existsSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import { getAgent } from "./agent-index";
16
+
17
+ interface ResolveOptions {
18
+ /** Override `~/.auggy/` for tests. */
19
+ auggyDir?: string;
20
+ }
21
+
22
+ /**
23
+ * Resolve the config file path from explicit flag or the agent index.
24
+ */
25
+ export function resolveConfigPath(
26
+ name: string,
27
+ configFlag?: string,
28
+ opts: ResolveOptions = {},
29
+ ): string {
30
+ if (configFlag) {
31
+ const absPath = resolve(configFlag);
32
+ if (!existsSync(absPath)) {
33
+ throw new Error(`Config file not found: ${absPath}`);
34
+ }
35
+ return absPath;
36
+ }
37
+
38
+ const entry = getAgent(name, opts);
39
+ if (!entry) {
40
+ throw new Error(
41
+ `Agent "${name}" is not registered.\n\n` +
42
+ ` Run \`auggy create ${name}\` to scaffold a new agent,\n` +
43
+ ` or \`auggy ls\` to see registered agents.`,
44
+ );
45
+ }
46
+
47
+ const cfg = join(entry.localDir, "agent.yaml");
48
+ if (!existsSync(cfg)) {
49
+ throw new Error(
50
+ `agent.yaml missing at indexed path: ${cfg}\n\n` +
51
+ ` The agent directory may have been deleted or moved manually.\n` +
52
+ ` Run \`auggy remove ${name}\` to clean up the index entry.`,
53
+ );
54
+ }
55
+ return cfg;
56
+ }